Skip to content

Repository files navigation

Prospector Local 🎯

App web desplegable en Vercel para encontrar negocios reales sin página web en la Zona Metropolitana de Tampico (Tampico, Ciudad Madero y Altamira, Tamaulipas, México) y ofrecerles desarrollo de apps web.

Stack: Next.js 15 (App Router) · TypeScript estricto · Tailwind CSS v4 · 100% APIs gratuitas.


✨ Funcionalidades

  • Mapa interactivo (Leaflet + OpenStreetMap, gratis sin API key) con marcadores coloreados por score de prospecto (verde = caliente, ámbar = tibio, rojo = frío).
  • Lista + filtros: por categoría (14), ciudad, score mínimo, estado del CRM y toggle "Solo sin web". Mapa y lista siempre sincronizados.
  • Scoring 0-100 justificado: sin web (+30), sin redes (+20), teléfono (+15), alto margen (+15), horario (+10), reseñas (+10).
  • CRM ligero: estado (nuevo/contactado/no interesado/cliente), notas y fecha de contacto. Supabase en la nube o localStorage automático (sin configuración).
  • Acciones de prospección: copiar pitch WhatsApp personalizado, abrir wa.me, llamar (tel:), ver en OpenStreetMap y exportar CSV de los filtrados (compatible con Excel).
  • Dashboard con stats + gráficas (Recharts): por categoría, ciudad y estado CRM, más el top de prospectos calientes.

🚀 Correr localmente

# Requiere Node 20+ (ver .nvmrc). Si usas nvm:
nvm use

npm install
npm run dev

Abre http://localhost:3000.

La app funciona sin ninguna configuración (usa localStorage como CRM de respaldo). Para persistencia en la nube, configura Supabase (abajo).

🗄️ Variables de entorno

Todas son opcionales. Copia .env.example a .env.local y completa lo que quieras:

Variable Descripción
NEXT_PUBLIC_SUPABASE_URL URL del proyecto Supabase (free tier)
NEXT_PUBLIC_SUPABASE_ANON_KEY Anon key pública de Supabase
OVERPASS_API_URL Endpoint de Overpass API (OpenStreetMap)
NOMINATIM_BASE_URL Endpoint de Nominatim (geocodificación)
YELP_API_KEY (Opcional) API key de Yelp Fusion para enriquecer contactos
GOOGLE_PLACES_API_KEY (Opcional, preferida) API key de Google Places (Nuevo) para enriquecer contactos

🗄️ Configurar Supabase (CRM en la nube)

  1. Crea un proyecto gratis en supabase.com (sin tarjeta).
  2. En SQL Editor, pega y ejecuta supabase/schema.sql (tabla + RLS).
  3. En Project Settings → API, copia la URL y la anon key a .env.local.
  4. Listo: el CRM usa Supabase automáticamente (multi-dispositivo).

Sin esas variables, el CRM usa localStorage del navegador (misma experiencia, solo en ese dispositivo). La UI muestra qué backend está activo en el encabezado.

☁️ Desplegar en Vercel

  1. Sube el repo a GitHub.
  2. Importa el proyecto en vercel.com/new (Next.js se detecta solo).
  3. (Opcional) En Settings → Environment Variables agrega las de Supabase.
  4. Deploy. Sin backend propio: las API routes corren como serverless functions.

Nota sobre la primera carga: el primer visitante puede tardar ~30-50 s mientras la serverless function consulta Overpass (la UI muestra un aviso y se auto-repara en segundo plano). Las visitas siguientes cargan al instante gracias al caché del navegador (24 h) + caché en memoria del server (12 h).


🔌 API (serverless functions)

GET /api/businesses

Proxy a Overpass API: consulta por ciudad, normaliza, puntúa y cachea 12 h en memoria.

Query param Valores Default
city tampico | madero | altamira | todas todas
category id de categoría (p. ej. restaurante) todas
radius radio en km (override) 15 km por ciudad
limit máx. de resultados 3000
refresh 1 fuerza re-consulta a Overpass off

city selecciona qué query(es) correr (los radios de 15 km se solapan). El filtro estricto por ciudad es client-side; el server devuelve la región completa.

GET /api/health

Healthcheck: confirma que la app responde y reporta si Supabase está configurado.

Diseño de robustez

  • Una sola query metro para city=todas: en vez de 3 consultas secuenciales (una por ciudad), se hace UNA sobre un radio que cubre los tres centros. Reduce 3× la presión de rate-limit y el cold start pasa de ~50 s (con abortos) a ~9-20 s. La atribución de ciudad sigue siendo por distancia (src/lib/geo.ts).
  • Reintentos: rate-limit (429) → backoff en el mismo endpoint; fallos de red/5xx → mirror alternativo; por consulta → 2 intentos.
  • Timeouts: intento 40 s · consulta 50 s · presupuesto total 57 s (Vercel Hobby: 60 s).
  • User-Agent descriptivo: obligatorio (las instancias públicas rechazan UAs por defecto como "node" → 406/429).
  • Caché de 2 niveles: en memoria del server (12 h) + localStorage del navegador (24 h) con auto-reparación: si un fetch devuelve warnings, reintenta en background hasta que el dataset converge a completo.

🎯 Acciones de prospección

En el panel de detalle de cada negocio:

  • Copiar pitch WhatsApp — mensaje personalizado con nombre, categoría y ciudad. Edita SALES_NAME y SALES_OFFER en src/lib/whatsapp.ts para personalizarlo.
  • Abrir WhatsApp (wa.me) y Llamar (tel:) con el número normalizado a +52.
  • Ver en OpenStreetMap — abre el elemento real en OSM para verificar antes de llamar.
  • Exportar CSV (en la barra de filtros) — descarga los prospectos filtrados con su estado/notas del CRM. Compatible con Excel (BOM UTF-8).

📊 Dashboard

Toggle 🗺️ Mapa ↔ 📊 Dashboard. Refleja los filtros activos:

  • Stats cards: total, sin web, sin teléfono, score promedio, contactados/clientes.
  • Gráficas (Recharts, gratis): por categoría (coloreadas por margen), por ciudad y por estado del CRM.
  • Top prospectos calientes: los 10 de mayor score del set filtrado.
  • Se carga bajo demanda (lazy) para no inflar el bundle inicial.

⚠️ Sobre la fuente de datos (OpenStreetMap)

  • Solo PYMEs: las cadenas y franquicias se eliminan automáticamente con 3 señales: tag brand (marca registrada), lista denylist de cadenas mexicanas/ globales (src/lib/chains.ts, editable) y heurística multi-sucursal (mismo nombre en ≥ 3 ubicaciones).
  • Los datos provienen de Overpass API (OSM) y su cobertura de contactos es parcial: "sin web" significa "sin web registrada en OSM". Se extraen teléfono, WhatsApp, email, Facebook, Instagram, Twitter, YouTube, TikTok y Telegram cuando OSM los tiene (los que no, no se inventan).
  • Yelp / Google Places (opcionales): configura YELP_API_KEY (free, 500/día) o GOOGLE_PLACES_API_KEY (preferida; $200/mes de crédito gratis, requiere tarjeta) para enriquecer cada prospecto con teléfono/web/rating al abrir su detalle. Google se intenta primero y Yelp sirve de respaldo; sin keys no se muestra nada.
  • Verifica con el botón "ver en OSM" antes de llamar. Es la única fuente masiva gratuita y sin API key para esta zona; Google Places cobra.

🧠 Decisiones de arquitectura

  • APIs gratuitas: Overpass API (datos masivos de negocios con website, phone, redes) + Nominatim (geocodificación). Leaflet + tiles OSM en vez de Google Maps.
  • Scoring 0-100 con pesos documentados en src/lib/constants.ts y colores por temperatura (rojo <45, ámbar 45-69, verde ≥70).
  • CRM: Supabase (free tier) con fallback automático a localStorage → la app cumple "funciona abriendo la URL sin configuración extra".
  • Proxy serverless (/api/businesses): la UI nunca golpea Overpass directamente; el server normaliza, puntúa, cachea y controla rate-limits/timeouts.
  • Persistencia del dataset: caché en memoria (server) + localStorage (cliente) + auto-reparación ante rate-limits de Overpass.

🧪 Scripts de desarrollo

Comando Qué hace
npm run dev Dev server
npm run build Build de producción
npm run smoke Smoke test del pipeline (Overpass real, radio 1.2 km)
npm run smoke:full Ídem con el radio real de 15 km
npm run bench Benchmark de endpoints Overpass
npm run check:csv Valida la generación del CSV contra la API local

🗂️ Estructura

.
├── scripts/                      # Utilidades de desarrollo (tsx)
│   ├── smoke.ts                  # Smoke test del pipeline Overpass
│   ├── bench-endpoints.ts        # Benchmark de endpoints
│   └── check-csv.ts              # Validación del CSV
├── supabase/
│   └── schema.sql                # DDL + RLS del CRM (justificado)
└── src/
    ├── app/
    │   ├── api/
    │   │   ├── businesses/route.ts   # Proxy a Overpass + scoring + caché
    │   │   └── health/route.ts       # Healthcheck
    │   ├── layout.tsx
    │   ├── page.tsx              # Mapa/Lista/Dashboard + toggle de vistas
    │   └── globals.css
    ├── components/
    │   ├── layout/Header.tsx
    │   ├── map/ProspectMap.tsx   # Leaflet + marcadores por score
    │   ├── prospect/             # FilterBar, List, Card, Detail (CRM)
    │   ├── dashboard/            # StatCard + gráficas (Recharts)
    │   └── ui/                   # ScoreBadge, Toggle, Select
    ├── hooks/
    │   └── useFilteredProspects.ts
    ├── lib/
    │   ├── types.ts              # Tipos de dominio
    │   ├── constants.ts          # Ciudades, categorías, pesos de scoring
    │   ├── overpass.ts           # Cliente Overpass + normalización
    │   ├── nominatim.ts          # Cliente Nominatim (geocodificación)
    │   ├── scoring.ts            # Algoritmo 0-100
    │   ├── geo.ts                # Haversine + atribución de ciudad
    │   ├── filtering.ts          # Filtro puro de prospectos
    │   ├── dashboard.ts          # Agregaciones del dashboard
    │   ├── cache.ts              # Caché en memoria con TTL
    │   ├── crm.ts                # Capa Supabase + fallback localStorage
    │   ├── supabase.ts           # Cliente singleton
    │   ├── database.types.ts     # Tipos de la BD (manuscritos)
    │   ├── csv.ts                # Exportación CSV
    │   ├── whatsapp.ts           # Pitch + enlaces wa.me
    │   └── utils.ts              # cn, formatos, enlaces tel/OSM
    └── store/
        ├── useProspectStore.ts   # Prospectos + filtros + fetch/caché
        └── useCrmStore.ts        # Estado/notas del CRM

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages