feat(newwhats): agenda inteligente — especialidades, situações, dúvidas, checklist do dono e pendências
Satélite da Secretária IA: - /contexto (dentistas+especialidades+situações+cautelas) e /duvida (encaminhar ao humano sem agendar); agenda_pedidos ganha tipo=duvida + resumo. - Situações da clínica: tabela agenda_situacoes + config (dono) + aba na UI. - Checklist do dono (checklist-defs.js + agenda_checklist): perguntas Sim/Não conferidas contra o banco (dentistas/pacientes) → pendências de config + cautelas p/ a IA não chutar. - Área de PENDÊNCIAS DO DONO (PendenciasDono.tsx): engloba config + fila operacional (horários/dúvidas); botão+badge no /wa-secretaria (só o dono). - Fila humana (PedidosPendentes): cards de dúvida + resolver; /resolver. - Botão DESLIGAR global da Secretária no ThinNav (secPowerApi, visível a todos). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,10 +8,14 @@ import {
|
||||
Sparkles, BookOpen, ShieldCheck, Calendar, AlertTriangle,
|
||||
CheckCircle, Clock, User, RefreshCw, MoreVertical, Power, Cpu,
|
||||
Phone, Shield, Star, Repeat, UserCheck, Building2, Stethoscope,
|
||||
Maximize2, Save, HelpCircle, ArrowLeft,
|
||||
Maximize2, Save, HelpCircle, ArrowLeft, ClipboardCheck,
|
||||
} from 'lucide-react'
|
||||
import { useSecretariaStore } from './store/secretariaStore'
|
||||
import type { SecAgent, BrainNode, SecConversation, BrainNodeType, CalendarSlot, SecNumber, NumberRole } from './services/secretariaApi'
|
||||
import { HybridBackend } from '../../services/backend.ts'
|
||||
import { PendenciasDono } from '../../components/PendenciasDono.tsx'
|
||||
import { PedidosPendentes } from '../../components/PedidosPendentes.tsx'
|
||||
import { secPowerApi } from './services/secretariaApi'
|
||||
|
||||
// ─── Thin Nav ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -25,7 +29,7 @@ const NAV_ITEMS: { id: NavTab; icon: React.ElementType; label: string }[] = [
|
||||
{ id: 'numbers', icon: Phone, label: 'Números' },
|
||||
]
|
||||
|
||||
function ThinNav({ active, onChange, onHelp, onBack }: { active: NavTab; onChange: (t: NavTab) => void; onHelp: () => void; onBack?: () => void }) {
|
||||
function ThinNav({ active, onChange, onHelp, onBack, onPendencias, pendenciasBadge, secOn, onTogglePower }: { active: NavTab; onChange: (t: NavTab) => void; onHelp: () => void; onBack?: () => void; onPendencias?: () => void; pendenciasBadge?: number; secOn?: boolean; onTogglePower?: () => void }) {
|
||||
return (
|
||||
<div className="w-[60px] h-full bg-white border-r border-gray-100 flex flex-col items-center py-4 shrink-0 z-30 gap-1">
|
||||
{onBack && (
|
||||
@@ -54,6 +58,34 @@ function ThinNav({ active, onChange, onHelp, onBack }: { active: NavTab; onChang
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Desligar/ligar a Secretária — GLOBAL, todos os chats, sem timer de retorno.
|
||||
Botão de segurança enquanto a IA amadurece; visível a todos. */}
|
||||
{onTogglePower && (
|
||||
<button
|
||||
title={secOn ? 'Desligar a Secretária (todos os chats)' : 'Secretária DESLIGADA — clique para ligar'}
|
||||
onClick={onTogglePower}
|
||||
className={`relative w-10 h-10 rounded-xl flex items-center justify-center transition-all mb-1 ${
|
||||
secOn ? 'text-emerald-600 hover:bg-emerald-50' : 'text-white bg-red-600 hover:bg-red-700 animate-pulse'
|
||||
}`}
|
||||
>
|
||||
<Power size={20} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Pendências do dono — checklist + config + fila (só o dono do workspace) */}
|
||||
{onPendencias && (
|
||||
<button
|
||||
title="Pendências do workspace"
|
||||
onClick={onPendencias}
|
||||
className="relative w-10 h-10 rounded-xl flex items-center justify-center text-gray-500 hover:text-indigo-600 hover:bg-gray-50 transition-all mb-1"
|
||||
>
|
||||
<ClipboardCheck size={20} />
|
||||
{!!pendenciasBadge && pendenciasBadge > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 px-1 bg-indigo-600 text-white text-[9px] font-black rounded-full flex items-center justify-center">{pendenciasBadge > 99 ? '99+' : pendenciasBadge}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Ajuda — modelos de como usar e configurar a secretária */}
|
||||
<button
|
||||
title="Ajuda — como usar"
|
||||
@@ -1578,13 +1610,52 @@ export function SecretariaView(props: { onNavigate?: (view: string) => void } =
|
||||
const [openNode, setOpenNode] = useState<BrainNode | null>(null)
|
||||
const [showHelp, setShowHelp] = useState(false)
|
||||
|
||||
// Pendências do dono (checklist + config + fila) — só o dono do workspace
|
||||
const clinicaId = (HybridBackend.getActiveWorkspace() as any)?.id as string | undefined
|
||||
const ehDono = ['admin', 'donoclinica', 'donoconsultorio'].includes(HybridBackend.getCurrentRole?.() || '')
|
||||
const [showPendencias, setShowPendencias] = useState(false)
|
||||
const [showFila, setShowFila] = useState(false)
|
||||
const [pendCount, setPendCount] = useState(0)
|
||||
|
||||
// Liga/desliga GLOBAL da Secretária (kill-switch auto_reply) — todos, sem timer
|
||||
const [secOn, setSecOn] = useState(true)
|
||||
const [powerBusy, setPowerBusy] = useState(false)
|
||||
|
||||
// Boot
|
||||
useEffect(() => {
|
||||
store.loadAgents()
|
||||
store.loadCalendar()
|
||||
store.loadNumbers()
|
||||
secPowerApi.get().then((r) => setSecOn(r.enabled)).catch(() => {})
|
||||
}, [])
|
||||
|
||||
const handleTogglePower = 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])
|
||||
|
||||
// Contagem de pendências para o badge (dono) — atualiza ao abrir e a cada 5 min.
|
||||
useEffect(() => {
|
||||
if (!ehDono || !clinicaId) return
|
||||
let vivo = true
|
||||
const carregar = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN')
|
||||
const API = (import.meta as any).env?.VITE_API_URL || '/api'
|
||||
const r = await fetch(`${API}/nw/agenda-config/clinica/${clinicaId}/pendencias`, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
|
||||
if (r.ok && vivo) { const d = await r.json(); setPendCount(d.total || 0) }
|
||||
} catch { /* silencioso */ }
|
||||
}
|
||||
carregar()
|
||||
const t = setInterval(carregar, 5 * 60 * 1000)
|
||||
return () => { vivo = false; clearInterval(t) }
|
||||
}, [ehDono, clinicaId, showPendencias, showFila])
|
||||
|
||||
// Sincroniza openNode quando o store atualiza o nó (ex: toggle active, node_model)
|
||||
useEffect(() => {
|
||||
if (!openNode) return
|
||||
@@ -1637,7 +1708,17 @@ export function SecretariaView(props: { onNavigate?: (view: string) => void } =
|
||||
<div className="flex h-full w-full bg-gray-50 overflow-hidden font-sans">
|
||||
|
||||
{/* Thin Nav */}
|
||||
<ThinNav active={activeTab} onChange={handleTabChange} onHelp={() => setShowHelp(true)} onBack={() => props.onNavigate?.('dashboard')} />
|
||||
<ThinNav active={activeTab} onChange={handleTabChange} onHelp={() => setShowHelp(true)} onBack={() => props.onNavigate?.('dashboard')}
|
||||
onPendencias={ehDono ? () => setShowPendencias(true) : undefined} pendenciasBadge={pendCount}
|
||||
secOn={secOn} onTogglePower={handleTogglePower} />
|
||||
|
||||
{ehDono && (
|
||||
<>
|
||||
<PendenciasDono isOpen={showPendencias} onClose={() => setShowPendencias(false)} clinicaId={clinicaId}
|
||||
onOpenFila={() => { setShowPendencias(false); setShowFila(true) }} />
|
||||
<PedidosPendentes isOpen={showFila} onClose={() => setShowFila(false)} clinicaId={clinicaId} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{showHelp && (
|
||||
<HelpModal
|
||||
|
||||
@@ -83,6 +83,13 @@ export interface CalendarSlot {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// ─── Power (liga/desliga global — kill-switch auto_reply) ──────────────────────
|
||||
|
||||
export const secPowerApi = {
|
||||
get: () => api.get<{ enabled: boolean }>(`${BASE}/power`).then((r) => r.data),
|
||||
set: (enabled: boolean) => api.post<{ enabled: boolean }>(`${BASE}/power`, { enabled }).then((r) => r.data),
|
||||
}
|
||||
|
||||
// ─── Agents ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const secAgentApi = {
|
||||
|
||||
Reference in New Issue
Block a user