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 ───────────────────────────────────────────────────────────────
|
// ─── Main view ───────────────────────────────────────────────────────────────
|
||||||
type Tab = 'buscar' | 'perfil' | 'propostas';
|
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 = () => {
|
export const ProfissionaisPlugin: React.FC = () => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const role = HybridBackend.getCurrentRole();
|
const role = HybridBackend.getCurrentRole();
|
||||||
@@ -146,6 +166,26 @@ export const ProfissionaisPlugin: React.FC = () => {
|
|||||||
// propostas
|
// propostas
|
||||||
const [propostas, setPropostas] = useState<{ enviadas: Proposta[]; recebidas: Proposta[] }>({ enviadas: [], recebidas: [] });
|
const [propostas, setPropostas] = useState<{ enviadas: Proposta[]; recebidas: Proposta[] }>({ enviadas: [], recebidas: [] });
|
||||||
const [proposingTo, setProposingTo] = useState<Profissional | null>(null);
|
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 () => {
|
const fetchLista = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -265,8 +305,11 @@ export const ProfissionaisPlugin: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* BUSCAR */}
|
{/* BUSCAR — bloqueia se faltar endereço/CEP da origem */}
|
||||||
{tab === 'buscar' && (
|
{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="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">
|
<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 ───────────────────────────────────────────────────────────────
|
// ─── Main view ───────────────────────────────────────────────────────────────
|
||||||
type Tab = 'marketplace' | 'minhas' | 'reservas';
|
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 = () => {
|
export const SalasPlugin: React.FC = () => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [tab, setTab] = useState<Tab>('marketplace');
|
const [tab, setTab] = useState<Tab>('marketplace');
|
||||||
@@ -460,6 +480,30 @@ export const SalasPlugin: React.FC = () => {
|
|||||||
const [editingSala, setEditingSala] = useState<Sala | null | 'new'>(null);
|
const [editingSala, setEditingSala] = useState<Sala | null | 'new'>(null);
|
||||||
const [reservingSala, setReservingSala] = useState<Sala | null>(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 () => {
|
const fetchMarketplace = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -560,7 +604,10 @@ export const SalasPlugin: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MARKETPLACE */}
|
{/* 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="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">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user