Files
scoreodonto.com/frontend/components/PedidosPendentes.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

142 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Fila da secretária humana: pedidos de horário da ZONA CINZA que a Secretária IA
// registrou ("vou confirmar e retorno"). A humana confirma (vira agendamento) ou
// recusa. Consome /api/nw/agenda-config/clinica/:id/pedidos*.
import React, { useState, useEffect, useCallback } from 'react';
import { X, CalendarClock, Check, Ban, Loader2, Phone, User, HelpCircle } 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;
}
const dm = (s: string) => s ? s.split('-').reverse().join('/') : s;
export const PedidosPendentes: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string; onChange?: () => void }> = ({ isOpen, onClose, clinicaId, onChange }) => {
const [pedidos, setPedidos] = useState<any[]>([]);
const [dentistas, setDentistas] = useState<any[]>([]);
const [sel, setSel] = useState<Record<string, string>>({}); // pedido_id → dentista_id escolhido
const [busy, setBusy] = useState<string | null>(null);
const [erro, setErro] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const carregar = useCallback(async () => {
if (!clinicaId) return;
setLoading(true); setErro(null);
try {
const p = await req(`/clinica/${clinicaId}/pedidos?status=pendente`);
setPedidos(p.pedidos || []);
const d = await req(`/clinica/${clinicaId}/dentistas`);
setDentistas(d.dentistas || []);
} catch (e: any) { setErro(e.message); } finally { setLoading(false); }
}, [clinicaId]);
useEffect(() => { if (isOpen) carregar(); }, [isOpen, carregar]);
const confirmar = async (p: any) => {
const dentistaId = p.dentista_id || sel[p.id];
if (!dentistaId) { setErro('Escolha o dentista para confirmar.'); return; }
setBusy(p.id); setErro(null);
try { await req(`/clinica/${clinicaId}/pedidos/${p.id}/confirmar`, { method: 'POST', body: JSON.stringify({ dentista_id: dentistaId }) }); await carregar(); onChange?.(); }
catch (e: any) { setErro(e.message); } finally { setBusy(null); }
};
const recusar = async (p: any) => {
setBusy(p.id); setErro(null);
try { await req(`/clinica/${clinicaId}/pedidos/${p.id}/recusar`, { method: 'POST' }); await carregar(); onChange?.(); }
catch (e: any) { setErro(e.message); } finally { setBusy(null); }
};
const resolver = async (p: any) => {
setBusy(p.id); setErro(null);
try { await req(`/clinica/${clinicaId}/pedidos/${p.id}/resolver`, { method: 'POST' }); await carregar(); onChange?.(); }
catch (e: any) { setErro(e.message); } finally { setBusy(null); }
};
if (!isOpen) return null;
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-[90vh] 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-amber-500 rounded-2xl shadow-lg shadow-amber-200"><CalendarClock size={22} className="text-white" /></div>
<div>
<h2 className="text-lg font-black text-gray-900 uppercase tracking-tighter">Pedidos aguardando confirmação</h2>
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mt-0.5">Horários fora do padrão que a Secretária IA encaminhou</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-xl text-gray-400"><X size={22} /></button>
</div>
{erro && <div className="px-8 py-2 text-xs font-bold bg-red-50 text-red-600">{erro}</div>}
<div className="flex-1 overflow-y-auto p-6 space-y-3">
{loading ? (
<div className="flex items-center justify-center py-16"><Loader2 size={22} className="animate-spin text-gray-400" /></div>
) : pedidos.length === 0 ? (
<p className="text-center text-sm text-gray-400 py-16">Nenhum pedido pendente. 🎉</p>
) : pedidos.map((p) => p.tipo === 'duvida' ? (
<div key={p.id} className="border-2 border-indigo-100 rounded-2xl p-4 bg-indigo-50/40">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div className="space-y-1.5 max-w-[75%]">
<div className="flex items-center gap-1.5 text-[10px] font-black text-indigo-600 uppercase tracking-widest"><HelpCircle size={13} /> Dúvida p/ decidir</div>
<div className="text-sm font-bold text-gray-800 leading-snug whitespace-pre-wrap">{p.resumo}</div>
<div className="flex items-center gap-2 text-xs text-gray-600 font-bold"><User size={13} className="text-gray-400" /> {p.paciente_nome || 'Cliente'}
{p.paciente_telefone && <span className="flex items-center gap-1 text-gray-400"><Phone size={12} /> {p.paciente_telefone}</span>}</div>
</div>
<div className="flex gap-2">
<button onClick={() => resolver(p)} disabled={busy === p.id}
className="bg-indigo-600 hover:bg-indigo-700 text-white text-[11px] font-black uppercase px-3 py-1.5 rounded-xl flex items-center gap-1 disabled:opacity-50">
{busy === p.id ? <Loader2 size={13} className="animate-spin" /> : <Check size={13} />} Resolvida
</button>
<button onClick={() => recusar(p)} disabled={busy === p.id}
className="bg-gray-100 hover:bg-red-50 text-gray-500 hover:text-red-600 text-[11px] font-black uppercase px-3 py-1.5 rounded-xl flex items-center gap-1">
<Ban size={13} /> Descartar
</button>
</div>
</div>
</div>
) : (
<div key={p.id} className="border border-gray-200 rounded-2xl p-4 bg-white">
<div className="flex items-start justify-between gap-3 flex-wrap">
<div className="space-y-1">
<div className="text-sm font-black text-gray-800">
{dm(p.data)} às {p.hora_inicio}{p.hora_fim ? `${p.hora_fim}` : ''}
</div>
<div className="flex items-center gap-2 text-xs text-gray-600 font-bold"><User size={13} className="text-gray-400" /> {p.paciente_nome || 'Cliente'}
{p.paciente_telefone && <span className="flex items-center gap-1 text-gray-400"><Phone size={12} /> {p.paciente_telefone}</span>}</div>
{p.procedimento && <div className="text-[11px] text-gray-500">{p.procedimento}</div>}
</div>
<div className="flex flex-col items-end gap-2">
{p.dentista_id ? (
<span className="text-[11px] font-bold text-teal-600">{p.dentista_nome}</span>
) : (
<select value={sel[p.id] || ''} onChange={(e) => setSel({ ...sel, [p.id]: e.target.value })}
className="bg-gray-50 border-2 border-transparent focus:border-teal-600 rounded-xl px-2 py-1.5 text-xs font-bold text-gray-800 outline-none">
<option value="">Escolher dentista</option>
{dentistas.map((d) => <option key={d.id} value={d.id}>{d.nome}</option>)}
</select>
)}
<div className="flex gap-2">
<button onClick={() => confirmar(p)} disabled={busy === p.id}
className="bg-emerald-600 hover:bg-emerald-700 text-white text-[11px] font-black uppercase px-3 py-1.5 rounded-xl flex items-center gap-1 disabled:opacity-50">
{busy === p.id ? <Loader2 size={13} className="animate-spin" /> : <Check size={13} />} Confirmar
</button>
<button onClick={() => recusar(p)} disabled={busy === p.id}
className="bg-gray-100 hover:bg-red-50 text-gray-500 hover:text-red-600 text-[11px] font-black uppercase px-3 py-1.5 rounded-xl flex items-center gap-1">
<Ban size={13} /> Recusar
</button>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};