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 ────────────────────────────────────────────────────────────── // ─── Config dedicada do NewWhats (dropdown de clínicas + backend + testar) ────── const NW_API = (import.meta as any).env?.VITE_API_URL || '/api'; const nwAuthHeaders = () => { const t = localStorage.getItem('SCOREODONTO_AUTH_TOKEN'); return { 'Content-Type': 'application/json', ...(t ? { Authorization: `Bearer ${t}` } : {}) }; }; const NewwhatsConfigModal: React.FC<{ plugin: PluginDef; onClose: () => void }> = ({ plugin, onClose }) => { const toast = useToast(); const [motorUrl, setMotorUrl] = useState(''); const [clinicaId, setClinicaId] = useState(''); const [integrationKey, setIntegrationKey] = useState(''); const [webhookSecret, setWebhookSecret] = useState(''); const [hasKey, setHasKey] = useState(false); const [hasSecret, setHasSecret] = useState(false); const [clinics, setClinics] = useState>([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [test, setTest] = useState<{ ok: boolean; msg: string } | null>(null); const [token, setToken] = useState(''); const [applying, setApplying] = useState(false); const [advanced, setAdvanced] = useState(false); const loadCfg = async () => { try { const cfgR = await fetch(`${NW_API}/nw/config`, { headers: nwAuthHeaders() }); if (cfgR.ok) { const c = await cfgR.json(); setMotorUrl(c.motorUrl || ''); setClinicaId(c.clinicaId || ''); setHasKey(!!c.hasIntegrationKey); setHasSecret(!!c.hasWebhookSecret); } } catch { /* noop */ } }; useEffect(() => { (async () => { try { const clR = await fetch(`${NW_API}/nw/clinicas`, { headers: nwAuthHeaders() }); if (clR.ok) setClinics(await clR.json()); await loadCfg(); } catch { /* noop */ } finally { setLoading(false); } })(); }, []); const applyToken = async () => { if (!token.trim()) return; setApplying(true); setTest(null); try { const r = await fetch(`${NW_API}/nw/config/token`, { method: 'POST', headers: nwAuthHeaders(), body: JSON.stringify({ token: token.trim() }) }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'Falha ao aplicar token'); await loadCfg(); setToken(''); setTest({ ok: true, msg: 'Token aplicado — ' + (j.detail || 'configurado') }); toast.success('TOKEN APLICADO! Conexão configurada.'); } catch (e: any) { setTest({ ok: false, msg: String(e.message).slice(0, 120) }); } finally { setApplying(false); } }; const handleSave = async () => { setSaving(true); try { const body: any = { motorUrl, clinicaId }; if (integrationKey) body.integrationKey = integrationKey; if (webhookSecret) body.webhookSecret = webhookSecret; const r = await fetch(`${NW_API}/nw/config`, { method: 'PUT', headers: nwAuthHeaders(), body: JSON.stringify(body) }); if (!r.ok) throw new Error(await r.text()); toast.success('CONFIGURAÇÕES SALVAS!'); onClose(); } catch (e: any) { toast.error('FALHA AO SALVAR: ' + String(e.message).slice(0, 80)); } finally { setSaving(false); } }; const handleTest = async () => { setTest(null); try { const r = await fetch(`${NW_API}/nw/status`, { headers: nwAuthHeaders() }); const s = await r.json(); setTest({ ok: !!s.motorOk, msg: s.detail || (s.motorOk ? 'OK' : 'Falha') }); } catch (e: any) { setTest({ ok: false, msg: String(e.message).slice(0, 80) }); } }; const inputCls = "w-full border border-gray-200 rounded-xl px-4 py-2.5 text-sm focus:outline-none focus:border-green-400 focus:ring-2 focus:ring-green-100 bg-white"; return (

CONFIGURAR {plugin.name}

CONEXÃO COM O MOTOR

{loading ?
Carregando…
: <> {/* Caminho principal: colar o token de pareamento → auto-configura */}