const { useState, useRef, useEffect } = React; window.AdminAjustes = function({ isMaster, settings = {}, refreshData, companyLogoUrl, setCompanyLogoUrl, showCompanyLogo, setShowCompanyLogo, inssRate, setInssRate, vacRate, setVacRate, lunchLimitMinutes, setLunchLimitMinutes, gracePeriodMinutes, setGracePeriodMinutes, maxTardinessAllowed, setMaxTardinessAllowed, tardinessPenaltyAction, setTardinessPenaltyAction, saveCompanySettings, newReasonInput, setNewReasonInput, setReasonsList, reasonsList, actions = [], roles = [], showToast }) { // DATOS DE LA EMPRESA / FISCALES const [companyName, setCompanyName] = useState(settings?.companyName || window.SHIFT_CONFIG?.companyName || ''); const [taxRegime, setTaxRegime] = useState(settings?.taxRegime || 'cuota_fija'); const [rucNumber, setRucNumber] = useState(settings?.rucNumber || ''); const [ownerName, setOwnerName] = useState(settings?.ownerName || ''); const [legalRepId, setLegalRepId] = useState(settings?.legalRepId || ''); const [businessAddress, setBusinessAddress] = useState(settings?.businessAddress || ''); const [municipality, setMunicipality] = useState(settings?.municipality || 'Managua'); // CORTES DE PLANILLA const [cutStartDayQ1, setCutStartDayQ1] = useState(settings?.cutStartDayQ1 || 1); const [cutEndDayQ1, setCutEndDayQ1] = useState(settings?.cutEndDayQ1 || 15); const [cutStartDayQ2, setCutStartDayQ2] = useState(settings?.cutStartDayQ2 || 16); const [cutEndDayQ2, setCutEndDayQ2] = useState(settings?.cutEndDayQ2 || 30); // INATEC const [appliesInatec, setAppliesInatec] = useState(settings?.appliesInatec !== false); // GEOCERCAS Y ASISTENCIA GPS const [geofenceEnabled, setGeofenceEnabled] = useState(Boolean(settings?.geofenceEnabled)); const [geofenceLat, setGeofenceLat] = useState(settings?.geofenceLat !== null && settings?.geofenceLat !== undefined ? settings.geofenceLat : ''); const [geofenceLng, setGeofenceLng] = useState(settings?.geofenceLng !== null && settings?.geofenceLng !== undefined ? settings.geofenceLng : ''); const [geofenceRadiusMeters, setGeofenceRadiusMeters] = useState(settings?.geofenceRadiusMeters || 150); const [geofenceStrictMode, setGeofenceStrictMode] = useState(Boolean(settings?.geofenceStrictMode)); const [isGettingGps, setIsGettingGps] = useState(false); const [gpsFeedback, setGpsFeedback] = useState(''); // DISPERSION BANCARIA DE PLANILLA const [companyDebitAccount, setCompanyDebitAccount] = useState(settings?.companyDebitAccount || ''); const [companyDefaultBank, setCompanyDefaultBank] = useState(settings?.companyDefaultBank || 'BAC Credomatic'); const [companyBankAccounts, setCompanyBankAccounts] = useState(settings?.companyBankAccounts || [ { id: 1, bankName: 'BAC Credomatic', accountNumber: settings?.companyDebitAccount || '365829101', currency: 'NIO', type: 'Planilla' } ]); const [newBankName, setNewBankName] = useState('BAC Credomatic'); const [newBankAccountNum, setNewBankAccountNum] = useState(''); const [newBankCurrency, setNewBankCurrency] = useState('NIO'); const [newBankType, setNewBankType] = useState('Planilla'); // RECORTE DE LOGO const [showCropModal, setShowCropModal] = useState(false); const [rawImageSrc, setRawImageSrc] = useState(null); const [zoomScale, setZoomScale] = useState(1); const [panOffset, setPanOffset] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const [isSavingLogo, setIsSavingLogo] = useState(false); // GOOGLE GEMINI API const [geminiApiKey, setGeminiApiKey] = useState(settings?.geminiApiKey || ''); const [showGeminiKey, setShowGeminiKey] = useState(false); const [testingGemini, setTestingGemini] = useState(false); const [savingGemini, setSavingGemini] = useState(false); const [geminiStatus, setGeminiStatus] = useState(null); // NOTIFICACIONES EMPRESARIALES (WHATSAPP & EMAIL) const [useGlobalWa, setUseGlobalWa] = useState(settings?.use_global_whatsapp !== false); const [useGlobalSmtp, setUseGlobalSmtp] = useState(settings?.use_global_smtp !== false); const [waCustomToken, setWaCustomToken] = useState(settings?.whatsapp_access_token || ''); const [waCustomPhoneId, setWaCustomPhoneId] = useState(settings?.whatsapp_phone_number_id || ''); const [waCustomWabaId, setWaCustomWabaId] = useState(settings?.whatsapp_waba_id || ''); const [smtpHost, setSmtpHost] = useState(settings?.smtp_host || ''); const [smtpPort, setSmtpPort] = useState(settings?.smtp_port || 587); const [smtpUser, setSmtpUser] = useState(settings?.smtp_user || ''); const [smtpPass, setSmtpPass] = useState(settings?.smtp_pass || ''); const [smtpFromEmail, setSmtpFromEmail] = useState(settings?.smtp_from_email || ''); const [smtpFromName, setSmtpFromName] = useState(settings?.smtp_from_name || 'Shift ERP'); const [smtpSecure, setSmtpSecure] = useState(settings?.smtp_secure || 'tls'); const [notifyPayslipsWa, setNotifyPayslipsWa] = useState(settings?.notify_payslips_whatsapp !== false); const [notifyPayslipsEmail, setNotifyPayslipsEmail] = useState(settings?.notify_payslips_email !== false); const [notifyTardinessWa, setNotifyTardinessWa] = useState(settings?.notify_tardiness_whatsapp !== false); const [notifyTardinessEmail, setNotifyTardinessEmail] = useState(settings?.notify_tardiness_email !== false); const [notifyOwnerTardinessWa, setNotifyOwnerTardinessWa] = useState(settings?.notify_owner_tardiness_whatsapp !== false); const [notifyWelcomeWa, setNotifyWelcomeWa] = useState(settings?.notify_welcome_whatsapp !== false); const [noshowMode, setNoshowMode] = useState(settings?.noshowMode || 'tolerance'); const [noshowToleranceMinutes, setNoshowToleranceMinutes] = useState(settings?.noshowToleranceMinutes ?? 30); const defaultMatrix = { tardiness: { wa_emp: true, email_emp: false, wa_sup: true, email_sup: false, wa_owner: false, email_owner: false }, noshow: { wa_emp: false, email_emp: false, wa_sup: true, email_sup: true, wa_owner: true, email_owner: true }, open_shift: { wa_emp: true, email_emp: false, wa_sup: true, email_sup: false, wa_owner: false, email_owner: true }, overtime: { wa_emp: true, email_emp: true, wa_sup: false, email_sup: false, wa_owner: true, email_owner: true }, payslip: { wa_emp: true, email_emp: true, wa_sup: false, email_sup: false, wa_owner: true, email_owner: true }, welcome: { wa_emp: true, email_emp: true, wa_sup: false, email_sup: false, wa_owner: false, email_owner: false } }; const [notifMatrix, setNotifMatrix] = useState(settings?.notificationMatrix || defaultMatrix); // DÍAS FERIADOS DE LA EMPRESA const [companyHolidays, setCompanyHolidays] = useState(settings?.holidays || []); const [showHolidayModal, setShowHolidayModal] = useState(false); const [editingHoliday, setEditingHoliday] = useState(null); const [holidayNameInput, setHolidayNameInput] = useState(''); const [holidayDateInput, setHolidayDateInput] = useState(''); const [holidayMandatoryInput, setHolidayMandatoryInput] = useState(true); const [isAuditingNoShows, setIsAuditingNoShows] = useState(false); const [testWaPhone, setTestWaPhone] = useState(''); const [testEmailAddr, setTestEmailAddr] = useState(''); const [testingCompanyWa, setTestingCompanyWa] = useState(false); const [testingCompanyEmail, setTestingCompanyEmail] = useState(false); const [testFeedback, setTestFeedback] = useState(''); // NAVEGACIÓN POR PESTAÑAS (TAB SYSTEM) const [activeTab, setActiveTab] = useState('empresa'); // 'empresa' | 'nomina' | 'asistencia' | 'notificaciones' | 'ia' // ESTADO GENERAL DE GUARDADO const [isSavingAll, setIsSavingAll] = useState(false); const [saveFeedback, setSaveFeedback] = useState(''); // TRABAJADORES ASEGURADOS const [insuredWorkersCount, setInsuredWorkersCount] = useState(0); // MOTIVOS DE EXCEPCION const [localReasonInput, setLocalReasonInput] = useState(''); const canvasRef = useRef(null); const imageObjRef = useRef(null); const mapContainerRef = useRef(null); const leafletMapRef = useRef(null); const leafletMarkerRef = useRef(null); const leafletCircleRef = useRef(null); const hasInitializedRef = useRef(false); const toggleMatrixCell = (eventKey, cellKey) => { setNotifMatrix(prev => { const currentEvent = prev?.[eventKey] || defaultMatrix[eventKey] || {}; const currentValue = Boolean(currentEvent[cellKey]); return { ...prev, [eventKey]: { ...currentEvent, [cellKey]: !currentValue } }; }); }; const handleSaveHoliday = async () => { if (!holidayNameInput.trim() || !holidayDateInput.trim()) { return alert("Por favor ingresa el nombre y la fecha del día feriado."); } try { const res = await window.apiCall('save_holiday', { id: editingHoliday ? editingHoliday.id : null, name: holidayNameInput.trim(), date: holidayDateInput.trim(), isMandatory: holidayMandatoryInput }); if (res && res.success) { setShowHolidayModal(false); setEditingHoliday(null); setHolidayNameInput(''); setHolidayDateInput(''); if (refreshData) await refreshData(); } else { alert(res?.error || "Error al guardar el día feriado."); } } catch (e) { alert("Error de conexión al guardar feriado."); } }; const handleDeleteHoliday = async (hId) => { if (!confirm("¿Deseas eliminar este día feriado de la empresa?")) return; try { const res = await window.apiCall('delete_holiday', { id: hId }); if (res && res.success && refreshData) await refreshData(); } catch (e) {} }; const handleLoadNicaraguaHolidays = async () => { if (!confirm("¿Deseas cargar la lista de feriados nacionales oficiales de Nicaragua para este año?")) return; try { const res = await window.apiCall('seed_nicaragua_holidays'); if (res && res.success) { if (refreshData) await refreshData(); alert(res.message || "Feriados nacionales de Nicaragua cargados exitosamente."); } else { alert(res?.error || "Error al cargar feriados nacionales."); } } catch (e) { alert("Error de conexión al cargar feriados nacionales."); } }; const handleRunNoShowAudit = async () => { setIsAuditingNoShows(true); try { const todayNIC = window.getNICDate ? window.getNICDate() : new Date().toISOString().split('T')[0]; const res = await window.apiCall('audit_noshow_absences', { date: todayNIC }); if (res && res.success) { const count = res.detected_no_shows?.length || 0; if (count === 0) { alert(`Auditoría completada: Todos los colaboradores programados para hoy (${res.target_date}) registraron su entrada o tienen feriado/vacaciones activas.`); } else { const names = res.detected_no_shows.map(m => `• ${m.name} (Horario: ${m.schedule_start} - ${m.schedule_end})`).join('\n'); alert(`Se detectaron ${count} colaborador(es) sin presentarse a laborar (No-Show):\n\n${names}\n\nSe enviaron ${res.alerts_dispatched} avisos a sus supervisores/propietario según la matriz.`); } } else { alert(res?.error || "No se pudo realizar la auditoría de No-Show."); } } catch (e) { alert("Error de conexión al auditar ausencias."); } finally { setIsAuditingNoShows(false); } }; const prevCompanyKeyRef = useRef(null); // Sincronizar datos de configuración únicamente al montar el componente o al cambiar de empresa useEffect(() => { if (!settings || Object.keys(settings).length === 0) return; const currentCompanyKey = settings.companyName || settings.rucNumber || 'default_company'; const isNewCompany = prevCompanyKeyRef.current !== currentCompanyKey; if (!hasInitializedRef.current || isNewCompany) { hasInitializedRef.current = true; prevCompanyKeyRef.current = currentCompanyKey; if (settings.companyName) setCompanyName(settings.companyName); if (settings.taxRegime) setTaxRegime(settings.taxRegime); if (settings.rucNumber !== undefined && settings.rucNumber !== null) setRucNumber(settings.rucNumber); if (settings.ownerName !== undefined && settings.ownerName !== null) setOwnerName(settings.ownerName); if (settings.legalRepId !== undefined && settings.legalRepId !== null) setLegalRepId(settings.legalRepId); if (settings.businessAddress !== undefined && settings.businessAddress !== null) setBusinessAddress(settings.businessAddress); if (settings.municipality !== undefined && settings.municipality !== null) setMunicipality(settings.municipality); if (settings.logoUrl && typeof setCompanyLogoUrl === 'function') setCompanyLogoUrl(settings.logoUrl); if (settings.cutStartDayQ1 !== undefined) setCutStartDayQ1(settings.cutStartDayQ1); if (settings.cutEndDayQ1 !== undefined) setCutEndDayQ1(settings.cutEndDayQ1); if (settings.cutStartDayQ2 !== undefined) setCutStartDayQ2(settings.cutStartDayQ2); if (settings.cutEndDayQ2 !== undefined) setCutEndDayQ2(settings.cutEndDayQ2); if (settings.appliesInatec !== undefined) setAppliesInatec(settings.appliesInatec); if (settings.geminiApiKey !== undefined) setGeminiApiKey(settings.geminiApiKey); if (settings.geofenceEnabled !== undefined) setGeofenceEnabled(Boolean(settings.geofenceEnabled)); if (settings.geofenceLat !== undefined && settings.geofenceLat !== null) setGeofenceLat(settings.geofenceLat); if (settings.geofenceLng !== undefined && settings.geofenceLng !== null) setGeofenceLng(settings.geofenceLng); if (settings.geofenceRadiusMeters !== undefined) setGeofenceRadiusMeters(settings.geofenceRadiusMeters); if (settings.geofenceStrictMode !== undefined) setGeofenceStrictMode(Boolean(settings.geofenceStrictMode)); if (settings.companyDebitAccount !== undefined) setCompanyDebitAccount(settings.companyDebitAccount); if (settings.companyDefaultBank !== undefined) setCompanyDefaultBank(settings.companyDefaultBank); if (settings.companyBankAccounts !== undefined && Array.isArray(settings.companyBankAccounts)) setCompanyBankAccounts(settings.companyBankAccounts); if (settings.use_global_whatsapp !== undefined) setUseGlobalWa(Boolean(settings.use_global_whatsapp)); if (settings.use_global_smtp !== undefined) setUseGlobalSmtp(Boolean(settings.use_global_smtp)); if (settings.whatsapp_access_token !== undefined) setWaCustomToken(settings.whatsapp_access_token); if (settings.whatsapp_phone_number_id !== undefined) setWaCustomPhoneId(settings.whatsapp_phone_number_id); if (settings.whatsapp_waba_id !== undefined) setWaCustomWabaId(settings.whatsapp_waba_id); if (settings.smtp_host !== undefined) setSmtpHost(settings.smtp_host); if (settings.smtp_port !== undefined) setSmtpPort(settings.smtp_port); if (settings.smtp_user !== undefined) setSmtpUser(settings.smtp_user); if (settings.smtp_pass !== undefined) setSmtpPass(settings.smtp_pass); if (settings.smtp_from_email !== undefined) setSmtpFromEmail(settings.smtp_from_email); if (settings.smtp_from_name !== undefined) setSmtpFromName(settings.smtp_from_name); if (settings.smtp_secure !== undefined) setSmtpSecure(settings.smtp_secure); if (settings.notify_payslips_whatsapp !== undefined) setNotifyPayslipsWa(Boolean(settings.notify_payslips_whatsapp)); if (settings.notify_payslips_email !== undefined) setNotifyPayslipsEmail(Boolean(settings.notify_payslips_email)); if (settings.notify_tardiness_whatsapp !== undefined) setNotifyTardinessWa(Boolean(settings.notify_tardiness_whatsapp)); if (settings.notify_tardiness_email !== undefined) setNotifyTardinessEmail(Boolean(settings.notify_tardiness_email)); if (settings.notify_owner_tardiness_whatsapp !== undefined) setNotifyOwnerTardinessWa(Boolean(settings.notify_owner_tardiness_whatsapp)); if (settings.notify_welcome_whatsapp !== undefined) setNotifyWelcomeWa(Boolean(settings.notify_welcome_whatsapp)); if (settings.noshowMode !== undefined) setNoshowMode(settings.noshowMode); if (settings.noshowToleranceMinutes !== undefined) setNoshowToleranceMinutes(settings.noshowToleranceMinutes); if (settings.notificationMatrix) { setNotifMatrix(prev => ({ ...defaultMatrix, ...(settings.notificationMatrix || {}) })); } if (settings.holidays) setCompanyHolidays(settings.holidays); } }, [settings]); const handleTestCompanyWa = async () => { const phone = testWaPhone.trim(); if (!phone) return alert("Ingresa un número de teléfono para enviar la prueba de WhatsApp."); setTestingCompanyWa(true); setTestFeedback(''); try { const res = await window.apiCall('test_whatsapp_gateway', { phone }); if (res && res.success) { setTestFeedback(`¡Mensaje de prueba de WhatsApp enviado con éxito al número +${res.recipient || phone}!`); } else { setTestFeedback(`Error en la prueba de WhatsApp: ${res?.error || 'No se pudo entregar'}`); } } catch (err) { setTestFeedback('Error de red: ' + err.message); } finally { setTestingCompanyWa(false); } }; const handleTestCompanyEmail = async () => { const email = testEmailAddr.trim(); if (!email) return alert("Ingresa un correo electrónico para enviar el email de prueba."); setTestingCompanyEmail(true); setTestFeedback(''); try { const res = await window.apiCall('test_email_gateway', { email }); if (res && res.success) { setTestFeedback(`¡Correo electrónico de prueba enviado con éxito a ${res.recipient || email}!`); } else { setTestFeedback(`Error en prueba de correo: ${res?.error || 'Fallo de entrega'}`); } } catch (err) { setTestFeedback('Error de red: ' + err.message); } finally { setTestingCompanyEmail(false); } }; // Cargar número de asegurados para el cálculo automático de INSS patronal useEffect(() => { const loadInitialStats = async () => { try { const res = await window.apiCall('get_initial_data', {}); if (res && res.success && res.data) { if (res.data.users && Array.isArray(res.data.users)) { const insured = res.data.users.filter(u => u.status === 'activo' && u.appliesInss !== false).length; setInsuredWorkersCount(insured); } } } catch (e) {} }; loadInitialStats(); }, []); // Captura satelital de la ubicación actual const handleGetCurrentLocation = () => { if (!navigator.geolocation) { setGpsFeedback('Tu dispositivo o navegador no soporta geolocalización GPS.'); return; } setIsGettingGps(true); setGpsFeedback('Obteniendo coordenadas satelitales en tiempo real...'); navigator.geolocation.getCurrentPosition( (pos) => { const lat = Number(pos.coords.latitude.toFixed(6)); const lng = Number(pos.coords.longitude.toFixed(6)); setGeofenceLat(lat); setGeofenceLng(lng); setIsGettingGps(false); setGpsFeedback(`Ubicación capturada con éxito (Precisión: ±${Math.round(pos.coords.accuracy)} metros)`); setTimeout(() => setGpsFeedback(''), 4500); }, (err) => { setIsGettingGps(false); setGpsFeedback('Error GPS: ' + (err.message || 'Permiso denegado')); setTimeout(() => setGpsFeedback(''), 4500); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } ); }; // Actualización reactiva del mapa perimetral Leaflet useEffect(() => { if (!geofenceEnabled || !mapContainerRef.current || typeof window.L === 'undefined') return; const defaultLat = (geofenceLat !== '' && geofenceLat !== null && !isNaN(geofenceLat)) ? parseFloat(geofenceLat) : 12.136389; const defaultLng = (geofenceLng !== '' && geofenceLng !== null && !isNaN(geofenceLng)) ? parseFloat(geofenceLng) : -86.251389; // Si el mapa ya existía en un contenedor anterior desmontado, destruirlo limpiamente if (leafletMapRef.current) { try { if (leafletMapRef.current.getContainer() !== mapContainerRef.current) { leafletMapRef.current.remove(); leafletMapRef.current = null; leafletMarkerRef.current = null; leafletCircleRef.current = null; } } catch(e) { leafletMapRef.current = null; } } if (!leafletMapRef.current) { const m = window.L.map(mapContainerRef.current, { center: [defaultLat, defaultLng], zoom: 16, zoomControl: true }); window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(m); leafletMapRef.current = m; m.on('click', (e) => { const newLat = Number(e.latlng.lat.toFixed(6)); const newLng = Number(e.latlng.lng.toFixed(6)); setGeofenceLat(newLat); setGeofenceLng(newLng); }); } const currentMap = leafletMapRef.current; if (currentMap) { const pos = [defaultLat, defaultLng]; if (!leafletMarkerRef.current) { leafletMarkerRef.current = window.L.marker(pos, { draggable: true }).addTo(currentMap); leafletMarkerRef.current.on('dragend', (e) => { const ll = e.target.getLatLng(); setGeofenceLat(Number(ll.lat.toFixed(6))); setGeofenceLng(Number(ll.lng.toFixed(6))); }); } else { leafletMarkerRef.current.setLatLng(pos); } if (!leafletCircleRef.current) { leafletCircleRef.current = window.L.circle(pos, { radius: parseInt(geofenceRadiusMeters) || 150, color: '#4f46e5', fillColor: '#6366f1', fillOpacity: 0.2, weight: 2 }).addTo(currentMap); } else { leafletCircleRef.current.setLatLng(pos); leafletCircleRef.current.setRadius(parseInt(geofenceRadiusMeters) || 150); } currentMap.setView(pos, currentMap.getZoom() || 16); setTimeout(() => { if (leafletMapRef.current) { leafletMapRef.current.invalidateSize(); } }, 300); } }, [geofenceEnabled, geofenceLat, geofenceLng, geofenceRadiusMeters]); // Limpieza de mapa al desmontar componente useEffect(() => { return () => { if (leafletMapRef.current) { try { leafletMapRef.current.remove(); } catch(e) {} leafletMapRef.current = null; } }; }, []); // Manejo de carga de imagen para logo const handleFileSelect = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { const img = new Image(); img.onload = () => { imageObjRef.current = img; setRawImageSrc(ev.target.result); setZoomScale(1); setPanOffset({ x: 0, y: 0 }); setShowCropModal(true); }; img.src = ev.target.result; }; reader.readAsDataURL(file); }; const drawCanvas = () => { const canvas = canvasRef.current; const img = imageObjRef.current; if (!canvas || !img) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.save(); ctx.translate(canvas.width / 2 + panOffset.x, canvas.height / 2 + panOffset.y); ctx.scale(zoomScale, zoomScale); const hRatio = (canvas.width * 0.85) / img.width; const vRatio = (canvas.height * 0.85) / img.height; const ratio = Math.min(hRatio, vRatio); const drawW = img.width * ratio; const drawH = img.height * ratio; ctx.drawImage(img, -drawW / 2, -drawH / 2, drawW, drawH); ctx.restore(); }; useEffect(() => { if (showCropModal) { drawCanvas(); } }, [showCropModal, zoomScale, panOffset]); const handleMouseDown = (e) => { setIsDragging(true); setDragStart({ x: e.clientX - panOffset.x, y: e.clientY - panOffset.y }); }; const handleMouseMove = (e) => { if (!isDragging) return; setPanOffset({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); }; const handleMouseUp = () => setIsDragging(false); const handleApplyCropAndSave = async () => { const canvas = canvasRef.current; if (!canvas) return; setIsSavingLogo(true); const croppedBase64 = canvas.toDataURL('image/png', 0.95); try { const res = await window.apiCall('upload_company_logo', { logo_base64: croppedBase64 }); if (res && res.success) { const finalUrl = res.logo_url; if (typeof setCompanyLogoUrl === 'function') { setCompanyLogoUrl(finalUrl); } if (window.SHIFT_CONFIG) { window.SHIFT_CONFIG.companyLogo = finalUrl; } setShowCropModal(false); } else { alert("Error al guardar el logo: " + (res?.error || "Error desconocido")); } } catch (err) { console.error("Error al subir logo:", err); } finally { setIsSavingLogo(false); } }; const handleToggleShowLogo = async (e) => { const newValue = e.target.checked; if (typeof setShowCompanyLogo === 'function') { setShowCompanyLogo(newValue); } if (typeof saveCompanySettings === 'function') { await saveCompanySettings({ show_company_logo: newValue ? 1 : 0 }); } }; // Agregar y eliminar motivos de excepción const handleAddReason = () => { const val = localReasonInput.trim(); if (!val) return; const currentList = Array.isArray(reasonsList) ? reasonsList : []; if (currentList.includes(val)) return; const updated = [...currentList, val]; if (typeof setReasonsList === 'function') { setReasonsList(updated); } setLocalReasonInput(''); }; const handleRemoveReason = (reasonToRemove) => { const currentList = Array.isArray(reasonsList) ? reasonsList : []; const updated = currentList.filter(r => r !== reasonToRemove); if (typeof setReasonsList === 'function') { setReasonsList(updated); } }; // Test de conexión Google Gemini const handleTestGeminiKey = async () => { if (!geminiApiKey.trim()) { setGeminiStatus({ type: 'error', text: 'Ingresa una API Key antes de probar la conexión.' }); return; } setTestingGemini(true); setGeminiStatus(null); try { const res = await window.apiCall('ask_ai_copilot', { prompt: 'Responde únicamente: Conexión confirmada.', customApiKey: geminiApiKey.trim() }); if (res && res.success && res.response) { setGeminiStatus({ type: 'success', text: 'Conexión con Gemini verificada correctamente.' }); } else { setGeminiStatus({ type: 'error', text: res?.error || 'Error al conectar con la API de Gemini.' }); } } catch (err) { setGeminiStatus({ type: 'error', text: 'Error de red: ' + (err.message || 'Fallo en la solicitud') }); } finally { setTestingGemini(false); } }; const handleSaveGeminiKey = async () => { if (!geminiApiKey.trim()) { setGeminiStatus({ type: 'error', text: 'Ingresa una clave válida antes de guardar.' }); return; } setSavingGemini(true); try { if (typeof saveCompanySettings === 'function') { await saveCompanySettings({ gemini_api_key: geminiApiKey.trim() }); setGeminiStatus({ type: 'success', text: 'Clave guardada exitosamente.' }); setTimeout(() => setGeminiStatus(null), 4000); } } catch (e) { setGeminiStatus({ type: 'error', text: 'Error al guardar la clave de Gemini.' }); } finally { setSavingGemini(false); } }; const autoRate = insuredWorkersCount >= 50 ? 22.5 : 21.5; // Guardado general centralizado const handleSaveAllSettings = async () => { setIsSavingAll(true); setSaveFeedback(''); try { const patronalValue = inssRate !== undefined ? parseFloat(inssRate) : parseFloat(document.getElementById('setting_inss_patronal_rate')?.value || autoRate); const vacationValue = vacRate !== undefined ? parseFloat(vacRate) : parseFloat(document.getElementById('setting_vacation_rate')?.value || 2.5); if (typeof saveCompanySettings === 'function') { const res = await saveCompanySettings({ company_name: companyName.trim(), tax_regime: taxRegime, ruc_number: rucNumber.trim(), owner_name: ownerName.trim(), legal_rep_id: legalRepId.trim(), business_address: businessAddress.trim(), municipality: municipality.trim(), company_debit_account: companyDebitAccount.trim(), company_default_bank: companyDefaultBank, company_bank_accounts: companyBankAccounts, geofence_enabled: geofenceEnabled ? 1 : 0, geofence_lat: (geofenceLat !== '' && geofenceLat !== null) ? parseFloat(geofenceLat) : null, geofence_lng: (geofenceLng !== '' && geofenceLng !== null) ? parseFloat(geofenceLng) : null, geofence_radius_meters: parseInt(geofenceRadiusMeters) || 150, geofence_strict_mode: geofenceStrictMode ? 1 : 0, cut_start_q1: parseInt(cutStartDayQ1) || 1, cut_end_q1: parseInt(cutEndDayQ1) || 15, cut_start_q2: parseInt(cutStartDayQ2) || 16, cut_end_q2: parseInt(cutEndDayQ2) || 30, applies_inatec: appliesInatec ? 1 : 0, inss_patronal_rate: patronalValue, vacation_rate: vacationValue, lunch_limit_minutes: parseInt(lunchLimitMinutes) || 60, grace_period_minutes: parseInt(gracePeriodMinutes) || 5, max_tardiness_allowed: parseInt(maxTardinessAllowed) || 3, tardiness_penalty_action: tardinessPenaltyAction || 'deduct_minutes', gemini_api_key: geminiApiKey.trim(), use_global_whatsapp: useGlobalWa ? 1 : 0, use_global_smtp: useGlobalSmtp ? 1 : 0, whatsapp_access_token: waCustomToken.trim(), whatsapp_phone_number_id: waCustomPhoneId.trim(), whatsapp_waba_id: waCustomWabaId.trim(), smtp_host: smtpHost.trim(), smtp_port: parseInt(smtpPort) || 587, smtp_user: smtpUser.trim(), smtp_pass: smtpPass.trim(), smtp_from_email: smtpFromEmail.trim(), smtp_from_name: smtpFromName.trim(), smtp_secure: smtpSecure, notify_payslips_whatsapp: notifyPayslipsWa ? 1 : 0, notify_payslips_email: notifyPayslipsEmail ? 1 : 0, notify_tardiness_whatsapp: notifyTardinessWa ? 1 : 0, notify_tardiness_email: notifyTardinessEmail ? 1 : 0, notify_owner_tardiness_whatsapp: notifyOwnerTardinessWa ? 1 : 0, notify_welcome_whatsapp: notifyWelcomeWa ? 1 : 0, noshow_mode: noshowMode, noshow_tolerance_minutes: parseInt(noshowToleranceMinutes) || 30, notification_matrix: notifMatrix, exception_reasons: Array.isArray(reasonsList) ? reasonsList : [] }); if (res && res.success === false) { setSaveFeedback('Error al guardar: ' + (res.error || 'No se pudieron guardar los ajustes.')); } else { setSaveFeedback('Configuración guardada exitosamente.'); setTimeout(() => setSaveFeedback(''), 4000); } } } catch (err) { console.error('Error al guardar ajustes:', err); setSaveFeedback('Error al guardar la configuración: ' + (err.message || 'Error de conexión')); } finally { setIsSavingAll(false); } }; return (
{/* BARRA SUPERIOR STICKY CON BOTON GUARDAR GENERAL */}

Configuración General y Ajustes

Administra parámetros corporativos, reglas de nómina, asistencias y notificaciones

{saveFeedback && ( {saveFeedback} )}
{/* BARRA DE NAVEGACIÓN PESTAÑAS (SEGMENTED TAB BAR) */}
{[ { id: 'empresa', label: 'Empresa e Identidad', icon: }, { id: 'nomina', label: 'Nómina y Leyes', icon: }, { id: 'asistencia', label: 'Asistencia y GPS', icon: }, { id: 'notificaciones', label: 'Notificaciones y Feriados', icon: }, { id: 'ia', label: 'Inteligencia Artificial', icon: }, { id: 'roles', label: 'Roles y Permisos', icon: } ].map(tab => ( ))}
{/* PESTAÑA 1: EMPRESA E IDENTIDAD */} {activeTab === 'empresa' && (
{/* SECCION 1: IDENTIDAD CORPORATIVA */}

Identidad Corporativa & Logo

Logo institucional visible en el portal, comprobantes y boletas de vacaciones

PNG o SVG transparente
Vista Previa: {companyLogoUrl ? ( Logo Empresa ) : ( Sin logo cargado )}
{/* SECCION 2: DATOS LEGALES Y FISCALES DE LA EMPRESA */}

Información Legal y Fiscal de la Empresa

Datos del empleador utilizados en contratos de trabajo, colillas y reportes oficiales

setCompanyName(e.target.value)} placeholder="Nombre de la empresa" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
setRucNumber(e.target.value)} placeholder="J0310000000001" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
setOwnerName(e.target.value)} placeholder="Nombre y Apellidos" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
setLegalRepId(e.target.value)} placeholder="001-000000-0000A" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
setBusinessAddress(e.target.value)} placeholder="De donde fue el banco, 2c al lago" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
setMunicipality(e.target.value)} placeholder="Managua" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
Dispersión Bancaria y Pagos Masivos
setCompanyDebitAccount(e.target.value)} placeholder="Ej. 109283746" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" /> Cuenta origen desde la cual se debitará la planilla al exportar los archivos bancarios.
)} {/* PESTAÑA 2: NÓMINA Y LEYES */} {activeTab === 'nomina' && (
{/* SECCION 3: PERIODOS Y CORTES DE PLANILLA */}

Ciclos y Ventanas de Corte de Planilla

Definición de días calendario para el cómputo de la primera y segunda quincena

Primera Quincena

Días del mes
Inicio setCutStartDayQ1(e.target.value)} className="w-14 p-2 bg-white border border-slate-200 rounded-lg font-bold text-xs text-center outline-none focus:border-slate-400" />
-
Fin setCutEndDayQ1(e.target.value)} className="w-14 p-2 bg-white border border-slate-200 rounded-lg font-bold text-xs text-center outline-none focus:border-slate-400" />

Segunda Quincena

Días del mes
Inicio setCutStartDayQ2(e.target.value)} className="w-14 p-2 bg-white border border-slate-200 rounded-lg font-bold text-xs text-center outline-none focus:border-slate-400" />
-
Fin setCutEndDayQ2(e.target.value)} className="w-14 p-2 bg-white border border-slate-200 rounded-lg font-bold text-xs text-center outline-none focus:border-slate-400" />
{/* SECCION 4: LEYES LABORALES Y TASAS DE NOMINA */}

Leyes Laborales & Tasas de Nómina

Seguridad Social (INSS), Código del Trabajo (Ley 185) e INATEC

typeof setInssRate === 'function' && setInssRate(parseFloat(e.target.value) || autoRate)} className="w-16 p-1.5 bg-white border border-slate-200 rounded-lg font-bold text-xs text-center outline-none focus:border-slate-400" /> Sugerido: {autoRate}%
typeof setVacRate === 'function' && setVacRate(parseFloat(e.target.value) || 2.5)} className="w-16 p-1.5 bg-white border border-slate-200 rounded-lg font-bold text-xs text-center outline-none focus:border-slate-400" /> días/mes (Art. 76 CT)
Aporte INATEC (2%) Capacitación Técnica
{/* SECCION DE CUENTAS BANCARIAS DE LA EMPRESA */}

Cuentas Bancarias de la Empresa

Registro de cuentas para la dispersión y pago interno de planillas

setNewBankAccountNum(e.target.value)} className="w-full p-2 bg-white border border-slate-200 rounded-lg text-xs font-bold outline-none" />
{companyBankAccounts.length === 0 ? (

No hay cuentas bancarias registradas.

) : ( companyBankAccounts.map(acc => (
{acc.bankName} N° {acc.accountNumber} • {acc.currency} • ({acc.type})
)) )}
)} {/* PESTAÑA 3: POLÍTICAS DE ASISTENCIA Y MOTIVOS DE EXCEPCIÓN */} {activeTab === 'asistencia' && (
{/* SECCION 5: POLITICAS DE TARDANZAS Y ALMUERZOS */}

Políticas de Asistencia y Tolerancia

Tiempos de gracia en entrada, límites de receso y acciones por tardanzas recurrentes

typeof setGracePeriodMinutes === 'function' && setGracePeriodMinutes(e.target.value)} className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
typeof setMaxTardinessAllowed === 'function' && setMaxTardinessAllowed(e.target.value)} className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
typeof setLunchLimitMinutes === 'function' && setLunchLimitMinutes(e.target.value)} className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
{/* SECCION 6: MOTIVOS DE EXCEPCION DE HORAS Y TURNOS */}

Motivos de Excepción de Turno

Opciones disponibles para justificar horas extraordinarias y cambios de turno

setLocalReasonInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddReason(); } }} placeholder="Escribe un nuevo motivo (ej. Inventario general)..." className="flex-1 p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
{(Array.isArray(reasonsList) && reasonsList.length > 0) ? ( reasonsList.map((reason, idx) => (
{reason}
)) ) : (

No hay motivos configurados actualmente.

)}
{/* SECCION 2.1: PERIMETRO DE ASISTENCIA GPS (GEOCERCA CORPORATIVA) */}

Perímetro de Asistencia GPS (Geocerca Corporativa)

Control perimetral satelital para validar marcas de entrada y salida dentro del radio autorizado

{geofenceEnabled && (
{gpsFeedback && (
{gpsFeedback}
)}
setGeofenceLat(e.target.value)} placeholder="12.132820" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
setGeofenceLng(e.target.value)} placeholder="-86.250400" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />
setGeofenceRadiusMeters(e.target.value)} placeholder="150" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs font-medium text-slate-800 outline-none focus:border-slate-400 focus:bg-white transition-all" />

Si se activa, impedirá registrar asistencia fuera del radio. Si está desactivado, etiquetará como 'Fuera de Perímetro'.

)}
)} {/* PESTAÑA 4: NOTIFICACIONES, NO-SHOW Y FERIADOS */} {activeTab === 'notificaciones' && (
{/* SECCIÓN NOTIFICACIONES Y MENSAJERÍA EMPRESARIAL */} {(() => { const clientMods = settings?.modules || window.SHIFT_CONFIG?.modules || {}; const hasWaModule = isMaster || (clientMods.whatsapp !== false && clientMods.whatsapp !== 0 && clientMods.module_whatsapp !== false); const hasEmailModule = isMaster || (clientMods.email !== false && clientMods.email !== 0 && clientMods.module_email !== false); return (

Canales de Notificación (WhatsApp & Email)

Configura qué notificaciones automáticas enviará el sistema a tus colaboradores y supervisores

{/* ESTATUS DE MÓDULOS ACTIVOS POR EL SAAS */}
WA

Módulo WhatsApp Meta Cloud

{hasWaModule ? 'Habilitado en tu plan empresarial' : 'No contratado en el plan SaaS'}

{hasWaModule ? 'ACTIVO' : 'BLOQUEADO'}
@

Módulo Correo Electrónico

{hasEmailModule ? 'Habilitado para envíos SMTP' : 'Inactivo en el plan SaaS'}

{hasEmailModule ? 'ACTIVO' : 'BLOQUEADO'}
{/* SI NINGÚN MÓDULO ESTÁ ACTIVO */} {!hasWaModule && !hasEmailModule && (
No tienes módulos de mensajería (WhatsApp ni Email) activados en tu suscripción. Contacta al administrador SaaS para habilitar las notificaciones automáticas.
)} {/* MATRIZ GRANULAR DE NOTIFICACIONES ENTERPRISE */} {(hasWaModule || hasEmailModule) && (
{/* CONFIGURACIÓN DE REGLAS DE TIEMPO Y TOLERANCIA NO-SHOW */}

Reglas de Tiempo y Margen de Tolerancia de Inasistencia (No-Show)

Configura cuándo el sistema consultará e informará la ausencia si un colaborador no registra su entrada.

{noshowMode === 'tolerance' && (

Ejemplo: Si la entrada es a las 8:00 AM y la tolerancia es de 30 minutos, el aviso se emitirá a partir de las 8:30 AM.

setNoshowToleranceMinutes(e.target.value)} className="w-24 p-2.5 bg-slate-50 border border-slate-300 rounded-xl font-bold text-slate-900 text-center text-xs outline-none focus:border-amber-500 focus:bg-white" /> minutos
)}

Matriz Granular de Comunicaciones y Notificaciones

Activa o desactiva qué canales (WhatsApp / Email) y a qué destinatarios (Colaborador, Supervisor o Propietario) se enviará cada alerta.

{hasWaModule && } {hasEmailModule && } {hasWaModule && } {hasEmailModule && } {hasWaModule && } {hasEmailModule && } {[ { key: 'tardiness', label: 'Llegada Tarde / Tardanza', desc: 'Alertas por llegada fuera del tiempo de gracia' }, { key: 'noshow', label: 'Ausencia / No Presentado (No-Show)', desc: 'Sin marca de entrada tras margen de tolerancia' }, { key: 'open_shift', label: 'Salida Olvidada / Turno Abierto', desc: 'Turno abierto sin marca de salida' }, { key: 'overtime', label: 'Aprobación de Horas Extras', desc: 'Aviso cuando se aprueban horas extras' }, { key: 'payslip', label: 'Entrega de Colilla de Pago PDF', desc: 'Emisión y envío de colilla salarial' }, { key: 'welcome', label: 'Bienvenida a Nuevo Ingreso', desc: 'Envío de credenciales a nuevo personal' } ].map(evt => ( {hasWaModule && ( )} {hasEmailModule && ( )} {hasWaModule && ( )} {hasEmailModule && ( )} {hasWaModule && ( )} {hasEmailModule && ( )} ))}
Evento de Sistema Colaborador Supervisor Directo Propietario / Admin
Tipo de IncidenciaWAEmailWAEmailWAEmail
{evt.label} {evt.desc} toggleMatrixCell(evt.key, 'wa_emp')} className="w-4 h-4 accent-emerald-600 rounded cursor-pointer" /> toggleMatrixCell(evt.key, 'email_emp')} className="w-4 h-4 accent-indigo-600 rounded cursor-pointer" /> toggleMatrixCell(evt.key, 'wa_sup')} className="w-4 h-4 accent-emerald-600 rounded cursor-pointer" /> toggleMatrixCell(evt.key, 'email_sup')} className="w-4 h-4 accent-indigo-600 rounded cursor-pointer" /> toggleMatrixCell(evt.key, 'wa_owner')} className="w-4 h-4 accent-emerald-600 rounded cursor-pointer" /> toggleMatrixCell(evt.key, 'email_owner')} className="w-4 h-4 accent-indigo-600 rounded cursor-pointer" />
)} {/* CONFIGURACIÓN DE PASARELA WHATSAPP (SOLO SI MODULE WHATSAPP ESTÁ HABILITADO) */} {hasWaModule && (

Configuración de Pasarela de WhatsApp Meta Cloud

Elige si deseas usar la pasarela oficial verificada de SHIFT o conectar tu propia cuenta business de Meta Cloud.

{!useGlobalWa && (
Credenciales Meta Cloud WhatsApp
setWaCustomPhoneId(e.target.value)} placeholder="Ej. 1362993856888988" className="w-full p-3 bg-white border border-slate-200 rounded-xl font-mono text-xs outline-none focus:border-emerald-600" />
setWaCustomWabaId(e.target.value)} placeholder="Ej. 1798705111135383" className="w-full p-3 bg-white border border-slate-200 rounded-xl font-mono text-xs outline-none focus:border-emerald-600" />
)}
)} {/* CONFIGURACIÓN DE SERVIDOR DE CORREO (SOLO SI MODULE EMAIL ESTÁ HABILITADO) */} {hasEmailModule && (

Configuración del Servidor de Correo (SMTP)

Define el servidor SMTP utilizado para emitir colillas y notificaciones por email.

{!useGlobalSmtp && (
Servidor SMTP Personalizado
setSmtpHost(e.target.value)} placeholder="mail.miempresa.com" className="w-full p-3 bg-white border border-slate-200 rounded-xl font-mono text-xs outline-none focus:border-indigo-600" />
setSmtpPort(e.target.value)} placeholder="587" className="w-full p-3 bg-white border border-slate-200 rounded-xl font-mono text-xs outline-none focus:border-indigo-600" />
setSmtpUser(e.target.value)} placeholder="notificaciones@miempresa.com" className="w-full p-3 bg-white border border-slate-200 rounded-xl text-xs outline-none focus:border-indigo-600" />
setSmtpPass(e.target.value)} placeholder="••••••••" className="w-full p-3 bg-white border border-slate-200 rounded-xl text-xs outline-none focus:border-indigo-600" />
setSmtpFromEmail(e.target.value)} placeholder="noreply@miempresa.com" className="w-full p-3 bg-white border border-slate-200 rounded-xl text-xs outline-none focus:border-indigo-600" />
setSmtpFromName(e.target.value)} placeholder="Mi Empresa - Nómina" className="w-full p-3 bg-white border border-slate-200 rounded-xl text-xs outline-none focus:border-indigo-600" />
)}
)} {/* WIDGETS DE VERIFICACION DE PASARELAS EN TIEMPO REAL */} {(hasWaModule || hasEmailModule) && (

Verificación de Pasarelas en Tiempo Real

Realiza un envío de prueba directo para confirmar la conectividad de la pasarela activa.

{testFeedback && (
{testFeedback}
)}
{/* PRUEBA WHATSAPP */} {hasWaModule && (
Test Mensaje WhatsApp Meta Cloud
setTestWaPhone(e.target.value)} placeholder="Número con código de país (ej. 50587289931)" className="flex-1 p-3 bg-white border border-slate-200 rounded-xl font-mono text-xs outline-none focus:border-emerald-600 text-slate-800" />
)} {/* PRUEBA CORREO */} {hasEmailModule && (
Test Correo Electrónico SMTP
setTestEmailAddr(e.target.value)} placeholder="correo@ejemplo.com" className="flex-1 p-3 bg-white border border-slate-200 rounded-xl text-xs outline-none focus:border-indigo-600 text-slate-800" />
)}
)}
); })()} {/* SECCION 7: CATALOGO DE DIAS FERIADOS DE LA EMPRESA */}

Catálogo de Días Feriados (Nicaragua)

{companyHolidays.length} Feriados

Días de asueto remunerado nacional/local. Exenta la auditoría de ausencias y calcula sobreprecio de pago doble si laboran.

{companyHolidays.length > 0 ? ( companyHolidays.map(h => ( )) ) : ( )}
Nombre del Feriado Fecha Tipo de Asueto Acciones
{h.name} {h.date} {h.isMandatory !== false ? 'Obligatorio / Remunerado' : 'Asueto Local / Opcional'}
No hay días feriados registrados para la empresa. Haz clic en "Agregar Día Feriado" para definir uno.
)} {/* PESTAÑA 5: INTELIGENCIA ARTIFICIAL */} {activeTab === 'ia' && (

Google Gemini AI

Motor Inteligente

Asistente conversacional de recursos humanos y auditorías inteligentes

{geminiStatus && ( {geminiStatus.text} )}
setGeminiApiKey(e.target.value)} placeholder="AIzaSy..." className="w-full p-2.5 pr-20 bg-slate-50 border border-slate-200 rounded-xl font-mono text-xs outline-none focus:border-slate-400 focus:bg-white transition-all text-slate-800" />
Obtén tu clave gratuita en Google AI Studio {geminiApiKey && Clave configurada}
)} {/* PESTAÑA 6: ROLES Y PERMISOS */} {activeTab === 'roles' && window.AdminRoles && (
)} {/* FEEDBACK Y BOTON PRINCIPAL DE GUARDADO */} {saveFeedback && (
{saveFeedback}
)} {/* MODAL DE RECORTE DE LOGO */} {showCropModal && (
setShowCropModal(false)}>
e.stopPropagation()}>

Ajustar y Recortar Logo

Arrastra para encuadrar y ajusta el zoom

Zoom {zoomScale.toFixed(1)}x
setZoomScale(parseFloat(e.target.value))} className="w-full h-1.5 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-slate-900" />
)} {/* MODAL CREAR / EDITAR DIA FERIADO */} {showHolidayModal && (
setShowHolidayModal(false)}>
e.stopPropagation()}>

{editingHoliday ? 'Editar Día Feriado' : 'Nuevo Día Feriado'}

Definición de asueto laboral

setHolidayNameInput(e.target.value)} placeholder="Ej. Día del Trabajo / Santo Domingo" className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl font-medium text-xs text-slate-800 outline-none focus:border-slate-400 focus:bg-white" />
setHolidayDateInput(e.target.value)} className="w-full p-2.5 bg-slate-50 border border-slate-200 rounded-xl font-medium text-xs text-slate-800 outline-none focus:border-slate-400 focus:bg-white" />
)}
); };