feat(wa-inbox): botão liga/desliga da Secretária por sessão na thin sidebar
Botão Power na thin sidebar do /wa-inbox age SÓ na sessão/número aberto (verde=on, vermelho pulsante=off). Abre modal com textarea de motivo (obrigatório p/ desligar), histórico de quem ligou/desligou e aviso de rate-limit (1x/hora). secSessionPowerApi + SessaoSecretariaPower.tsx. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
// Liga/desliga a Secretária IA APENAS desta sessão (instância aberta no /wa-inbox).
|
||||||
|
// Rate-limit 1x/hora, motivo obrigatório ao desligar, histórico de quem apertou.
|
||||||
|
// Fica na thin sidebar; abre um modal com o textarea de motivo + histórico.
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Power, X, Loader2, History, AlertTriangle } from 'lucide-react';
|
||||||
|
import { secSessionPowerApi, SessionPower } from '../services/secretariaApi';
|
||||||
|
|
||||||
|
const userName = (() => {
|
||||||
|
try { return JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}')?.nome || 'Usuário'; }
|
||||||
|
catch { return 'Usuário'; }
|
||||||
|
})();
|
||||||
|
|
||||||
|
const fmt = (iso?: string | null) => {
|
||||||
|
if (!iso) return '';
|
||||||
|
try { return new Date(iso).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); }
|
||||||
|
catch { return ''; }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SessaoSecretariaPower: React.FC<{ instanceId: string | null }> = ({ instanceId }) => {
|
||||||
|
const [sp, setSp] = useState<SessionPower | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [erro, setErro] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const carregar = useCallback(async () => {
|
||||||
|
if (!instanceId) return;
|
||||||
|
try { setSp(await secSessionPowerApi.get(instanceId)); } catch { /* silencioso */ }
|
||||||
|
}, [instanceId]);
|
||||||
|
|
||||||
|
useEffect(() => { carregar(); }, [carregar]);
|
||||||
|
|
||||||
|
if (!instanceId) return null;
|
||||||
|
const enabled = sp ? sp.enabled : true;
|
||||||
|
|
||||||
|
const toggle = async () => {
|
||||||
|
if (!sp?.can_toggle) return;
|
||||||
|
const proximo = !enabled;
|
||||||
|
if (proximo === false && !reason.trim()) { setErro('Escreva o motivo para desligar.'); return; }
|
||||||
|
setBusy(true); setErro(null);
|
||||||
|
try {
|
||||||
|
await secSessionPowerApi.set(instanceId, proximo, reason.trim(), userName);
|
||||||
|
setReason('');
|
||||||
|
await carregar();
|
||||||
|
} catch (e: any) {
|
||||||
|
setErro(e?.message || 'Não foi possível alterar.');
|
||||||
|
await carregar();
|
||||||
|
} finally { setBusy(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
title={enabled ? 'Secretária IA ligada nesta sessão — clique para gerenciar' : 'Secretária IA DESLIGADA nesta sessão'}
|
||||||
|
onClick={() => { setErro(null); setOpen(true); }}
|
||||||
|
className={`w-10 h-10 mb-2 rounded-full flex items-center justify-center transition-colors ${
|
||||||
|
enabled ? 'text-emerald-600 hover:bg-[#d9dbdf]/60' : 'text-white bg-red-600 hover:bg-red-700 animate-pulse'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Power className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div className="fixed inset-0 bg-black/60 z-[80] flex items-center justify-center backdrop-blur-md p-4" onClick={() => setOpen(false)}>
|
||||||
|
<div className="bg-white w-full max-w-md rounded-[1.5rem] shadow-2xl border border-gray-100 flex flex-col max-h-[90vh] overflow-hidden" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="px-6 py-5 border-b border-gray-50 flex items-center justify-between bg-gradient-to-br from-gray-50 to-white">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded-xl ${enabled ? 'bg-emerald-500' : 'bg-red-600'} shadow`}><Power size={18} className="text-white" /></div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-black text-gray-900 uppercase tracking-tighter leading-none">Secretária desta sessão</h2>
|
||||||
|
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mt-1">{enabled ? 'Ligada' : 'Desligada'} · só este número</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setOpen(false)} className="p-2 hover:bg-gray-100 rounded-xl text-gray-400"><X size={20} /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{erro && <div className="px-6 py-2 text-xs font-bold bg-red-50 text-red-600">{erro}</div>}
|
||||||
|
|
||||||
|
<div className="p-6 space-y-4 overflow-y-auto">
|
||||||
|
{!enabled && sp?.reason && (
|
||||||
|
<div className="text-xs bg-red-50 text-red-700 rounded-xl p-3">
|
||||||
|
<span className="font-black">Desligada.</span> Motivo: {sp.reason}
|
||||||
|
{sp.changed_by && <div className="text-red-400 mt-1">por {sp.changed_by} · {fmt(sp.changed_at)}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Motivo (só ao desligar) */}
|
||||||
|
{enabled && (
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest block mb-1.5">Motivo para desligar</label>
|
||||||
|
<textarea value={reason} onChange={(e) => setReason(e.target.value)} rows={3}
|
||||||
|
placeholder="Ex.: revisando as respostas da IA; horário de pico; treinamento…"
|
||||||
|
className="w-full bg-gray-50 border-2 border-transparent focus:border-red-500 rounded-xl px-3 py-2 text-sm outline-none resize-y" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Rate-limit */}
|
||||||
|
{!sp?.can_toggle && (
|
||||||
|
<p className="text-[11px] text-amber-600 font-bold flex items-center gap-1.5"><AlertTriangle size={13} /> Só é possível alternar 1x por hora. Disponível às {fmt(sp?.next_toggle_at)}.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button onClick={toggle} disabled={busy || !sp?.can_toggle}
|
||||||
|
className={`w-full py-2.5 rounded-xl text-sm font-black uppercase tracking-wide flex items-center justify-center gap-2 disabled:opacity-40 ${
|
||||||
|
enabled ? 'bg-red-600 hover:bg-red-700 text-white' : 'bg-emerald-600 hover:bg-emerald-700 text-white'
|
||||||
|
}`}>
|
||||||
|
{busy ? <Loader2 size={16} className="animate-spin" /> : <Power size={16} />}
|
||||||
|
{enabled ? 'Desligar nesta sessão' : 'Ligar nesta sessão'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Histórico */}
|
||||||
|
{!!sp?.history?.length && (
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest flex items-center gap-1.5 mb-2"><History size={12} /> Histórico</p>
|
||||||
|
<div className="space-y-1.5 max-h-40 overflow-y-auto">
|
||||||
|
{sp.history.map((h, i) => (
|
||||||
|
<div key={i} className="text-[11px] flex items-start gap-2">
|
||||||
|
<span className={`mt-1 w-1.5 h-1.5 rounded-full shrink-0 ${h.enabled ? 'bg-emerald-500' : 'bg-red-500'}`} />
|
||||||
|
<div className="min-w-0">
|
||||||
|
<span className="font-bold text-gray-700">{h.enabled ? 'Ligou' : 'Desligou'}</span>
|
||||||
|
<span className="text-gray-400"> · {h.by || '—'} · {fmt(h.at)}</span>
|
||||||
|
{!h.enabled && h.reason && <div className="text-gray-500 leading-snug">{h.reason}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
BrainCircuit,
|
BrainCircuit,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { getAvatarProxyUrl } from '../utils/inboxUtils';
|
import { getAvatarProxyUrl } from '../utils/inboxUtils';
|
||||||
|
import { SessaoSecretariaPower } from './SessaoSecretariaPower';
|
||||||
|
|
||||||
export type TabType = 'chats' | 'broadcasts' | 'groups' | 'settings';
|
export type TabType = 'chats' | 'broadcasts' | 'groups' | 'settings';
|
||||||
|
|
||||||
@@ -64,6 +65,8 @@ export default function ThinSidebar({ activeTab, setActiveTab, activeInstance, o
|
|||||||
|
|
||||||
{/* Bottom Icons */}
|
{/* Bottom Icons */}
|
||||||
<div className="w-full flex flex-col items-center pb-2">
|
<div className="w-full flex flex-col items-center pb-2">
|
||||||
|
{/* Liga/desliga a Secretária IA APENAS desta sessão (número atual) */}
|
||||||
|
<SessaoSecretariaPower instanceId={activeInstance?.instance ?? null} />
|
||||||
{/* Configurações — abre o painel na área central (aba 'settings') */}
|
{/* Configurações — abre o painel na área central (aba 'settings') */}
|
||||||
{renderIcon('settings', Settings, 'Configurações')}
|
{renderIcon('settings', Settings, 'Configurações')}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,22 @@ export const secPowerApi = {
|
|||||||
set: (enabled: boolean) => api.post<{ enabled: boolean }>(`${BASE}/power`, { enabled }).then((r) => r.data),
|
set: (enabled: boolean) => api.post<{ enabled: boolean }>(`${BASE}/power`, { enabled }).then((r) => r.data),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Liga/desliga a Secretária POR SESSÃO (instância atual do /wa-inbox).
|
||||||
|
export interface SessionPower {
|
||||||
|
enabled: boolean
|
||||||
|
reason: string | null
|
||||||
|
changed_by: string | null
|
||||||
|
changed_at: string | null
|
||||||
|
can_toggle: boolean
|
||||||
|
next_toggle_at: string | null
|
||||||
|
history: { enabled: boolean; reason: string | null; by: string | null; at: string }[]
|
||||||
|
}
|
||||||
|
export const secSessionPowerApi = {
|
||||||
|
get: (instanceId: string) => api.get<SessionPower>(`${BASE}/session-power`, { params: { instance_id: instanceId } }).then((r) => r.data),
|
||||||
|
set: (instanceId: string, enabled: boolean, reason: string, by: string) =>
|
||||||
|
api.post<{ ok: boolean; enabled: boolean }>(`${BASE}/session-power`, { instance_id: instanceId, enabled, reason, by }).then((r) => r.data),
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Agents ───────────────────────────────────────────────────────────────────
|
// ─── Agents ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const secAgentApi = {
|
export const secAgentApi = {
|
||||||
|
|||||||
Reference in New Issue
Block a user