import React, { useState, useEffect } from 'react'; import { Puzzle, Radio, CheckCircle2, XCircle, Settings2, Zap, ChevronDown, ChevronUp, Eye, EyeOff, Save, X } from 'lucide-react'; import { PageHeader } from '../components/PageHeader.tsx'; import { useToast } from '../contexts/ToastContext.tsx'; import { HybridBackend } from '../services/backend.ts'; import { getActivePlugins, togglePlugin, getPluginConfig, savePluginConfig, ALWAYS_ON_PLUGINS } from './plugins/pluginRegistry.ts'; // ─── Plugin definitions ─────────────────────────────────────────────────────── interface ConfigField { key: string; label: string; type: 'text' | 'password'; placeholder: string; defaultValue: string; } interface PluginDef { id: string; name: string; tagline: string; description: string; version: string; author: string; category: string; color: string; sidebarItems: Array<{ id: string; label: string }>; features: string[]; configFields: ConfigField[]; } const PLUGINS: PluginDef[] = [ { id: 'newwhats', name: 'NewWhats — WhatsApp & Secretária IA', tagline: 'Inbox WhatsApp + Atendente virtual', description: 'Integração com o motor NewWhats: Inbox de WhatsApp em tempo real e Secretária IA. Inbox e Secretária ficam disponíveis para todos os usuários; esta configuração é exclusiva do super-admin.', version: '1.0.0', author: 'ScoreOdonto', category: 'COMUNICAÇÃO', color: '#16a34a', sidebarItems: [ { id: 'wa-inbox', label: 'WHATSAPP' }, { id: 'wa-secretaria', label: 'SECRETÁRIA IA' }, ], features: [ 'Inbox WhatsApp (conversas, mensagens, envio)', 'Secretária IA (atendente virtual do motor)', 'Base de conhecimento da clínica sincronizada com o motor', ], configFields: [ { key: 'NEWWHATS_URL', label: 'URL DO MOTOR', type: 'text', placeholder: 'https://newwhats.clube67.com', defaultValue: '' }, { key: 'NEWWHATS_INTEGRATION_KEY', label: 'CHAVE DE INTEGRAÇÃO', type: 'password', placeholder: 'nw_...', defaultValue: '' }, { key: 'NEWWHATS_WEBHOOK_SECRET', label: 'SEGREDO DO WEBHOOK', type: 'password', placeholder: '(HMAC)', defaultValue: '' }, { key: 'NEWWHATS_CLINICA_ID', label: 'CLÍNICA (ID)', type: 'text', placeholder: 'c_...', defaultValue: '' }, ], }, { id: 'rx-scoreodonto', name: 'RX ScoreOdonto', tagline: 'Radiografias & GTOs', description: 'Integração completa com o sistema de radiografias. Gerencie imagens, GTOs e sincronize com o client Windows instalado nas clínicas.', version: '', author: 'ScoreOdonto', category: 'DIAGNÓSTICO', color: '#7c3aed', sidebarItems: [{ id: 'rx-radiografias', label: 'RX / RADIOGRAFIAS' }], features: [ 'Visualizar radiografias por paciente', 'Upload de imagens (arquivo ou Base64)', 'Gerenciar GTOs e marcar como enviadas', 'Sincronização com client Windows', 'Criar fichas de pacientes remotamente', ], configFields: [ { key: 'apiToken', label: 'TOKEN DE API', type: 'password', placeholder: 'api_...', defaultValue: 'api_d5c8b4f4297ca579c7beceac6b4b1a274eb0df599331d06b', }, { key: 'apiUrl', label: 'URL DO SERVIDOR RX', type: 'text', placeholder: 'https://dev.rx.scoreodonto.com', defaultValue: 'https://dev.rx.scoreodonto.com', }, ], }, { id: 'locacao-salas', name: 'Locação de Salas', tagline: 'Marketplace de salas', description: 'Disponibilize salas clínicas, de consultório ou independentes para locação por hora, turno, diária, semana ou mês — e reserve salas equipadas de outros profissionais com anti-conflito de agenda.', version: '1.0', author: 'ScoreOdonto', category: 'MARKETPLACE', color: '#0d9488', sidebarItems: [{ id: 'salas', label: 'LOCAÇÃO DE SALAS' }], features: [ 'Cadastrar salas com equipamentos, endereço e valores', 'Modelos de locação: hora, turno, diária, semanal e mensal', 'Marketplace com busca por tipo, cidade e estado', 'Reservas com confirmação do locador', 'Anti-conflito: bloqueia choque com a agenda global do profissional', ], configFields: [], }, { id: 'profissionais-marketplace', name: 'Profissionais', tagline: 'Marketplace de profissionais', description: 'Encontre e contrate dentistas, biomédicos(as) e protéticos por especialidade, cidade, CEP e raio de atuação. Profissionais divulgam o perfil e recebem propostas das clínicas e consultórios.', version: '1.0', author: 'ScoreOdonto', category: 'MARKETPLACE', color: '#0d9488', sidebarItems: [{ id: 'marketplace-profissionais', label: 'PROFISSIONAIS' }], features: [ 'Busca por especialidade, cidade, estado e região (CEP)', 'Perfil profissional com raio de atuação e apresentação', 'Inclui dentistas, biomédicos(as) e protéticos', 'Propostas de contratação com aceite/recusa', 'Notificações automáticas para as duas partes', ], configFields: [], }, ]; // ─── Config Modal ───────────────────────────────────────────────────────────── const ConfigModal: React.FC<{ plugin: PluginDef; onClose: () => void }> = ({ plugin, onClose }) => { const toast = useToast(); const savedConfig = getPluginConfig(plugin.id); const [values, setValues] = useState>( Object.fromEntries(plugin.configFields.map(f => [f.key, savedConfig[f.key] ?? f.defaultValue])) ); const [showPassword, setShowPassword] = useState>({}); const handleSave = () => { savePluginConfig(plugin.id, values); toast.success('CONFIGURAÇÕES SALVAS!'); onClose(); }; const inputCls = "w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm font-mono focus:outline-none focus:border-purple-400 focus:ring-2 focus:ring-purple-100 bg-white pr-10"; return (

CONFIGURAR {plugin.name}

PARÂMETROS DO PLUGIN

{plugin.configFields.map(field => (
setValues(v => ({ ...v, [field.key]: e.target.value }))} className={inputCls} placeholder={field.placeholder} /> {field.type === 'password' && ( )}
))}
); }; // ─── Plugin Card ────────────────────────────────────────────────────────────── const PluginCard: React.FC<{ plugin: PluginDef; isActive: boolean; alwaysOn?: boolean; price?: number; onToggle: () => void; onConfigure: () => void }> = ({ plugin, isActive, alwaysOn, price, onToggle, onConfigure }) => { const [expanded, setExpanded] = useState(false); return (
{/* Header strip */}

{plugin.name}

{plugin.category} {/* Preço do add-on vem da Tabela de Preços (superadmin/financeiro) */} {price !== undefined && ( 0 ? 'bg-emerald-50 text-emerald-600' : 'bg-gray-100 text-gray-500'}`}> {price > 0 ? `R$ ${price.toFixed(2).replace('.', ',')}/MÊS` : 'INCLUÍDO NO PLANO'} )}

{plugin.version ? `v${plugin.version} · ` : ''}por {plugin.author}

{/* Active badge */} {isActive && ( ATIVO )}

{plugin.description}

{/* Features (collapsible) */} {expanded && (
    {plugin.features.map((f, i) => (
  • {f}
  • ))}
)} {/* Sidebar items info */} {isActive && plugin.sidebarItems.length > 0 && (

ADICIONADO AO MENU

{plugin.sidebarItems.map(item => (
{item.label}
))}
)} {/* Actions */}
{alwaysOn ? (
SEMPRE INCLUÍDO
) : ( )} {plugin.configFields.length > 0 && ( )}
); }; // ─── Main View ──────────────────────────────────────────────────────────────── export const PluginsView: React.FC = () => { const toast = useToast(); const [activePlugins, setActivePlugins] = useState(getActivePlugins); const [configuringPlugin, setConfiguringPlugin] = useState(null); const [pluginVersions, setPluginVersions] = useState>({}); const [pluginPrices, setPluginPrices] = useState>({}); useEffect(() => { // Busca versão real do servidor RX const rxUrl = getPluginConfig('rx-scoreodonto').apiUrl || 'https://rx.scoreodonto.com'; fetch(`${rxUrl.replace(/\/$/, '')}/api/version`) .then(r => r.ok ? r.json() : null) .then(d => { if (d?.client) setPluginVersions(v => ({ ...v, 'rx-scoreodonto': d.client })); }) .catch(() => {}); // Preços dos add-ons (Tabela de Preços do superadmin → chaves `plugin:`). // Falha silenciosa p/ papéis sem acesso ao pricing: card fica sem o chip de preço. fetch('/api/superadmin/pricing', { headers: { Authorization: `Bearer ${localStorage.getItem('SCOREODONTO_AUTH_TOKEN')}` } }) .then(r => r.ok ? r.json() : null) .then(d => { if (!d) return; const prices: Record = {}; for (const [k, v] of Object.entries(d)) if (k.startsWith('plugin:')) prices[k.slice(7)] = Number(v) || 0; setPluginPrices(prices); }) .catch(() => {}); }, []); const handleToggle = (plugin: PluginDef) => { const nowActive = togglePlugin(plugin.id); setActivePlugins(getActivePlugins()); // 'locacao-salas' é ativado POR CONTA no servidor (fonte da verdade); persiste o estado. if (plugin.id === 'locacao-salas') HybridBackend.setMyPlugin('locacao-salas', nowActive); if (nowActive) { toast.success(`${plugin.name.toUpperCase()} ATIVADO! RECARREGANDO...`); } else { toast.info(`${plugin.name.toUpperCase()} DESATIVADO. RECARREGANDO...`); } setTimeout(() => window.location.reload(), 1200); }; return (
{/* Summary bar */}

{PLUGINS.length} PLUGIN{PLUGINS.length !== 1 ? 'S' : ''} DISPONÍVEL{PLUGINS.length !== 1 ? 'S' : ''}

{activePlugins.length} ativo{activePlugins.length !== 1 ? 's' : ''}

{PLUGINS.map(p => (
))}
{/* Plugin grid */}
{PLUGINS.map(plugin => ( handleToggle(plugin)} onConfigure={() => setConfiguringPlugin(plugin)} /> ))}
{/* Config modal */} {configuringPlugin && ( setConfiguringPlugin(null)} /> )}
); };