From 65244c983f4c2a411bfb09de0f6a7e22fe0113d5 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Mon, 6 Jul 2026 22:49:34 +0200 Subject: [PATCH] =?UTF-8?q?feat(superadmin+wa-secretaria):=20motivos=20do?= =?UTF-8?q?=20bot=C3=A3o=20"desativar=20para=20todos=20os=20chats"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Botão global no /wa-secretaria agora abre modal com textarea de motivo (obrigatório p/ desativar) + histórico, em vez de um confirm. Página super admin "Secretária IA" passa a listar os motivos por ESCOPO: "Todos os chats" (global) e "Uma sessão", mesclando os dois logs do motor (/secretaria/power/log + /session-power/log). Co-Authored-By: Claude Opus 4.8 --- backend/server.js | 22 +++-- frontend/views/SuperAdminView.tsx | 10 ++- frontend/views/newwhats/SecretariaView.tsx | 88 ++++++++++++++++--- .../views/newwhats/services/secretariaApi.ts | 4 +- 4 files changed, 104 insertions(+), 20 deletions(-) diff --git a/backend/server.js b/backend/server.js index 8d81c3b..46a8c57 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9450,12 +9450,22 @@ app.get('/api/superadmin/secretaria-desligamentos', superadminGuard, async (req, 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 : [] }); + const headers = { 'x-nw-key': cfg.integrationKey, 'x-nw-email': email }; + const pull = async (path, escopo) => { + try { + const r = await fetch(`${cfg.motorUrl}/api/ext/v1${path}?limit=${limit}`, { headers }); + if (!r.ok) return []; + const j = await r.json(); + return (Array.isArray(j) ? j : []).map((e) => ({ ...e, escopo })); + } catch { return []; } + }; + // "global" = botão que desativa a Secretária para TODOS os chats; "sessao" = por número. + const [glob, sess] = await Promise.all([ + pull('/secretaria/power/log', 'global'), + pull('/secretaria/session-power/log', 'sessao'), + ]); + const log = [...glob, ...sess].sort((a, b) => new Date(b.at) - new Date(a.at)); + res.json({ configurado: true, log }); } catch (err) { res.status(500).json({ error: err.message }); } }); diff --git a/frontend/views/SuperAdminView.tsx b/frontend/views/SuperAdminView.tsx index ab166d0..e619092 100644 --- a/frontend/views/SuperAdminView.tsx +++ b/frontend/views/SuperAdminView.tsx @@ -113,8 +113,8 @@ const TabSecretaria: React.FC = () => {
-

Secretária IA — motivos de desligamento

-

Contexto GERAL para identificar bugs/estratégias no atendimento da IA e corrigir. Sem dados das clínicas (nem número, nem quem). {totalOff > 0 && `${totalOff} desligamento(s).`}

+

Secretária IA — motivos de desativação

+

Motivos de desativar a Secretária — para TODOS os chats (global) ou por sessão. Contexto GERAL p/ identificar bugs/estratégias no atendimento e corrigir. Sem dados das clínicas. {totalOff > 0 && `${totalOff} desativação(ões).`}

@@ -130,6 +130,7 @@ const TabSecretaria: React.FC = () => { Ação + Escopo Quando Motivo @@ -142,6 +143,11 @@ const TabSecretaria: React.FC = () => { ? Ligou : Desligou} + + {l.escopo === 'global' + ? Todos os chats + : Uma sessão} + {fmt(l.at)} {l.reason || } diff --git a/frontend/views/newwhats/SecretariaView.tsx b/frontend/views/newwhats/SecretariaView.tsx index 472b8c4..9f029b9 100644 --- a/frontend/views/newwhats/SecretariaView.tsx +++ b/frontend/views/newwhats/SecretariaView.tsx @@ -1617,27 +1617,41 @@ export function SecretariaView(props: { onNavigate?: (view: string) => void } = const [showFila, setShowFila] = useState(false) const [pendCount, setPendCount] = useState(0) - // Liga/desliga GLOBAL da Secretária (kill-switch auto_reply) — todos, sem timer + // Liga/desliga GLOBAL da Secretária (kill-switch auto_reply) — todos os chats. + // Ao desligar exige MOTIVO (contexto geral p/ o super admin) e guarda histórico. const [secOn, setSecOn] = useState(true) const [powerBusy, setPowerBusy] = useState(false) + const [powerModal, setPowerModal] = useState(false) + const [powerReason, setPowerReason] = useState('') + const [powerData, setPowerData] = useState(null) + const [powerErr, setPowerErr] = useState(null) // Boot useEffect(() => { store.loadAgents() store.loadCalendar() store.loadNumbers() - secPowerApi.get().then((r) => setSecOn(r.enabled)).catch(() => {}) + secPowerApi.get().then((r) => { setSecOn(r.enabled); setPowerData(r) }).catch(() => {}) }, []) - const handleTogglePower = useCallback(async () => { + const openPowerModal = useCallback(() => { + setPowerErr(null); setPowerReason('') + secPowerApi.get().then((r) => { setSecOn(r.enabled); setPowerData(r) }).catch(() => {}) + setPowerModal(true) + }, []) + + const confirmTogglePower = useCallback(async () => { if (powerBusy) return const proximo = !secOn - // Ao DESLIGAR (para todos os chats, sem retorno automático), confirma. - if (!proximo && !window.confirm('Desligar a Secretária para TODOS os chats? Ela para de responder e só volta quando você ligar de novo.')) return - setPowerBusy(true) - try { const r = await secPowerApi.set(proximo); setSecOn(r.enabled) } - catch { /* mantém estado atual */ } finally { setPowerBusy(false) } - }, [secOn, powerBusy]) + if (!proximo && !powerReason.trim()) { setPowerErr('Escreva o motivo para desativar.'); return } + setPowerBusy(true); setPowerErr(null) + try { + const userName = (() => { try { return JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}')?.nome || 'Usuário' } catch { return 'Usuário' } })() + const r = await secPowerApi.set(proximo, powerReason.trim(), userName) + setSecOn(r.enabled); setPowerReason('') + const fresh = await secPowerApi.get().catch(() => null); if (fresh) setPowerData(fresh) + } catch (e: any) { setPowerErr(e?.message || 'Não foi possível alterar.') } finally { setPowerBusy(false) } + }, [secOn, powerBusy, powerReason]) // Contagem de pendências para o badge (dono) — atualiza ao abrir e a cada 5 min. useEffect(() => { @@ -1710,7 +1724,61 @@ export function SecretariaView(props: { onNavigate?: (view: string) => void } = {/* Thin Nav */} setShowHelp(true)} onBack={() => props.onNavigate?.('dashboard')} onPendencias={ehDono ? () => setShowPendencias(true) : undefined} pendenciasBadge={pendCount} - secOn={secOn} onTogglePower={handleTogglePower} /> + secOn={secOn} onTogglePower={openPowerModal} /> + + {powerModal && ( +
setPowerModal(false)}> +
e.stopPropagation()}> +
+
+
+
+

Secretária — todos os chats

+

{secOn ? 'Ativa' : 'Desativada'} · vale para TODAS as sessões

+
+
+ +
+ {powerErr &&
{powerErr}
} +
+ {!secOn && powerData?.reason && ( +
Desativada. Motivo: {powerData.reason}
+ )} + {secOn && ( +
+ +