From 17f83e5c86b3ad13c7ad6a79d58b1326e4f5e5ce Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Sun, 14 Jun 2026 03:23:32 +0200 Subject: [PATCH] =?UTF-8?q?feat(marketplace):=20exigir=20endere=C3=A7o/CEP?= =?UTF-8?q?=20da=20origem=20antes=20da=20busca=20por=20proximidade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Profissionais e Salas: aba de busca bloqueia se a origem não tiver cep+cidade+estado - clínica/consultório (donoclinica/donoconsultorio/admin/funcionário): checa endereço da clínica ativa → CTA p/ Configurações - dentista/biomédico/protético: checa o próprio perfil → CTA p/ completar perfil - fail-safe: em erro de checagem não bloqueia Co-Authored-By: Claude Opus 4.8 --- .../views/plugins/ProfissionaisPlugin.tsx | 47 +++++++++++++++++- frontend/views/plugins/SalasPlugin.tsx | 49 ++++++++++++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/frontend/views/plugins/ProfissionaisPlugin.tsx b/frontend/views/plugins/ProfissionaisPlugin.tsx index 53c108e..462faaf 100644 --- a/frontend/views/plugins/ProfissionaisPlugin.tsx +++ b/frontend/views/plugins/ProfissionaisPlugin.tsx @@ -123,6 +123,26 @@ const PropostaModal: React.FC<{ prof: Profissional; onClose: () => void; onSent: // ─── Main view ─────────────────────────────────────────────────────────────── type Tab = 'buscar' | 'perfil' | 'propostas'; +// Bloqueio quando falta endereço/CEP da origem (clínica ou perfil) — a busca é por proximidade. +const GateEndereco: React.FC<{ isClinic: boolean; onCompletarPerfil: () => void }> = ({ isClinic, onCompletarPerfil }) => ( +
+
+ +
+

Complete seu endereço

+

+ A busca de profissionais é por proximidade. Preencha {isClinic + ? 'a localização e o CEP da sua clínica/consultório (em Configurações)' + : 'a localização e o CEP do seu perfil'} para continuar. +

+ +
+); + export const ProfissionaisPlugin: React.FC = () => { const toast = useToast(); const role = HybridBackend.getCurrentRole(); @@ -146,6 +166,26 @@ export const ProfissionaisPlugin: React.FC = () => { // propostas const [propostas, setPropostas] = useState<{ enviadas: Proposta[]; recebidas: Proposta[] }>({ enviadas: [], recebidas: [] }); const [proposingTo, setProposingTo] = useState(null); + // null = checando; true/false = endereço da origem (clínica ou perfil) preenchido? + const [enderecoOk, setEnderecoOk] = useState(null); + + const checkEndereco = useCallback(async () => { + try { + if (isProfissional) { + const res = await apiFetch('/api/profissionais/perfil'); + const p = res.ok ? await res.json() : {}; + setEnderecoOk(!!(p?.cep && p?.cidade && p?.estado)); + } else if (ws?.id) { + const r = await HybridBackend.getClinicaProfile(ws.id); + const c: any = r?.clinica || {}; + setEnderecoOk(!!(c.cep && c.cidade && c.estado)); + } else { + setEnderecoOk(true); // sem contexto claro → não bloqueia + } + } catch { setEnderecoOk(true); } // em erro, não bloqueia a tela + }, [isProfissional, ws?.id]); + + useEffect(() => { checkEndereco(); }, [checkEndereco]); const fetchLista = useCallback(async () => { setLoading(true); @@ -265,8 +305,11 @@ export const ProfissionaisPlugin: React.FC = () => { })} - {/* BUSCAR */} - {tab === 'buscar' && ( + {/* BUSCAR — bloqueia se faltar endereço/CEP da origem */} + {tab === 'buscar' && enderecoOk === false && ( + setTab('perfil')} /> + )} + {tab === 'buscar' && enderecoOk !== false && ( <>
diff --git a/frontend/views/plugins/SalasPlugin.tsx b/frontend/views/plugins/SalasPlugin.tsx index e8c7969..6312a5e 100644 --- a/frontend/views/plugins/SalasPlugin.tsx +++ b/frontend/views/plugins/SalasPlugin.tsx @@ -440,6 +440,26 @@ const ReservaRow: React.FC<{ r: Reserva; recebida?: boolean; onAction: (acao: 'c // ─── Main view ─────────────────────────────────────────────────────────────── type Tab = 'marketplace' | 'minhas' | 'reservas'; +// Bloqueio quando falta endereço/CEP da origem (clínica ou perfil) — a busca é por proximidade. +const GateEndereco: React.FC<{ isClinic: boolean }> = ({ isClinic }) => ( +
+
+ +
+

Complete seu endereço

+

+ A busca de salas é por proximidade. Preencha {isClinic + ? 'a localização e o CEP da sua clínica/consultório (em Configurações)' + : 'a localização e o CEP do seu perfil (em Profissionais → Meu Perfil)'} para continuar. +

+ +
+); + export const SalasPlugin: React.FC = () => { const toast = useToast(); const [tab, setTab] = useState('marketplace'); @@ -460,6 +480,30 @@ export const SalasPlugin: React.FC = () => { const [editingSala, setEditingSala] = useState(null); const [reservingSala, setReservingSala] = useState(null); + // Endereço da origem (clínica ou perfil) preenchido? null = checando. + const role = HybridBackend.getCurrentRole(); + const ws = HybridBackend.getActiveWorkspace(); + const isProfissional = ['dentista', 'biomedico', 'protetico'].includes(role); + const [enderecoOk, setEnderecoOk] = useState(null); + + const checkEndereco = useCallback(async () => { + try { + if (isProfissional) { + const res = await apiFetch('/api/profissionais/perfil'); + const p = res.ok ? await res.json() : {}; + setEnderecoOk(!!(p?.cep && p?.cidade && p?.estado)); + } else if (ws?.id) { + const r = await HybridBackend.getClinicaProfile(ws.id); + const c: any = r?.clinica || {}; + setEnderecoOk(!!(c.cep && c.cidade && c.estado)); + } else { + setEnderecoOk(true); + } + } catch { setEnderecoOk(true); } + }, [isProfissional, ws?.id]); + + useEffect(() => { checkEndereco(); }, [checkEndereco]); + const fetchMarketplace = useCallback(async () => { setLoading(true); try { @@ -560,7 +604,10 @@ export const SalasPlugin: React.FC = () => {
{/* MARKETPLACE */} - {tab === 'marketplace' && ( + {tab === 'marketplace' && enderecoOk === false && ( + + )} + {tab === 'marketplace' && enderecoOk !== false && ( <>