From e9f8a24a938be9c7415837bb4a56d7a8564b680c Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Wed, 8 Jul 2026 00:14:32 +0200 Subject: [PATCH] =?UTF-8?q?fix(comunicacao-interna):=20remove=20duplica?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20agenda=20=E2=80=94=20fonte=20=C3=BAnica=20?= =?UTF-8?q?(jornada=20real)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A seção "Agenda/Horários de atendimento" do modal era uma 3ª fonte de horário que a Secretária NÃO lia (ela agenda por clinicas_horarios + dentistas_horarios). Removida: - Modal cuida só de contato (endereço + números + janelas de envio). Para dentista, mostra o STATUS da jornada real e um botão que abre "Horários de Funcionamento" (HorariosConfig, aba dentistas) por cima; ao fechar, recarrega. - Completude do DENTISTA passa a exigir a jornada REAL configurada (dentistas_horarios), não mais a agenda do modal. GET recomputa `completo` ao vivo + devolve jornada_configurada. - Backend: comunicacaoCompleto(p, ehDentista, jornadaOk) + helper jornadaConfigurada. Verificado: dentista com jornada → completo; query bate com dentistas_horarios real. Co-Authored-By: Claude Opus 4.8 --- backend/server.js | 27 ++++++--- .../components/ComunicacaoInternaModal.tsx | 58 +++++++++++-------- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/backend/server.js b/backend/server.js index c1f62f1..d6c7c94 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7556,22 +7556,30 @@ 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) { +function comunicacaoCompleto(p, ehDentista, jornadaOk) { const e = p.endereco || {}; const nums = Array.isArray(p.numeros) ? p.numeros : []; - const ag = p.agenda || {}; 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); - // Agenda só é exigida de DENTISTA (quem atende); demais papéis não precisam. - const agendaOk = !ehDentista || ag.aberta === false || (ag.aberta === true && Array.isArray(ag.horarios) && ag.horarios.length >= 1); - return enderecoOk && numerosOk && agendaOk; + // A agenda saiu do modal (fonte única = dentistas_horarios). Do DENTISTA exige-se a + // jornada REAL configurada em "Horários de Funcionamento". + const jornadaExigidaOk = !ehDentista || !!jornadaOk; + return enderecoOk && numerosOk && jornadaExigidaOk; } async function papelNaClinica(usuarioId, clinicaId) { const { rows } = await pool.query('SELECT role FROM vinculos WHERE usuario_id=$1 AND clinica_id=$2', [usuarioId, clinicaId]); return rows[0]?.role || ''; } +// Jornada de atendimento do dentista já configurada? (fonte real usada pela Secretária) +async function jornadaConfigurada(usuarioId, clinicaId) { + const { rows: dr } = await pool.query('SELECT id FROM dentistas WHERE usuario_id=$1 AND clinica_id=$2 LIMIT 1', [usuarioId, clinicaId]); + const dentId = dr[0]?.id; + if (!dentId) return false; + 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; +} // Clínica do usuário: param explícito → primeiro vínculo. async function resolveClinicaUsuario(userId, clinicaIdParam) { if (clinicaIdParam) return String(clinicaIdParam); @@ -7626,7 +7634,11 @@ app.get('/api/comunicacao/:usuarioId', authGuard, async (req, res) => { if (cr[0] && (cr[0].cidade || cr[0].logradouro)) { p.endereco = { ...cr[0], ...(p.endereco || {}) }; enderecoDaClinica = true; } } const role = await papelNaClinica(alvo, clinicaId); - res.json({ usuario_id: alvo, clinica_id: clinicaId, ...p, endereco_da_clinica: enderecoDaClinica, role }); + const ehDentista = role === 'dentista'; + 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 }); } catch (e) { res.status(500).json({ error: e.message }); } }); @@ -7641,7 +7653,8 @@ app.put('/api/comunicacao/:usuarioId', authGuard, async (req, res) => { 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 completo = comunicacaoCompleto({ endereco, numeros, agenda }, ehDentista); + const jornadaOk = ehDentista ? await jornadaConfigurada(alvo, clinicaId) : true; + const completo = comunicacaoCompleto({ endereco, numeros }, ehDentista, jornadaOk); 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 fe48e66..4264cf5 100644 --- a/frontend/components/ComunicacaoInternaModal.tsx +++ b/frontend/components/ComunicacaoInternaModal.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { X, Plus, Trash2, MapPin, Phone, Clock, CalendarClock, Save, Loader2, MessageSquare, AlertTriangle, CheckCircle2 } from 'lucide-react'; import { useToast } from '../contexts/ToastContext.tsx'; import { buscarCep } from '../services/viacep.ts'; +import { HorariosConfig } from './HorariosConfig.tsx'; // Comunicação interna: perfil de contato de não-pacientes (dentistas/funcionários). // Números de WhatsApp múltiplos (cada um com descrição, situações e janelas de horário @@ -76,9 +77,10 @@ export const ComunicacaoInternaModal: React.FC<{ const [saving, setSaving] = useState(false); const [endereco, setEndereco] = useState({}); const [numeros, setNumeros] = useState([]); - const [agenda, setAgenda] = useState({}); const [enderecoDaClinica, setEnderecoDaClinica] = useState(false); - const [isDentista, setIsDentista] = useState(false); // só dentista precisa de agenda + 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 load = useCallback(async () => { setLoading(true); @@ -88,9 +90,9 @@ export const ComunicacaoInternaModal: React.FC<{ const d = await r.json(); setEndereco(d.endereco || {}); setNumeros(Array.isArray(d.numeros) ? d.numeros : []); - setAgenda(d.agenda || {}); setEnderecoDaClinica(!!d.endereco_da_clinica); setIsDentista((d.role || '') === 'dentista'); + setJornadaOk(!!d.jornada_configurada); } catch { /* mantém vazio */ } finally { setLoading(false); } }, [usuarioId, clinicaId]); useEffect(() => { load(); }, [load]); @@ -123,13 +125,13 @@ 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 agendaOk = !isDentista || agenda.aberta === false || (agenda.aberta === true && (agenda.horarios?.length ?? 0) >= 1); - const completo = enderecoOk && numerosOk && agendaOk; + const jornadaFeita = !isDentista || jornadaOk; // dentista precisa da jornada real configurada + const completo = enderecoOk && numerosOk && jornadaFeita; const salvar = async () => { setSaving(true); try { - const r = await api(`/api/comunicacao/${usuarioId}`, { method: 'PUT', body: JSON.stringify({ clinicaId, endereco, numeros, agenda }) }); + const r = await api(`/api/comunicacao/${usuarioId}`, { method: 'PUT', body: JSON.stringify({ clinicaId, endereco, numeros }) }); const d = await r.json(); if (!r.ok) throw new Error(d.error || 'Erro ao salvar.'); toast.success('DADOS SALVOS!'); @@ -141,6 +143,7 @@ export const ComunicacaoInternaModal: React.FC<{ }; return ( + <>
{/* Header */}
@@ -254,25 +257,23 @@ export const ComunicacaoInternaModal: React.FC<{
- {/* ── Agenda (abrir/fechar) — só para dentista (quem atende) ── */} + {/* ── Agenda de atendimento — fonte única: "Horários de Funcionamento" ── */} {isDentista && ( -
-

Agenda

-

Sua agenda está aberta para a Secretária marcar atendimentos? Se sim, informe os horários em que ela pode agendar.

-
- {[{ v: true, l: 'Agenda aberta' }, { v: false, l: 'Agenda fechada' }].map(o => ( - - ))} -
- {agenda.aberta === true && ( -
- - setAgenda(a => ({ ...a, horarios: h }))} addLabel="Adicionar horário de atendimento" /> -
- )} -
+
+

Agenda de atendimento

+

São os horários em que a Secretária pode marcar consultas para você. Ficam em "Horários de Funcionamento" (a mesma jornada que a agenda usa).

+ {jornadaOk ? ( +
+ Jornada configurada + +
+ ) : ( +
+

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

+ +
+ )} +
)}
@@ -283,7 +284,7 @@ export const ComunicacaoInternaModal: React.FC<{
{enderecoOk ? '✓' : '○'} Endereço {numerosOk ? '✓' : '○'} Números - {isDentista && {agendaOk ? '✓' : '○'} Agenda} + {isDentista && {jornadaFeita ? '✓' : '○'} Agenda}
{!forced && onSnooze && (