feat(geo): busca por raio (PostGIS) em Salas e Profissionais
- geocoder de CEP com cache (cep_geo) + rotação: AwesomeAPI → Nominatim(CEP, c/ User-Agent) → ViaCEP+Nominatim(endereço) - colunas geo geography(Point,4326) + índice GiST em salas e usuarios - geocoda ao salvar (POST/PUT salas, PUT perfil) via setGeoByCep - marketplaces aceitam ?cep=&raio= → ST_DWithin + dist_km, ordenado por distância - frontend: filtro Meu CEP + Raio + badge ~X KM nos cards - fix: clinicas.nome inexistente → nome_fantasia (salas/minhas + marketplace) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@ interface Profissional {
|
||||
especialidade: string;
|
||||
raio_atuacao_km?: number | null;
|
||||
bio_profissional?: string | null;
|
||||
dist_km?: number | string | null;
|
||||
}
|
||||
|
||||
interface Proposta {
|
||||
@@ -138,6 +139,7 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
const [fCidade, setFCidade] = useState('');
|
||||
const [fEspecialidade, setFEspecialidade] = useState('');
|
||||
const [fCep, setFCep] = useState('');
|
||||
const [fRaio, setFRaio] = useState('');
|
||||
// perfil
|
||||
const [perfil, setPerfil] = useState<any>(null);
|
||||
const [savingPerfil, setSavingPerfil] = useState(false);
|
||||
@@ -154,11 +156,12 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
if (fCidade) params.set('cidade', fCidade);
|
||||
if (fEspecialidade) params.set('especialidade', fEspecialidade);
|
||||
if (fCep) params.set('cep', fCep);
|
||||
if (fCep && Number(fRaio) > 0) params.set('raio', fRaio);
|
||||
const res = await apiFetch(`/api/profissionais/marketplace?${params}`);
|
||||
if (res.ok) setLista(await res.json());
|
||||
} catch { /* silently fail */ }
|
||||
finally { setLoading(false); }
|
||||
}, [fTipo, fEstado, fCidade, fEspecialidade, fCep]);
|
||||
}, [fTipo, fEstado, fCidade, fEspecialidade, fCep, fRaio]);
|
||||
|
||||
const fetchPerfil = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -288,8 +291,12 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
<input className={`${inputCls} uppercase`} value={fEspecialidade} onChange={e => setFEspecialidade(e.target.value.toUpperCase())} placeholder="ORTODONTIA" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>CEP (região)</label>
|
||||
<input className={inputCls} value={fCep} onChange={e => setFCep(e.target.value)} placeholder="79000" />
|
||||
<label className={labelCls}>Meu CEP</label>
|
||||
<input className={inputCls} value={fCep} onChange={e => setFCep(e.target.value)} placeholder="79000-000" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={labelCls}>Raio (km)</label>
|
||||
<input type="number" min="1" className={inputCls} value={fRaio} onChange={e => setFRaio(e.target.value)} placeholder="20" />
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={fetchLista} disabled={loading}
|
||||
@@ -331,6 +338,7 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-500 font-medium">
|
||||
<MapPin size={12} className="shrink-0 text-gray-400" />
|
||||
<span>{[p.bairro, p.cidade, p.estado].filter(Boolean).join(' · ')}{p.raio_atuacao_km ? ` · ATENDE EM ATÉ ${p.raio_atuacao_km} KM` : ''}</span>
|
||||
{p.dist_km != null && <span className="ml-auto shrink-0 text-[9px] font-black px-2 py-0.5 rounded-full bg-orange-100 text-orange-700">~{p.dist_km} KM</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-500 font-medium">
|
||||
|
||||
Reference in New Issue
Block a user