diff --git a/frontend/components/HorariosConfig.tsx b/frontend/components/HorariosConfig.tsx index b608bb7..5b7104f 100644 --- a/frontend/components/HorariosConfig.tsx +++ b/frontend/components/HorariosConfig.tsx @@ -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, AlertTriangle } from 'lucide-react'; +import { X, Clock, CalendarOff, Plus, Trash2, Save, Loader2, User, Copy, AlertTriangle, Check } from 'lucide-react'; import { SegmentedTabs } from './SegmentedTabs.tsx'; const API = (import.meta as any).env?.VITE_API_URL || '/api'; @@ -89,8 +89,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' | 'situacoes'; soDentista?: boolean }> = ({ isOpen, onClose, clinicaId, initialTab, soDentista }) => { - const [aba, setAba] = useState<'clinica' | 'dentistas' | 'situacoes'>(initialTab || 'clinica'); +export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string; initialTab?: 'clinica' | 'dentistas' | 'situacoes' | 'procedimentos'; soDentista?: boolean }> = ({ isOpen, onClose, clinicaId, initialTab, soDentista }) => { + const [aba, setAba] = useState<'clinica' | 'dentistas' | 'situacoes' | 'procedimentos'>(initialTab || 'clinica'); useEffect(() => { if (isOpen && initialTab) setAba(initialTab); }, [isOpen, initialTab]); const [erro, setErro] = useState(null); const [ok, setOk] = useState(null); @@ -110,6 +110,7 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl // Situações (regras que a Secretária IA segue) const [situacoes, setSituacoes] = useState([]); + const [procedimentos, setProcedimentos] = useState>([]); const flash = (setter: (v: string | null) => void, msg: string) => { setter(msg); setTimeout(() => setter(null), 3000); }; @@ -138,10 +139,20 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl const carregarSituacoes = useCallback(async () => { if (!clinicaId) return; try { const s = await req(`/clinica/${clinicaId}/situacoes`); setSituacoes((s.situacoes || []).map((r: any) => r.texto)); } + finally { /* noop */ } + }, [clinicaId]); + const carregarProcedimentos = useCallback(async () => { + if (!clinicaId) return; + try { const d = await req(`/clinica/${clinicaId}/procedimentos-atende`); setProcedimentos((d.procedimentos || []).map((r: any) => ({ nome: r.nome, atende: r.atende !== 0 && r.atende !== false, motivo: r.motivo || '', como_falar: r.como_falar || '' }))); } catch (e: any) { flash(setErro, e.message); } }, [clinicaId]); - useEffect(() => { if (isOpen) { carregarClinica(); carregarDentistas(); if (!soDentista) carregarSituacoes(); } }, [isOpen, carregarClinica, carregarDentistas, carregarSituacoes, soDentista]); + useEffect(() => { if (isOpen) { carregarClinica(); carregarDentistas(); if (!soDentista) { carregarSituacoes(); carregarProcedimentos(); } } }, [isOpen, carregarClinica, carregarDentistas, carregarSituacoes, carregarProcedimentos, soDentista]); + const salvarProcedimentos = async () => { + setSaving(true); + try { await req(`/clinica/${clinicaId}/procedimentos-atende`, { method: 'PUT', body: JSON.stringify({ procedimentos: procedimentos.map((p) => ({ nome: p.nome.trim(), atende: p.atende, motivo: p.motivo, como_falar: p.como_falar })).filter((p) => p.nome) }) }); flash(setOk, 'Procedimentos salvos!'); } + catch (e: any) { flash(setErro, e.message); } finally { setSaving(false); } + }; const carregarFolgas = useCallback((id: string) => { req(`/dentista/${id}/folgas`).then((f) => setFolgas(f.folgas || [])).catch(() => setFolgas([])); }, []); @@ -207,7 +218,7 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl {/* Tabs */}
({ id: t, label: t === 'clinica' ? 'Clínica' : t === 'dentistas' ? 'Dentistas' : 'Situações' }))} + tabs={(soDentista ? (['dentistas'] as const) : (['clinica', 'dentistas', 'situacoes', 'procedimentos'] as const)).map((t) => ({ id: t, label: t === 'clinica' ? 'Clínica' : t === 'dentistas' ? 'Dentistas' : t === 'situacoes' ? 'Situações' : 'Procedimentos' }))} value={aba} onChange={setAba} />
@@ -299,7 +310,7 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl )} - ) : ( + ) : aba === 'situacoes' ? (

Situações e regras da Secretária IA

@@ -323,6 +334,47 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
{situacoes.length === 0 &&

Nenhuma situação cadastrada. A IA seguirá apenas o roteiro padrão.

}
+ ) : ( +
+
+

Procedimentos que a clínica atende

+ +
+

+ Marque o que a clínica atende. Ao desmarcar, a Secretária IA não vai agendar aquele procedimento — preencha o motivo e o como falar (o que ela deve responder ao paciente). +

+
+ {procedimentos.map((p, i) => ( +
+
+ + setProcedimentos(procedimentos.map((x, j) => j === i ? { ...x, nome: e.target.value } : x))} + placeholder="Nome do procedimento" + className="flex-1 bg-gray-50 border-2 border-transparent focus:border-teal-600 rounded-xl px-3 py-2 text-sm font-bold text-gray-800 outline-none" /> + {p.atende ? 'Atende' : 'Não atende'} + +
+ {!p.atende && ( +
+ setProcedimentos(procedimentos.map((x, j) => j === i ? { ...x, motivo: e.target.value } : x))} + placeholder="Motivo de não atender (uso interno)" + className="w-full bg-gray-50 border-2 border-transparent focus:border-teal-600 rounded-xl px-3 py-2 text-xs font-medium text-gray-700 outline-none" /> +