From c8443a146a259b00b5e49a85893d4cc69643b686 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Wed, 8 Jul 2026 00:34:04 +0200 Subject: [PATCH] =?UTF-8?q?fix(nw):=20handoff=20=C3=A9=20a=C3=A7=C3=A3o=20?= =?UTF-8?q?de=20inbox=20(n=C3=A3o=20Secret=C3=A1ria)=20+=20bot=C3=A3o=20do?= =?UTF-8?q?=20dono=20p/=20hor=C3=A1rio=20da=20cl=C3=ADnica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) 403 ao "desligar a IA no chat": guardAreaAccess tratava TODO /sec/* como config da Secretária (exige can_secretaria). Mas /sec/handoff é assumir/soltar o CHAT = INBOX. Reclassificado p/ can_inbox — a recep (can_inbox, sem can_secretaria) volta a assumir o chat. /sec/agent|nodes|slots e /secretaria/* seguem exigindo can_secretaria. (session-power global segue can_secretaria — o dono delega se quiser.) 2) Duplicação de horário (parte 2): o DONO agora tem, no modal, o card + botão p/ configurar o "Horário da clínica" (clinicas_horarios) em Horários de Funcionamento (aba clínica). Completude do dono passa a exigir esse horário. Backend: comunicacaoCompleto(p, {ehDentista,jornadaOk,ehDono,horarioClinicaOk}); GET devolve horario_clinica_configurado. Verificado: dono da Consultt sem horário → gate exige. Co-Authored-By: Claude Opus 4.8 --- backend/newwhats/proxy.js | 7 ++- backend/server.js | 31 ++++++++---- .../components/ComunicacaoInternaModal.tsx | 47 ++++++++++++++----- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/backend/newwhats/proxy.js b/backend/newwhats/proxy.js index 2c994dd..9a22217 100644 --- a/backend/newwhats/proxy.js +++ b/backend/newwhats/proxy.js @@ -152,8 +152,11 @@ export async function operatorSignature(pool, userId, clinicaId) { // Gate de acesso a ÁREAS (Inbox / Secretária). Dono passa sempre; os demais só com // delegação (can_inbox / can_secretaria) do dono em alguma sessão do workspace. async function guardAreaAccess(pool, tail, userId, clinicaId) { - const isSecretaria = /^\/(secretaria|sec)(\/|$|\?)/.test(tail); - const isInbox = /^\/(inbox|conversations)(\/|$|\?)/.test(tail); + // /sec/handoff = assumir/soltar o CHAT (ação de INBOX), não config da Secretária. + // O resto de /sec/* e todo /secretaria/* é config (agents/nodes/slots/power) = Secretária. + const isHandoff = /^\/sec\/handoff(\/|$|\?)/.test(tail); + const isSecretaria = !isHandoff && /^\/(secretaria|sec)(\/|$|\?)/.test(tail); + const isInbox = isHandoff || /^\/(inbox|conversations)(\/|$|\?)/.test(tail); if (!isSecretaria && !isInbox) return { ok: true }; if (!pool || !clinicaId) return { ok: true }; // sem workspace resolvido, não barra aqui const ownerId = await resolveOwnerId(pool, clinicaId); diff --git a/backend/server.js b/backend/server.js index d6c7c94..f4505ea 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7556,17 +7556,19 @@ async function resolveOrigem(req, cep, clinicaId) { // ─── Comunicação interna (perfil de contato de não-pacientes) ──────────────────── // Regra de completude que destrava o gate: endereço + ≥1 número (com descrição e ≥1 // janela de horário) + disponibilidade de agenda decidida. -function comunicacaoCompleto(p, ehDentista, jornadaOk) { +function comunicacaoCompleto(p, opts) { + const { ehDentista, jornadaOk, ehDono, horarioClinicaOk } = opts || {}; const e = p.endereco || {}; const nums = Array.isArray(p.numeros) ? p.numeros : []; const enderecoOk = !!(e.cidade && e.estado && (e.cep || e.logradouro)); const numerosOk = nums.length >= 1 && nums.every((n) => n && String(n.numero || '').replace(/\D/g, '').length >= 10 && String(n.descricao || '').trim() && Array.isArray(n.janelas) && n.janelas.length >= 1); - // A agenda saiu do modal (fonte única = dentistas_horarios). Do DENTISTA exige-se a - // jornada REAL configurada em "Horários de Funcionamento". + // Agenda = fonte única. DENTISTA exige jornada real (dentistas_horarios); DONO exige o + // horário de funcionamento da CLÍNICA (clinicas_horarios) — ambos em "Horários de Funcionamento". const jornadaExigidaOk = !ehDentista || !!jornadaOk; - return enderecoOk && numerosOk && jornadaExigidaOk; + const clinicaExigidaOk = !ehDono || !!horarioClinicaOk; + return enderecoOk && numerosOk && jornadaExigidaOk && clinicaExigidaOk; } async function papelNaClinica(usuarioId, clinicaId) { const { rows } = await pool.query('SELECT role FROM vinculos WHERE usuario_id=$1 AND clinica_id=$2', [usuarioId, clinicaId]); @@ -7580,6 +7582,12 @@ async function jornadaConfigurada(usuarioId, clinicaId) { const { rows } = await pool.query('SELECT 1 FROM dentistas_horarios WHERE dentista_id=$1 AND COALESCE(ativo,1) <> 0 LIMIT 1', [dentId]); return rows.length > 0; } +// Horário de FUNCIONAMENTO da clínica já configurado? +async function clinicaHorariosConfigurados(clinicaId) { + const { rows } = await pool.query('SELECT 1 FROM clinicas_horarios WHERE clinica_id=$1 AND COALESCE(ativo,1) <> 0 LIMIT 1', [clinicaId]); + return rows.length > 0; +} +const EH_DONO = (role) => ['donoclinica', 'donoconsultorio'].includes(role); // Clínica do usuário: param explícito → primeiro vínculo. async function resolveClinicaUsuario(userId, clinicaIdParam) { if (clinicaIdParam) return String(clinicaIdParam); @@ -7635,10 +7643,12 @@ app.get('/api/comunicacao/:usuarioId', authGuard, async (req, res) => { } const role = await papelNaClinica(alvo, clinicaId); const ehDentista = role === 'dentista'; + const ehDono = EH_DONO(role); const jornadaOk = ehDentista ? await jornadaConfigurada(alvo, clinicaId) : true; - // `completo` recomputado AO VIVO (a jornada pode ter sido configurada fora do modal). - const completoLive = comunicacaoCompleto({ endereco: p.endereco, numeros: p.numeros }, ehDentista, jornadaOk); - res.json({ usuario_id: alvo, clinica_id: clinicaId, ...p, completo: completoLive, endereco_da_clinica: enderecoDaClinica, role, jornada_configurada: jornadaOk }); + const horarioClinicaOk = ehDono ? await clinicaHorariosConfigurados(clinicaId) : true; + // `completo` recomputado AO VIVO (jornada/horário podem ter sido configurados fora do modal). + const completoLive = comunicacaoCompleto({ endereco: p.endereco, numeros: p.numeros }, { ehDentista, jornadaOk, ehDono, horarioClinicaOk }); + res.json({ usuario_id: alvo, clinica_id: clinicaId, ...p, completo: completoLive, endereco_da_clinica: enderecoDaClinica, role, jornada_configurada: jornadaOk, horario_clinica_configurado: horarioClinicaOk }); } catch (e) { res.status(500).json({ error: e.message }); } }); @@ -7652,9 +7662,12 @@ app.put('/api/comunicacao/:usuarioId', authGuard, async (req, res) => { const endereco = b.endereco && typeof b.endereco === 'object' ? b.endereco : {}; const numeros = Array.isArray(b.numeros) ? b.numeros : []; const agenda = b.agenda && typeof b.agenda === 'object' ? b.agenda : {}; - const ehDentista = (await papelNaClinica(alvo, clinicaId)) === 'dentista'; + const role = await papelNaClinica(alvo, clinicaId); + const ehDentista = role === 'dentista'; + const ehDono = EH_DONO(role); const jornadaOk = ehDentista ? await jornadaConfigurada(alvo, clinicaId) : true; - const completo = comunicacaoCompleto({ endereco, numeros }, ehDentista, jornadaOk); + const horarioClinicaOk = ehDono ? await clinicaHorariosConfigurados(clinicaId) : true; + const completo = comunicacaoCompleto({ endereco, numeros }, { ehDentista, jornadaOk, ehDono, horarioClinicaOk }); await pool.query( `INSERT INTO pessoa_comunicacao (usuario_id, clinica_id, endereco, numeros, agenda, completo, updated_at) VALUES ($1,$2,$3,$4,$5,$6,NOW()) diff --git a/frontend/components/ComunicacaoInternaModal.tsx b/frontend/components/ComunicacaoInternaModal.tsx index 4264cf5..04eacb4 100644 --- a/frontend/components/ComunicacaoInternaModal.tsx +++ b/frontend/components/ComunicacaoInternaModal.tsx @@ -78,9 +78,11 @@ export const ComunicacaoInternaModal: React.FC<{ const [endereco, setEndereco] = useState({}); const [numeros, setNumeros] = useState([]); const [enderecoDaClinica, setEnderecoDaClinica] = useState(false); - const [isDentista, setIsDentista] = useState(false); // só dentista precisa de jornada - const [jornadaOk, setJornadaOk] = useState(false); // jornada real (dentistas_horarios) já configurada - const [showHorarios, setShowHorarios] = useState(false); // abre "Horários de Funcionamento" por cima + const [isDentista, setIsDentista] = useState(false); // dentista precisa da jornada + const [isDono, setIsDono] = useState(false); // dono precisa do horário da clínica + const [jornadaOk, setJornadaOk] = useState(false); // jornada real (dentistas_horarios) configurada + const [horarioClinicaOk, setHorarioClinicaOk] = useState(false); // clinicas_horarios configurado + const [horariosTab, setHorariosTab] = useState<'dentistas' | 'clinica' | null>(null); // abre "Horários de Funcionamento" const load = useCallback(async () => { setLoading(true); @@ -92,7 +94,9 @@ export const ComunicacaoInternaModal: React.FC<{ setNumeros(Array.isArray(d.numeros) ? d.numeros : []); setEnderecoDaClinica(!!d.endereco_da_clinica); setIsDentista((d.role || '') === 'dentista'); + setIsDono(['donoclinica', 'donoconsultorio'].includes(d.role || '')); setJornadaOk(!!d.jornada_configurada); + setHorarioClinicaOk(!!d.horario_clinica_configurado); } catch { /* mantém vazio */ } finally { setLoading(false); } }, [usuarioId, clinicaId]); useEffect(() => { load(); }, [load]); @@ -125,8 +129,9 @@ export const ComunicacaoInternaModal: React.FC<{ // Completude (espelha o backend) — orienta o preenchimento e destrava o gate. const enderecoOk = !!(endereco.cidade && endereco.estado && (endereco.cep || endereco.logradouro)); const numerosOk = numeros.length >= 1 && numeros.every(n => (n.numero || '').replace(/\D/g, '').length >= 10 && (n.descricao || '').trim() && (n.janelas?.length ?? 0) >= 1); - const jornadaFeita = !isDentista || jornadaOk; // dentista precisa da jornada real configurada - const completo = enderecoOk && numerosOk && jornadaFeita; + const jornadaFeita = !isDentista || jornadaOk; // dentista: jornada real configurada + const clinicaFeita = !isDono || horarioClinicaOk; // dono: horário da clínica configurado + const completo = enderecoOk && numerosOk && jornadaFeita && clinicaFeita; const salvar = async () => { setSaving(true); @@ -265,12 +270,31 @@ export const ComunicacaoInternaModal: React.FC<{ {jornadaOk ? (
Jornada configurada - +
) : (

Você ainda não configurou sua agenda de atendimento. Sem ela, a Secretária não consegue marcar consultas com você.

- + +
+ )} + + )} + + {/* ── Horário de funcionamento da clínica — só para o DONO ── */} + {isDono && ( +
+

Horário da clínica

+

É o horário de funcionamento que a Secretária usa para atender e marcar consultas. Fica em "Horários de Funcionamento".

+ {horarioClinicaOk ? ( +
+ Horário da clínica configurado + +
+ ) : ( +
+

A clínica ainda não tem horário de funcionamento. Sem ele, a Secretária não consegue atender nem marcar consultas.

+
)}
@@ -285,6 +309,7 @@ export const ComunicacaoInternaModal: React.FC<{ {enderecoOk ? '✓' : '○'} Endereço {numerosOk ? '✓' : '○'} Números {isDentista && {jornadaFeita ? '✓' : '○'} Agenda} + {isDono && {clinicaFeita ? '✓' : '○'} Clínica} {!forced && onSnooze && (