From b67fa15c551bb19a1a39a5d1695760d9dd6ff0ec Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Mon, 6 Jul 2026 20:02:39 +0200 Subject: [PATCH] =?UTF-8?q?feat(superadmin):=20p=C3=A1gina=20"Secret=C3=A1?= =?UTF-8?q?ria=20IA"=20com=20motivos=20de=20liga/desliga=20por=20sess?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nova aba no super admin (/superadmin/secretaria): tabela com quem ligou/desligou a Secretária de cada número, quando e o MOTIVO do desligamento. Endpoint /api/superadmin/secretaria-desligamentos (superadminGuard) busca o log global no motor (resolve um e-mail de dono pareado; log é global). Item no Sidebar + rotas. Co-Authored-By: Claude Opus 4.8 --- backend/server.js | 21 +++++++++ frontend/App.tsx | 28 ++++++++++-- frontend/components/Sidebar.tsx | 3 +- frontend/views/SuperAdminView.tsx | 72 ++++++++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/backend/server.js b/backend/server.js index 497e4c4..8d81c3b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9438,6 +9438,27 @@ app.get('/api/superadmin/login-log', superadminGuard, async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); +// Super admin: motivos de LIGAR/DESLIGAR a Secretária por sessão (histórico global, +// vem do motor — sec_session_power_log). Requer integração newwhats configurada. +app.get('/api/superadmin/secretaria-desligamentos', superadminGuard, async (req, res) => { + try { + const { getConfig } = await import('./newwhats/config.js'); + const cfg = getConfig(); + if (!cfg.motorUrl || !cfg.integrationKey) return res.json({ configurado: false, log: [] }); + // O log é GLOBAL no motor; qualquer conta de dono pareada serve para autenticar. + const { rows } = await pool.query(`SELECT u.email FROM usuarios u JOIN vinculos v ON v.usuario_id = u.id WHERE v.role IN ('donoclinica','donoconsultorio') ORDER BY u.email LIMIT 1`); + const email = rows[0]?.email; + if (!email) return res.json({ configurado: true, log: [] }); + const limit = Math.min(500, Math.max(1, parseInt(String(req.query.limit || '150'), 10))); + const r = await fetch(`${cfg.motorUrl}/api/ext/v1/secretaria/session-power/log?limit=${limit}`, { + headers: { 'x-nw-key': cfg.integrationKey, 'x-nw-email': email }, + }); + if (!r.ok) return res.status(502).json({ error: 'Motor indisponível', status: r.status }); + const log = await r.json(); + res.json({ configurado: true, log: Array.isArray(log) ? log : [] }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + // Defaults mesclados sob o JSON salvo: chaves novas (biomedico, add-ons de plugin) // aparecem na tabela mesmo p/ configs gravadas antes delas existirem. const PRICING_DEFAULTS = { diff --git a/frontend/App.tsx b/frontend/App.tsx index e2bf915..4eeb8d0 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -121,6 +121,7 @@ export type ViewKey = | 'superadmin-logs' | 'superadmin-financeiro' | 'superadmin-notificacoes' + | 'superadmin-secretaria' | 'diretorio-profissionais'; // ------- URL <-> VIEW mapping ------- @@ -181,6 +182,7 @@ const ROUTE_MAP: Record = { '/superadmin/logs': 'superadmin-logs', '/superadmin/financeiro': 'superadmin-financeiro', '/superadmin/notificacoes': 'superadmin-notificacoes', + '/superadmin/secretaria': 'superadmin-secretaria', '/aguardando-convite': 'aguardando-convite', '/diretorio-profissionais': 'diretorio-profissionais', '/proteses': 'proteses', @@ -248,6 +250,7 @@ const VIEW_TO_ROUTE: Record = { 'superadmin-logs': '/superadmin/logs', 'superadmin-financeiro': '/superadmin/financeiro', 'superadmin-notificacoes': '/superadmin/notificacoes', + 'superadmin-secretaria': '/superadmin/secretaria', 'diretorio-profissionais': '/diretorio-profissionais', proteses: '/proteses', }; @@ -315,6 +318,13 @@ const MobileBottomNav: React.FC<{ const App: React.FC = () => { const ver = useAppVersion(); const [isSidebarOpen, setSidebarOpen] = useState(false); + // Evita que a sidebar ANIME (slide-in) a cada reload no desktop: a transição só liga + // após o 1º paint, então no load ela já nasce posicionada; toggles do usuário animam. + const [uiMounted, setUiMounted] = useState(false); + useEffect(() => { setUiMounted(true); }, []); + // Gaveta de controles da Agenda no MOBILE (título/AGENDAR/ações) — comporta-se como + // a sidebar: some por padrão p/ dar o máximo de espaço ao calendário; abre pelo ⋯. + const [agendaControlsOpen, setAgendaControlsOpen] = useState(false); // Recolher a sidebar no DESKTOP (persistente) — dá largura ao conteúdo (ex.: agenda). const [isSidebarCollapsed, setSidebarCollapsed] = useState(() => { try { return localStorage.getItem('scoreodonto:sidebar-collapsed') === '1'; } catch { return false; } @@ -508,7 +518,7 @@ const App: React.FC = () => { case 'leads': return ; case 'public': return ; case 'pacientes': return ; - case 'agenda': return toggleSidebarCollapsed(!isSidebarCollapsed)} />; + case 'agenda': return toggleSidebarCollapsed(!isSidebarCollapsed)} controlsOpen={agendaControlsOpen} onCloseControls={() => setAgendaControlsOpen(false)} />; case 'ortodontia': return ; case 'financeiro': return ; case 'dentistas': return ; @@ -561,6 +571,7 @@ const App: React.FC = () => { case 'superadmin-logs': return ; case 'superadmin-financeiro': return ; case 'superadmin-notificacoes': return ; + case 'superadmin-secretaria': return ; // Plugin Profissionais ativo absorve o diretório antigo (mesma rota, view nova). case 'diretorio-profissionais': return isPluginActive('profissionais-marketplace') ? : ; case 'login': @@ -599,7 +610,7 @@ const App: React.FC = () => { > )} -
+
handleNavigate(t as ViewKey)} @@ -652,7 +663,18 @@ const App: React.FC = () => {
SCOREODONTO
-
+ {/* ⋯ controles da Agenda (só nesta view) — abre a gaveta; senão, espaçador */} + {currentView === 'agenda' ? ( + + ) : ( +
+ )} )} diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index 30257d0..f29af10 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useConfirm } from '../contexts/ConfirmContext.tsx'; import { Users, Calendar as CalendarIcon, LayoutDashboard, - DollarSign, RefreshCw, Activity, BookOpen, Award, Megaphone, LogOut, ClipboardList, Bell, Building2, User, BarChart3, Settings, FileText, Puzzle, ScanLine, Monitor, UsersRound, GraduationCap, UserSearch, Briefcase, TrendingUp, Eye, FlaskConical, DoorOpen, Sparkles, FileSignature, Percent, ShieldAlert, ChevronDown, MessageCircle, Bot, Smartphone + DollarSign, RefreshCw, Activity, BookOpen, Award, Megaphone, LogOut, ClipboardList, Bell, Building2, User, BarChart3, Settings, FileText, Puzzle, ScanLine, Monitor, UsersRound, GraduationCap, UserSearch, Briefcase, TrendingUp, Eye, FlaskConical, DoorOpen, Sparkles, FileSignature, Percent, ShieldAlert, ChevronDown, MessageCircle, Bot, Smartphone, Power } from 'lucide-react'; import { ToothIcon } from './ToothIcon.tsx'; import { HybridBackend } from '../services/backend.ts'; @@ -83,6 +83,7 @@ export const Sidebar: React.FC = ({ activeTab, setActiveTab }) => { id: 'superadmin-logs', label: 'ACESSOS', icon: Activity }, { id: 'superadmin-financeiro', label: 'FINANCEIRO', icon: DollarSign }, { id: 'superadmin-notificacoes', label: 'NOTIFICAÇÕES', icon: Bell }, + { id: 'superadmin-secretaria', label: 'SECRETÁRIA IA', icon: Power }, { id: 'sala-home', label: 'PAINEL DA SALA', icon: DoorOpen }, { id: 'lab-home', label: 'PAINEL DO LAB', icon: LayoutDashboard }, { id: 'lab-bancada', label: 'BANCADA', icon: FlaskConical }, diff --git a/frontend/views/SuperAdminView.tsx b/frontend/views/SuperAdminView.tsx index e32b70d..18a77cb 100644 --- a/frontend/views/SuperAdminView.tsx +++ b/frontend/views/SuperAdminView.tsx @@ -5,7 +5,7 @@ import { CheckCircle2, XCircle, Clock, Search, ChevronLeft, ChevronRight, TrendingUp, Stethoscope, FlaskConical, User, ToggleLeft, ToggleRight, Save, AlertTriangle, Wifi, WifiOff, Briefcase, Trash2, Bell, Send, - MapPin, Phone, MoreVertical, X, ArrowLeft, UserX, Sparkles, DoorOpen, UserSearch + MapPin, Phone, MoreVertical, X, ArrowLeft, UserX, Sparkles, DoorOpen, UserSearch, Power } from 'lucide-react'; const apiFetch = (url: string, opts: RequestInit = {}) => { @@ -74,7 +74,7 @@ const KPI: React.FC<{ icon: React.ElementType; label: string; value: string | nu ); }; -type SuperAdminTab = 'overview' | 'users' | 'clinicas' | 'logs' | 'financeiro' | 'notificacoes'; +type SuperAdminTab = 'overview' | 'users' | 'clinicas' | 'logs' | 'financeiro' | 'notificacoes' | 'secretaria'; /* ═══════════════════════════════════════════════════ MAIN VIEW */ export const SuperAdminView: React.FC<{ tab?: SuperAdminTab }> = ({ tab = 'overview' }) => { @@ -86,6 +86,74 @@ export const SuperAdminView: React.FC<{ tab?: SuperAdminTab }> = ({ tab = 'overv {tab === 'logs' && } {tab === 'financeiro' && } {tab === 'notificacoes' && } + {tab === 'secretaria' && } + + ); +}; + +/* ═══════════════════════════════════════════════════ TAB SECRETÁRIA (liga/desliga por sessão) */ +const TabSecretaria: React.FC = () => { + const [log, setLog] = useState([]); + const [configurado, setConfigurado] = useState(true); + const [loading, setLoading] = useState(true); + + const load = useCallback(() => { + setLoading(true); + apiFetch('/api/superadmin/secretaria-desligamentos') + .then(r => r.json()) + .then(d => { setLog(Array.isArray(d.log) ? d.log : []); setConfigurado(d.configurado !== false); }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + useEffect(() => { load(); }, [load]); + + const totalOff = log.filter(l => !l.enabled).length; + + return ( +
+
+
+

Secretária IA — liga/desliga por sessão

+

Quem ligou/desligou a Secretária de cada número, e o motivo do desligamento. {totalOff > 0 && `${totalOff} desligamento(s).`}

+
+ +
+ {!configurado ? ( +
Integração com o motor da Secretária não está configurada neste ambiente.
+ ) : loading ? ( +
Carregando…
+ ) : log.length === 0 ? ( +
Nenhum registro de liga/desliga ainda.
+ ) : ( +
+ + + + + + + + + + + + {log.map((l, i) => ( + + + + + + + + ))} + +
AçãoNúmeroQuemQuandoMotivo
+ {l.enabled + ? Ligou + : Desligou} + {l.phone || l.name || l.instance_id}{l.by || '—'}{fmt(l.at)}{l.reason || }
+
+ )}
); }; -- 2.53.0