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:
@@ -2,7 +2,7 @@
|
||||
// horários dos dentistas. Consome /api/nw/agenda-config (o backend garante o
|
||||
// ownership: horário/feriado da clínica = dono; horário do dentista = ele ou dono).
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { X, Clock, CalendarOff, Plus, Trash2, Save, Loader2, User, Copy } from 'lucide-react';
|
||||
import { X, Clock, CalendarOff, Plus, Trash2, Save, Loader2, User, Copy, AlertTriangle } from 'lucide-react';
|
||||
|
||||
const API = (import.meta as any).env?.VITE_API_URL || '/api';
|
||||
const BASE = `${API}/nw/agenda-config`;
|
||||
@@ -88,8 +88,8 @@ const GradeSemanal: React.FC<{ faixas: Faixa[]; onChange: (f: Faixa[]) => void;
|
||||
);
|
||||
};
|
||||
|
||||
export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string; initialTab?: 'clinica' | 'dentistas'; soDentista?: boolean }> = ({ isOpen, onClose, clinicaId, initialTab, soDentista }) => {
|
||||
const [aba, setAba] = useState<'clinica' | 'dentistas'>(initialTab || 'clinica');
|
||||
export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string; initialTab?: 'clinica' | 'dentistas' | 'situacoes'; soDentista?: boolean }> = ({ isOpen, onClose, clinicaId, initialTab, soDentista }) => {
|
||||
const [aba, setAba] = useState<'clinica' | 'dentistas' | 'situacoes'>(initialTab || 'clinica');
|
||||
useEffect(() => { if (isOpen && initialTab) setAba(initialTab); }, [isOpen, initialTab]);
|
||||
const [erro, setErro] = useState<string | null>(null);
|
||||
const [ok, setOk] = useState<string | null>(null);
|
||||
@@ -107,6 +107,9 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
const [folgas, setFolgas] = useState<any[]>([]);
|
||||
const [novaFolga, setNovaFolga] = useState({ data_inicio: '', data_fim: '', motivo: '' });
|
||||
|
||||
// Situações (regras que a Secretária IA segue)
|
||||
const [situacoes, setSituacoes] = useState<string[]>([]);
|
||||
|
||||
const flash = (setter: (v: string | null) => void, msg: string) => { setter(msg); setTimeout(() => setter(null), 3000); };
|
||||
|
||||
const carregarClinica = useCallback(async () => {
|
||||
@@ -131,7 +134,13 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
} catch (e: any) { flash(setErro, e.message); }
|
||||
}, [clinicaId, soDentista]);
|
||||
|
||||
useEffect(() => { if (isOpen) { carregarClinica(); carregarDentistas(); } }, [isOpen, carregarClinica, carregarDentistas]);
|
||||
const carregarSituacoes = useCallback(async () => {
|
||||
if (!clinicaId) return;
|
||||
try { const s = await req(`/clinica/${clinicaId}/situacoes`); setSituacoes((s.situacoes || []).map((r: any) => r.texto)); }
|
||||
catch (e: any) { flash(setErro, e.message); }
|
||||
}, [clinicaId]);
|
||||
|
||||
useEffect(() => { if (isOpen) { carregarClinica(); carregarDentistas(); if (!soDentista) carregarSituacoes(); } }, [isOpen, carregarClinica, carregarDentistas, carregarSituacoes, soDentista]);
|
||||
const carregarFolgas = useCallback((id: string) => {
|
||||
req(`/dentista/${id}/folgas`).then((f) => setFolgas(f.folgas || [])).catch(() => setFolgas([]));
|
||||
}, []);
|
||||
@@ -171,6 +180,11 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
try { await req(`/clinica/${clinicaId}/feriados/${id}`, { method: 'DELETE' }); carregarClinica(); }
|
||||
catch (e: any) { flash(setErro, e.message); }
|
||||
};
|
||||
const salvarSituacoes = async () => {
|
||||
setSaving(true); setErro(null);
|
||||
try { await req(`/clinica/${clinicaId}/situacoes`, { method: 'PUT', body: JSON.stringify({ situacoes: situacoes.map((s) => s.trim()).filter(Boolean) }) }); flash(setOk, 'Situações salvas!'); }
|
||||
catch (e: any) { flash(setErro, e.message); } finally { setSaving(false); }
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -191,10 +205,10 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 px-8 pt-4 border-b border-gray-50">
|
||||
{(soDentista ? (['dentistas'] as const) : (['clinica', 'dentistas'] as const)).map((t) => (
|
||||
{(soDentista ? (['dentistas'] as const) : (['clinica', 'dentistas', 'situacoes'] as const)).map((t) => (
|
||||
<button key={t} onClick={() => setAba(t)}
|
||||
className={`px-4 py-2.5 text-xs font-black uppercase tracking-wide rounded-t-xl transition-colors ${aba === t ? 'bg-teal-600 text-white' : 'text-gray-400 hover:text-gray-600'}`}>
|
||||
{t === 'clinica' ? 'Clínica' : 'Dentistas'}
|
||||
{t === 'clinica' ? 'Clínica' : t === 'dentistas' ? 'Dentistas' : 'Situações'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -244,7 +258,7 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
) : aba === 'dentistas' ? (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -287,6 +301,30 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-black text-gray-800 uppercase tracking-tight flex items-center gap-2"><AlertTriangle size={16} className="text-teal-600" /> Situações e regras da Secretária IA</h3>
|
||||
<button onClick={salvarSituacoes} disabled={saving} className="bg-teal-600 hover:bg-teal-700 text-white text-xs font-black uppercase tracking-wide px-4 py-2 rounded-xl flex items-center gap-2 disabled:opacity-50">
|
||||
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />} Salvar
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500 leading-relaxed bg-amber-50 rounded-2xl p-4">
|
||||
Escreva regras que a Secretária IA deve seguir ao atender — uma por caixa. Ex.: <span className="italic">"Pelo plano X não temos profissional para canal em dente posterior; não agende direto — ofereça uma avaliação para o dentista examinar melhor o dente."</span> Quando uma situação exigir decisão humana, a IA encaminha o caso e pede para o paciente aguardar (aparece em Pedidos pendentes).
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{situacoes.map((s, i) => (
|
||||
<div key={i} className="flex items-start gap-2">
|
||||
<textarea value={s} rows={2} onChange={(e) => setSituacoes(situacoes.map((x, j) => j === i ? e.target.value : x))}
|
||||
placeholder="Descreva a situação e o que a IA deve fazer…"
|
||||
className="flex-1 bg-gray-50 border-2 border-transparent focus:border-teal-600 rounded-xl px-3 py-2 text-sm font-medium text-gray-800 outline-none resize-y" />
|
||||
<button onClick={() => setSituacoes(situacoes.filter((_, j) => j !== i))} className="mt-2 text-gray-300 hover:text-red-600"><Trash2 size={16} /></button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => setSituacoes([...situacoes, ''])} className="bg-gray-900 hover:bg-black text-white text-xs font-black uppercase px-4 py-2.5 rounded-xl flex items-center gap-1"><Plus size={14} /> Nova situação</button>
|
||||
</div>
|
||||
{situacoes.length === 0 && <p className="text-[11px] text-gray-400 italic">Nenhuma situação cadastrada. A IA seguirá apenas o roteiro padrão.</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user