const { useState, useEffect, useMemo } = React; window.AdminSaaS = function({ activeViewTab = 'saas_clients', setActiveClientId, setTab, refreshData }) { const [saasData, setSaasData] = useState({ clients: [], invoices: [], masterGeminiApiKey: '' }); const [selectedClient, setSelectedClient] = useState(null); const [showModuleModal, setShowModuleModal] = useState(false); const [showInvoiceModal, setShowInvoiceModal] = useState(false); // Modal de Nueva / Editar Empresa const [showCompanyModal, setShowCompanyModal] = useState(false); const [editingCompany, setEditingCompany] = useState(null); const [masterKeyInput, setMasterKeyInput] = useState(''); const [isSaving, setIsSaving] = useState(false); const [showKey, setShowKey] = useState(false); const [saveFeedback, setSaveFeedback] = useState(''); // Estados Pasarela WhatsApp Meta Cloud, Chatwoot & SMTP Email const [waToken, setWaToken] = useState(''); const [waPhoneId, setWaPhoneId] = useState(''); const [waWabaId, setWaWabaId] = useState(''); const [waEnabled, setWaEnabled] = useState(true); // Chatwoot & AI Copilot Integración Omnicanal const [chatwootUrl, setChatwootUrl] = useState(''); const [chatwootAccountId, setChatwootAccountId] = useState(1); const [chatwootInboxId, setChatwootInboxId] = useState(1); const [chatwootApiToken, setChatwootApiToken] = useState(''); const [useChatwootForWhatsapp, setUseChatwootForWhatsapp] = useState(false); const [aiAutoReplyEnabled, setAiAutoReplyEnabled] = useState(true); const [testingChatwoot, setTestingChatwoot] = useState(false); const [smtpHost, setSmtpHost] = useState(''); const [smtpPort, setSmtpPort] = useState(587); const [smtpUser, setSmtpUser] = useState(''); const [smtpPass, setSmtpPass] = useState(''); const [smtpFromEmail, setSmtpFromEmail] = useState(''); const [smtpFromName, setSmtpFromName] = useState('Shift ERP'); const [smtpSecure, setSmtpSecure] = useState('tls'); const [emailEnabled, setEmailEnabled] = useState(true); const [gatewayFeedback, setGatewayFeedback] = useState(''); // Búsqueda y Filtros const [clientSearch, setClientSearch] = useState(''); const [clientFilter, setClientFilter] = useState('all'); // all, active, suspended, demo, paid const [invoiceSearch, setInvoiceSearch] = useState(''); const [invoiceFilter, setInvoiceFilter] = useState('all'); // all, unpaid, paid, overdue // Estados de prueba y sincronización const [testPhoneInput, setTestPhoneInput] = useState(''); const [testEmailInput, setTestEmailInput] = useState(''); const [testingWa, setTestingWa] = useState(false); const [testingEmail, setTestingEmail] = useState(false); const [billingSyncing, setBillingSyncing] = useState(false); const [notifyingInvId, setNotifyingInvId] = useState(null); const loadMasterData = async () => { const res = await window.apiCall('get_master_dashboard_data', {}, 0); if (res && res.success) { setSaasData(res); setMasterKeyInput(res.masterGeminiApiKey || ''); if (res.masterWhatsApp) { setWaToken(res.masterWhatsApp.access_token || ''); setWaPhoneId(res.masterWhatsApp.phone_number_id || ''); setWaWabaId(res.masterWhatsApp.waba_id || ''); setWaEnabled(res.masterWhatsApp.enabled !== false); } if (res.masterChatwoot) { setChatwootUrl(res.masterChatwoot.chatwoot_url || ''); setChatwootAccountId(res.masterChatwoot.chatwoot_account_id || 1); setChatwootInboxId(res.masterChatwoot.chatwoot_inbox_id || 1); setChatwootApiToken(res.masterChatwoot.chatwoot_api_token || ''); setUseChatwootForWhatsapp(res.masterChatwoot.use_chatwoot_for_whatsapp !== 0); setAiAutoReplyEnabled(res.masterChatwoot.ai_auto_reply_enabled !== 0); } if (res.masterEmail) { setSmtpHost(res.masterEmail.smtp_host || ''); setSmtpPort(res.masterEmail.smtp_port || 587); setSmtpUser(res.masterEmail.smtp_user || ''); setSmtpPass(res.masterEmail.smtp_pass || ''); setSmtpFromEmail(res.masterEmail.smtp_from_email || ''); setSmtpFromName(res.masterEmail.smtp_from_name || 'Shift ERP'); setSmtpSecure(res.masterEmail.smtp_secure || 'tls'); setEmailEnabled(res.masterEmail.enabled !== false); } } }; useEffect(() => { loadMasterData(); }, []); const metrics = useMemo(() => { const totalClients = saasData.clients.length; const activeClients = saasData.clients.filter(c => c.service_status === 'active').length; const suspendedClients = saasData.clients.filter(c => c.service_status === 'suspended').length; const demoClients = saasData.clients.filter(c => c.account_type === 'demo').length; const paidClients = saasData.clients.filter(c => c.account_type === 'subscription').length; let mrrUSD = 0; saasData.clients.forEach(c => { if (c.service_status === 'active') { mrrUSD += parseFloat(c.monthly_fee || 0); } }); const today = new Date().toISOString().split('T')[0]; let totalInvoicedUSD = 0; let totalCollectedUSD = 0; let totalPendingUSD = 0; const overdueInvoices = saasData.invoices.filter(inv => { const amt = parseFloat(inv.amount || 0); totalInvoicedUSD += amt; if (inv.status === 'paid') { totalCollectedUSD += amt; } else { totalPendingUSD += amt; } if (inv.status !== 'unpaid') return false; const diffDays = Math.floor((new Date(today) - new Date(inv.due_date)) / (1000 * 60 * 60 * 24)); return diffDays >= 5; }); return { totalClients, activeClients, suspendedClients, demoClients, paidClients, mrrUSD, overdueInvoices, totalInvoicedUSD, totalCollectedUSD, totalPendingUSD }; }, [saasData]); const filteredClients = useMemo(() => { return (saasData.clients || []).filter(c => { if (clientFilter === 'active' && c.service_status !== 'active') return false; if (clientFilter === 'suspended' && c.service_status !== 'suspended') return false; if (clientFilter === 'demo' && c.account_type !== 'demo') return false; if (clientFilter === 'paid' && c.account_type !== 'subscription') return false; if (clientSearch.trim()) { const q = clientSearch.toLowerCase().trim(); const nameMatch = (c.company_name || '').toLowerCase().includes(q); const subMatch = (c.shift_subdomain || '').toLowerCase().includes(q); const contactMatch = (c.contact_name || '').toLowerCase().includes(q); const emailMatch = (c.contact_email || '').toLowerCase().includes(q); const rucMatch = (c.ruc_number || '').toLowerCase().includes(q); return nameMatch || subMatch || contactMatch || emailMatch || rucMatch; } return true; }); }, [saasData.clients, clientFilter, clientSearch]); const filteredInvoices = useMemo(() => { const today = new Date().toISOString().split('T')[0]; return (saasData.invoices || []).filter(inv => { if (invoiceFilter === 'paid' && inv.status !== 'paid') return false; if (invoiceFilter === 'unpaid' && inv.status !== 'unpaid') return false; if (invoiceFilter === 'overdue') { if (inv.status !== 'unpaid') return false; const diffDays = Math.floor((new Date(today) - new Date(inv.due_date)) / (1000 * 60 * 60 * 24)); if (diffDays < 5) return false; } if (invoiceSearch.trim()) { const q = invoiceSearch.toLowerCase().trim(); const numMatch = (inv.invoice_number || '').toLowerCase().includes(q); const compMatch = (inv.company_name || '').toLowerCase().includes(q); return numMatch || compMatch; } return true; }); }, [saasData.invoices, invoiceFilter, invoiceSearch]); const handleEnterPortal = (client) => { if (setActiveClientId) { setActiveClientId(Number(client.id)); if (setTab) setTab('logs'); } }; const handleExportClientsCSV = () => { const headers = ["ID", "Empresa", "Subdominio", "Tipo Cuenta", "Estado Servicio", "Tarifa Mensual", "Moneda", "Día Cobro", "Pagado Hasta", "Contacto", "Email", "Teléfono", "RUC"]; const rows = (saasData.clients || []).map(c => [ c.id, `"${c.company_name || ''}"`, `${c.shift_subdomain || ''}.shift.marcasnicaragua.com`, c.account_type || 'demo', c.service_status || 'active', c.monthly_fee || 0, c.currency || 'USD', c.billing_day || 1, c.service_paid_until || '', `"${c.contact_name || ''}"`, c.contact_email || '', c.contact_phone || '', c.ruc_number || '' ]); if (window.exportToCSV) { window.exportToCSV(`Directorio_SaaS_Shift_${new Date().toISOString().split('T')[0]}`, headers, rows); } }; const handleTriggerBillingCycle = async () => { setBillingSyncing(true); try { const res = await window.apiCall('trigger_saas_billing_cycle', {}, 0); if (res && res.success) { alert(`✅ Ciclo de facturación sincronizado con éxito.\n• Facturas nuevas generadas: ${res.invoices_generated || 0}\n• Empresas suspendidas por mora (>5 días): ${res.clients_suspended || 0}`); await loadMasterData(); } else { alert("Error al sincronizar ciclo: " + (res?.error || "")); } } catch (err) { alert("Error de conexión: " + err.message); } finally { setBillingSyncing(false); } }; const handleSendInvoiceNotification = async (inv, channel = 'all') => { setNotifyingInvId(inv.id); try { const res = await window.apiCall('send_invoice_notification', { invoice_id: inv.id, channel }, 0); if (res && res.success) { let details = []; if (res.whatsapp) { details.push(res.whatsapp.success ? '✅ WhatsApp enviado' : '⚠️ WhatsApp: ' + (res.whatsapp.error || 'Fallo')); } if (res.email) { details.push(res.email.success ? '✅ Email enviado' : '⚠️ Email: ' + (res.email.error || 'Fallo')); } alert(`Notificación de factura ${inv.invoice_number}:\n` + (details.join('\n') || 'Procesado')); } else { alert("Error al enviar notificación: " + (res?.error || "Fallo")); } } catch (err) { alert("Error de conexión: " + err.message); } finally { setNotifyingInvId(null); } }; const handleSaveMasterKey = async () => { if (!masterKeyInput.trim()) return alert("Ingresa la API Key Maestra de Google Gemini."); setIsSaving(true); setSaveFeedback(''); try { const res = await window.apiCall('save_master_gemini_key', { gemini_api_key: masterKeyInput.trim() }, 0); if (res && res.success) { setSaveFeedback('API Key Maestra guardada correctamente en el servidor.'); await loadMasterData(); } else { setSaveFeedback('Error: ' + (res?.error || 'No se pudo guardar')); } } catch (err) { setSaveFeedback('Error de conexión: ' + err.message); } finally { setIsSaving(false); } }; const handleSaveMasterGateways = async () => { setIsSaving(true); setGatewayFeedback(''); try { const res = await window.apiCall('save_master_notification_gateways', { whatsapp_access_token: waToken.trim(), whatsapp_phone_number_id: waPhoneId.trim(), whatsapp_waba_id: waWabaId.trim(), whatsapp_enabled: waEnabled ? 1 : 0, chatwoot_url: chatwootUrl.trim(), chatwoot_account_id: parseInt(chatwootAccountId) || 1, chatwoot_inbox_id: parseInt(chatwootInboxId) || 1, chatwoot_api_token: chatwootApiToken.trim(), use_chatwoot_for_whatsapp: useChatwootForWhatsapp ? 1 : 0, ai_auto_reply_enabled: aiAutoReplyEnabled ? 1 : 0, 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, email_enabled: emailEnabled ? 1 : 0 }, 0); if (res && res.success) { setGatewayFeedback('Pasarelas de WhatsApp Meta Cloud, Chatwoot y Email guardadas exitosamente.'); await loadMasterData(); setTimeout(() => setGatewayFeedback(''), 4000); } else { setGatewayFeedback('Error: ' + (res?.error || 'No se pudo guardar')); } } catch (err) { setGatewayFeedback('Error de red: ' + err.message); } finally { setIsSaving(false); } }; const handleTestChatwoot = async () => { const phone = testPhoneInput.trim() || '87289931'; setTestingChatwoot(true); setGatewayFeedback(''); try { const res = await window.apiCall('test_chatwoot_connection', { chatwoot_url: chatwootUrl, chatwoot_account_id: chatwootAccountId, chatwoot_inbox_id: chatwootInboxId, chatwoot_api_token: chatwootApiToken, phone: phone }, 0); if (res && res.success) { setGatewayFeedback(`¡Conexión con Chatwoot exitosa! Mensaje enviado al inbox (${res.via || 'Chatwoot'}).`); } else { setGatewayFeedback(`Error en Chatwoot: ${res?.error || 'Fallo de envío'}`); } } catch (err) { setGatewayFeedback('Error de conexión: ' + err.message); } finally { setTestingChatwoot(false); } }; const handleTestWhatsApp = async (useTemplateMode = false) => { const phone = testPhoneInput.trim(); if (!phone) return alert("Ingresa un número de teléfono (ej. 87289931) para enviar el WhatsApp de prueba."); setTestingWa(true); setGatewayFeedback(''); try { const res = await window.apiCall('test_whatsapp_gateway', { phone, use_template: useTemplateMode }, 0); if (res && res.success) { setGatewayFeedback(`¡Mensaje de prueba de WhatsApp enviado con éxito a +${res.recipient || phone}! ${res.notice ? '(' + res.notice + ')' : ''}`); } else { setGatewayFeedback(`Error en prueba de WhatsApp: ${res?.error || 'Fallo de entrega'}. Detalle: ${JSON.stringify(res?.details || '')}`); } } catch (err) { setGatewayFeedback('Error de conexión: ' + err.message); } finally { setTestingWa(false); } }; const handleTestEmail = async () => { const email = testEmailInput.trim(); if (!email) return alert("Ingresa un correo electrónico para enviar el email de prueba."); setTestingEmail(true); setGatewayFeedback(''); try { const res = await window.apiCall('test_email_gateway', { email }, 0); if (res && res.success) { setGatewayFeedback(`¡Correo electrónico de prueba enviado con éxito a ${res.recipient || email}!`); } else { setGatewayFeedback(`Error al enviar correo de prueba: ${res?.error || 'Fallo de servidor SMTP'}`); } } catch (err) { setGatewayFeedback('Error de conexión: ' + err.message); } finally { setTestingEmail(false); } }; const handleOpenNewCompany = () => { setEditingCompany(null); setShowCompanyModal(true); }; const handleOpenEditCompany = (cl) => { setEditingCompany(cl); setShowCompanyModal(true); }; const handleSaveCompanySubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); const payload = { id: editingCompany ? editingCompany.id : null, company_name: fd.get('company_name'), shift_subdomain: fd.get('shift_subdomain'), contact_name: fd.get('contact_name'), contact_email: fd.get('contact_email'), contact_phone: fd.get('contact_phone'), admin_password: fd.get('admin_password'), tax_regime: fd.get('tax_regime'), ruc_number: fd.get('ruc_number'), owner_name: fd.get('owner_name'), legal_rep_id: fd.get('legal_rep_id'), business_address: fd.get('business_address'), municipality: fd.get('municipality'), module_shift: 1 }; setIsSaving(true); const res = await window.apiCall('save_master_client', payload, 0); setIsSaving(false); if (res && res.success) { setShowCompanyModal(false); await loadMasterData(); if (refreshData) await refreshData(0); } else { alert("Error al guardar empresa: " + (res?.error || "")); } }; const handleOpenModules = (cl) => { setSelectedClient(cl); setShowModuleModal(true); }; const handleSaveModulesAndBilling = async (e) => { e.preventDefault(); const fd = new FormData(e.target); const payload = { id: selectedClient.id, account_type: fd.get('account_type'), monthly_fee: fd.get('monthly_fee'), currency: fd.get('currency'), billing_day: fd.get('billing_day'), service_status: fd.get('service_status'), service_paid_until: fd.get('service_paid_until'), module_asistencia: fd.get('module_asistencia') ? 1 : 0, module_personal: fd.get('module_personal') ? 1 : 0, module_contratos: fd.get('module_contratos') ? 1 : 0, module_planilla: fd.get('module_planilla') ? 1 : 0, module_vacaciones: fd.get('module_vacaciones') ? 1 : 0, module_ausencias: fd.get('module_ausencias') ? 1 : 0, module_ai: fd.get('module_ai') ? 1 : 0, module_facial: fd.get('module_facial') ? 1 : 0, module_whatsapp: fd.get('module_whatsapp') ? 1 : 0, module_email: fd.get('module_email') ? 1 : 0 }; setIsSaving(true); const res = await window.apiCall('save_client_modules_and_billing', payload, 0); setIsSaving(false); if (res && res.success) { setShowModuleModal(false); await loadMasterData(); if (refreshData) await refreshData(0); } }; const handleToggleSuspension = async (client, currentStatus) => { const nextStatus = currentStatus === 'suspended' ? 'active' : 'suspended'; const msg = nextStatus === 'suspended' ? `¿Deseas suspender el acceso al portal de ${client.company_name}? Sus colaboradores no podrán registrar asistencia.` : `¿Deseas reactivar el acceso al portal de ${client.company_name}?`; if (!confirm(msg)) return; const res = await window.apiCall('toggle_service_suspension', { client_id: client.id, status: nextStatus }, 0); if (res && res.success) { await loadMasterData(); if (refreshData) await refreshData(0); } }; const handleMarkPaid = async (inv) => { const ref = prompt("Referencia bancaria o método de pago (ej. Transferencia BAC #9482):", "Transferencia BAC"); if (ref === null) return; const res = await window.apiCall('mark_invoice_paid', { invoice_id: inv.id, payment_method: 'Transferencia Bancaria', payment_reference: ref }, 0); if (res && res.success) { alert(`Factura ${inv.invoice_number} marcada como PAGADA.`); await loadMasterData(); } }; const handleCreateInvoice = async (e) => { e.preventDefault(); const fd = new FormData(e.target); const clId = fd.get('client_id'); const cl = saasData.clients.find(c => Number(c.id) === Number(clId)); const payload = { client_id: clId, company_name: cl ? cl.company_name : 'Empresa Cliente', amount: fd.get('amount'), currency: fd.get('currency'), issue_date: fd.get('issue_date'), due_date: fd.get('due_date'), period_start: fd.get('period_start'), period_end: fd.get('period_end'), status: 'unpaid' }; setIsSaving(true); const res = await window.apiCall('create_client_invoice', payload, 0); setIsSaving(false); if (res && res.success) { setShowInvoiceModal(false); await loadMasterData(); } }; return (
Gestión centralizada de empresas, suscripciones mensuales y pasarelas de notificación
El sistema ha aplicado suspensión preventiva. Registra el pago para reactivación inmediata.
{metrics.paidClients} de pago • {metrics.demoClients} demo
Operando normalmente
Por mora o solicitud
Recurrente mensual
No se encontraron empresas con el criterio de búsqueda.
Histórico global
Facturas cobradas
Por recaudar
Sujetas a suspensión
Control mensual de recibos y facturas emitidas a empresas
No hay facturas registradas en este filtro.
Período: {inv.period_start} al {inv.period_end} • Vence: {inv.due_date}
{isPaid && (Pagado con {inv.payment_method} ({inv.payment_reference || 'Confirmado'})
)}Alimenta el Copilot AI de todas las empresas con aislamiento estricto de datos
{saveFeedback}
)}Número oficial registrado en Meta for Developers (+505 8728-9931)
Permite supervisar mensajes de WhatsApp en Chatwoot y auto-responder consultas de colaboradores
Configuración de servidor saliente para envío de colillas y facturas
Información comercial, fiscal y credenciales de acceso administrativo
Activa o desactiva las funciones según el plan contratado