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.
- 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.
# Requiere Node 20+ (ver .nvmrc). Si usas nvm:
nvm use
npm install
npm run devAbre http://localhost:3000.
La app funciona sin ninguna configuración (usa
localStoragecomo CRM de respaldo). Para persistencia en la nube, configura Supabase (abajo).
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 |
- Crea un proyecto gratis en supabase.com (sin tarjeta).
- En SQL Editor, pega y ejecuta
supabase/schema.sql(tabla + RLS). - En Project Settings → API, copia la
URLy laanon keya.env.local. - Listo: el CRM usa Supabase automáticamente (multi-dispositivo).
Sin esas variables, el CRM usa
localStoragedel navegador (misma experiencia, solo en ese dispositivo). La UI muestra qué backend está activo en el encabezado.
- Sube el repo a GitHub.
- Importa el proyecto en vercel.com/new (Next.js se detecta solo).
- (Opcional) En Settings → Environment Variables agrega las de Supabase.
- 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).
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 |
cityselecciona 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.
Healthcheck: confirma que la app responde y reporta si Supabase está configurado.
- 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-Agentdescriptivo: obligatorio (las instancias públicas rechazan UAs por defecto como "node" → 406/429).- Caché de 2 niveles: en memoria del server (12 h) +
localStoragedel navegador (24 h) con auto-reparación: si un fetch devuelve warnings, reintenta en background hasta que el dataset converge a completo.
En el panel de detalle de cada negocio:
- Copiar pitch WhatsApp — mensaje personalizado con nombre, categoría y ciudad.
Edita
SALES_NAMEySALES_OFFERensrc/lib/whatsapp.tspara 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).
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.
- 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) oGOOGLE_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.
- 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.tsy 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.
| 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 |
.
├── 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