Files
scoreodonto.com/frontend/components/PendenciasDono.tsx
T
VPS 4 Builder 40bed96515 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>
2026-07-06 15:40:51 +02:00

166 lines
10 KiB
TypeScript

// Área de PENDÊNCIAS DO DONO do workspace (só o dono vê). Engloba tudo:
// 1) Checklist da clínica — perguntas Sim/Não que o dono responde;
// 2) Pendências de configuração — quando a resposta não bate com o banco
// (ex.: marcou "mais de um dentista" mas só há 1 cadastrado);
// 3) Fila operacional — resumo dos pedidos/dúvidas aguardando a secretária humana.
// Consome /api/nw/agenda-config/clinica/:id/{checklist,pendencias}.
import React, { useState, useEffect, useCallback } from 'react';
import { X, ClipboardCheck, AlertTriangle, Check, Ban, Loader2, Save, CalendarClock, HelpCircle, ChevronRight } from 'lucide-react';
const API = (import.meta as any).env?.VITE_API_URL || '/api';
const BASE = `${API}/nw/agenda-config`;
async function req(path: string, opts: RequestInit = {}) {
const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN');
const res = await fetch(`${BASE}${path}`, { ...opts, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(opts.headers || {}) } });
const t = await res.text(); const d = t ? JSON.parse(t) : {};
if (!res.ok) throw new Error(d.error || `Erro ${res.status}`);
return d;
}
type Questao = { chave: string; pergunta: string; ajuda?: string; resposta: number | null };
type PendConfig = { chave: string; severidade: string; titulo: string; detalhe: string };
export const PendenciasDono: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string; onOpenFila?: () => void }> = ({ isOpen, onClose, clinicaId, onOpenFila }) => {
const [questoes, setQuestoes] = useState<Questao[]>([]);
const [config, setConfig] = useState<PendConfig[]>([]);
const [oper, setOper] = useState<{ horarios: number; duvidas: number; total: number }>({ horarios: 0, duvidas: 0, total: 0 });
const [contagens, setContagens] = useState<{ dentistas: number; pacientes: number }>({ dentistas: 0, pacientes: 0 });
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [erro, setErro] = useState<string | null>(null);
const [ok, setOk] = useState<string | null>(null);
const flash = (setter: (v: string | null) => void, m: string) => { setter(m); setTimeout(() => setter(null), 3000); };
const carregar = useCallback(async () => {
if (!clinicaId) return;
setLoading(true); setErro(null);
try {
const c = await req(`/clinica/${clinicaId}/checklist`);
setQuestoes(c.questoes || []);
const p = await req(`/clinica/${clinicaId}/pendencias`);
setConfig(p.config || []);
setOper(p.operacionais || { horarios: 0, duvidas: 0, total: 0 });
setContagens(p.contagens || { dentistas: 0, pacientes: 0 });
} catch (e: any) { setErro(e.message); } finally { setLoading(false); }
}, [clinicaId]);
useEffect(() => { if (isOpen) carregar(); }, [isOpen, carregar]);
const setResposta = (chave: string, resposta: number | null) =>
setQuestoes((qs) => qs.map((q) => q.chave === chave ? { ...q, resposta } : q));
const salvar = async () => {
setSaving(true); setErro(null);
try {
const respostas: Record<string, number | null> = {};
questoes.forEach((q) => { respostas[q.chave] = q.resposta; });
await req(`/clinica/${clinicaId}/checklist`, { method: 'PUT', body: JSON.stringify({ respostas }) });
flash(setOk, 'Checklist salvo!');
await carregar();
} catch (e: any) { setErro(e.message); } finally { setSaving(false); }
};
if (!isOpen) return null;
const sevColor = (s: string) => s === 'alta' ? 'border-red-200 bg-red-50' : 'border-amber-200 bg-amber-50';
const sevText = (s: string) => s === 'alta' ? 'text-red-700' : 'text-amber-700';
return (
<div className="fixed inset-0 bg-black/60 z-[70] flex items-center justify-center backdrop-blur-md p-0">
<div className="bg-white shadow-2xl w-full h-dvh md:w-[92vw] md:max-w-3xl md:h-[92vh] rounded-none md:rounded-[2rem] flex flex-col overflow-hidden border border-gray-100">
<div className="px-8 py-6 border-b border-gray-50 flex justify-between items-center bg-gradient-to-br from-gray-50 to-white">
<div className="flex items-center gap-3">
<div className="p-2.5 bg-indigo-600 rounded-2xl shadow-lg shadow-indigo-200"><ClipboardCheck size={22} className="text-white" /></div>
<div>
<h2 className="text-lg font-black text-gray-900 uppercase tracking-tighter">Pendências do workspace</h2>
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mt-0.5">Config da clínica + o que a Secretária precisa que você resolva</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-xl text-gray-400"><X size={22} /></button>
</div>
{(erro || ok) && <div className={`px-8 py-2 text-xs font-bold ${erro ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>{erro || ok}</div>}
<div className="flex-1 overflow-y-auto p-6 space-y-8">
{loading ? (
<div className="flex items-center justify-center py-16"><Loader2 size={22} className="animate-spin text-gray-400" /></div>
) : (
<>
{/* Fila operacional */}
<section>
<h3 className="text-xs font-black text-gray-500 uppercase tracking-widest mb-3">Aguardando a secretária humana</h3>
<button onClick={() => onOpenFila?.()} disabled={!onOpenFila}
className="w-full flex items-center justify-between gap-3 border-2 border-amber-100 bg-amber-50/50 rounded-2xl px-4 py-3.5 text-left hover:bg-amber-50 disabled:cursor-default disabled:hover:bg-amber-50/50">
<div className="flex items-center gap-3">
<CalendarClock size={20} className="text-amber-600" />
<div>
<div className="text-sm font-black text-gray-800">{oper.total} pedido(s) na fila</div>
<div className="text-[11px] text-gray-500 font-bold">{oper.horarios} horário(s) · {oper.duvidas} dúvida(s)</div>
</div>
</div>
{onOpenFila && <ChevronRight size={18} className="text-amber-400" />}
</button>
</section>
{/* Pendências de configuração */}
<section>
<h3 className="text-xs font-black text-gray-500 uppercase tracking-widest mb-3">Configuração a resolver</h3>
{config.length === 0 ? (
<p className="text-sm text-gray-400 flex items-center gap-2 py-2"><Check size={16} className="text-emerald-500" /> Nenhuma pendência de configuração. 🎉</p>
) : (
<div className="space-y-2">
{config.map((p, i) => (
<div key={i} className={`border-2 rounded-2xl p-4 ${sevColor(p.severidade)}`}>
<div className={`flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest ${sevText(p.severidade)}`}><AlertTriangle size={13} /> {p.severidade === 'alta' ? 'Importante' : 'Atenção'}</div>
<div className="text-sm font-black text-gray-800 mt-1">{p.titulo}</div>
<div className="text-[12px] text-gray-600 mt-0.5 leading-snug">{p.detalhe}</div>
</div>
))}
</div>
)}
<p className="text-[10px] text-gray-400 mt-2">No sistema: {contagens.dentistas} dentista(s) · {contagens.pacientes} paciente(s) cadastrado(s).</p>
</section>
{/* Checklist */}
<section>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-black text-gray-500 uppercase tracking-widest">Checklist da clínica</h3>
<button onClick={salvar} disabled={saving} className="bg-indigo-600 hover:bg-indigo-700 text-white text-[11px] font-black uppercase tracking-wide px-4 py-2 rounded-xl flex items-center gap-2 disabled:opacity-50">
{saving ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />} Salvar
</button>
</div>
<p className="text-[11px] text-gray-500 bg-gray-50 rounded-2xl p-3 mb-3 leading-relaxed">
Responda sobre a clínica. A Secretária usa isto para saber quando pode agendar sozinha quando algo não bate com o cadastro, ela evita agendar e encaminha ao humano, e aparece aqui em cima como pendência.
</p>
<div className="space-y-2">
{questoes.map((q) => (
<div key={q.chave} className="border border-gray-200 rounded-2xl p-4">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div className="flex-1 min-w-[200px]">
<div className="text-sm font-black text-gray-800 flex items-center gap-1.5"><HelpCircle size={14} className="text-gray-300" /> {q.pergunta}</div>
{q.ajuda && <div className="text-[11px] text-gray-400 mt-1 leading-snug">{q.ajuda}</div>}
</div>
<div className="flex gap-1.5">
<button onClick={() => setResposta(q.chave, q.resposta === 1 ? null : 1)}
className={`text-[11px] font-black uppercase px-3 py-1.5 rounded-xl flex items-center gap-1 transition-colors ${q.resposta === 1 ? 'bg-emerald-600 text-white' : 'bg-gray-100 text-gray-500 hover:bg-emerald-50'}`}>
<Check size={13} /> Sim
</button>
<button onClick={() => setResposta(q.chave, q.resposta === 0 ? null : 0)}
className={`text-[11px] font-black uppercase px-3 py-1.5 rounded-xl flex items-center gap-1 transition-colors ${q.resposta === 0 ? 'bg-red-600 text-white' : 'bg-gray-100 text-gray-500 hover:bg-red-50'}`}>
<Ban size={13} /> Não
</button>
</div>
</div>
</div>
))}
</div>
</section>
</>
)}
</div>
</div>
</div>
);
};