feat(marketplace): exigir endereço/CEP da origem antes da busca por proximidade
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }) => (
|
||||
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-8 text-center">
|
||||
<div className="w-14 h-14 rounded-2xl bg-amber-50 flex items-center justify-center mx-auto mb-4">
|
||||
<MapPin size={26} className="text-amber-500" />
|
||||
</div>
|
||||
<h3 className="text-base font-black uppercase tracking-tight text-gray-900">Complete seu endereço</h3>
|
||||
<p className="text-sm text-gray-500 mt-2 max-w-md mx-auto leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
<button onClick={() => isClinic ? window.location.assign('/configuracoes') : onCompletarPerfil()}
|
||||
className="mt-5 inline-flex items-center gap-2 px-5 py-3 rounded-xl text-white text-xs font-black uppercase tracking-widest hover:opacity-90 transition-opacity"
|
||||
style={{ backgroundColor: COLOR }}>
|
||||
{isClinic ? 'Ir para Configurações' : 'Completar meu perfil'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
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<Profissional | null>(null);
|
||||
// null = checando; true/false = endereço da origem (clínica ou perfil) preenchido?
|
||||
const [enderecoOk, setEnderecoOk] = useState<boolean | null>(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 = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* BUSCAR */}
|
||||
{tab === 'buscar' && (
|
||||
{/* BUSCAR — bloqueia se faltar endereço/CEP da origem */}
|
||||
{tab === 'buscar' && enderecoOk === false && (
|
||||
<GateEndereco isClinic={!isProfissional} onCompletarPerfil={() => setTab('perfil')} />
|
||||
)}
|
||||
{tab === 'buscar' && enderecoOk !== false && (
|
||||
<>
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3 mb-4">
|
||||
|
||||
@@ -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 }) => (
|
||||
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-8 text-center">
|
||||
<div className="w-14 h-14 rounded-2xl bg-amber-50 flex items-center justify-center mx-auto mb-4">
|
||||
<MapPin size={26} className="text-amber-500" />
|
||||
</div>
|
||||
<h3 className="text-base font-black uppercase tracking-tight text-gray-900">Complete seu endereço</h3>
|
||||
<p className="text-sm text-gray-500 mt-2 max-w-md mx-auto leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
<button onClick={() => window.location.assign(isClinic ? '/configuracoes' : '/marketplace-profissionais')}
|
||||
className="mt-5 inline-flex items-center gap-2 px-5 py-3 rounded-xl text-white text-xs font-black uppercase tracking-widest hover:opacity-90 transition-opacity"
|
||||
style={{ backgroundColor: COLOR }}>
|
||||
{isClinic ? 'Ir para Configurações' : 'Completar meu perfil'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SalasPlugin: React.FC = () => {
|
||||
const toast = useToast();
|
||||
const [tab, setTab] = useState<Tab>('marketplace');
|
||||
@@ -460,6 +480,30 @@ export const SalasPlugin: React.FC = () => {
|
||||
const [editingSala, setEditingSala] = useState<Sala | null | 'new'>(null);
|
||||
const [reservingSala, setReservingSala] = useState<Sala | null>(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<boolean | null>(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 = () => {
|
||||
</div>
|
||||
|
||||
{/* MARKETPLACE */}
|
||||
{tab === 'marketplace' && (
|
||||
{tab === 'marketplace' && enderecoOk === false && (
|
||||
<GateEndereco isClinic={!isProfissional} />
|
||||
)}
|
||||
{tab === 'marketplace' && enderecoOk !== false && (
|
||||
<>
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-5">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||
|
||||
Reference in New Issue
Block a user