<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>CRM ExpoMadera — Empresas</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- SheetJS para leer/escribir Excel -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
  .card:hover { transform: translateY(-4px); transition: 0.18s; }
  textarea {resize: vertical;}
</style>
</head>
<body class="bg-gray-100 text-gray-800">

<div class="max-w-7xl mx-auto p-6">

  <header class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
    <h1 class="text-2xl font-bold">📋 CRM ExpoMadera — Directorio de Empresas</h1>

    <div class="flex gap-2 items-center">
      <label class="bg-white px-3 py-2 rounded shadow flex items-center gap-2">
        <input id="fileInput" type="file" accept=".xlsx,.xls,.csv" class="hidden" />
        <button id="btnLoad" class="text-sm text-gray-700">Cargar Excel / CSV</button>
      </label>

      <button id="toggleView" class="bg-blue-600 text-white px-3 py-2 rounded">Cambiar a Vista Cards</button>

      <button id="btnExportXLSX" class="bg-green-600 text-white px-3 py-2 rounded">Exportar Excel</button>

      <button id="btnDownloadJSON" class="bg-gray-700 text-white px-3 py-2 rounded">Descargar JSON</button>

      <button id="btnSendWebhook" class="bg-purple-700 text-white px-3 py-2 rounded">Enviar a Google (Webhook)</button>
    </div>
  </header>

  <!-- Buscar / filtros -->
  <div class="mb-4 grid grid-cols-1 md:grid-cols-3 gap-3">
    <input id="searchInput" type="search" placeholder="Buscar por nombre, categoría o url..." class="px-4 py-2 rounded border" />
    <select id="filterCategoria" class="px-4 py-2 rounded border">
      <option value="">Filtrar por categoría (todas)</option>
    </select>
    <div class="flex gap-2">
      <button id="btnTemplate" class="px-3 py-2 bg-yellow-500 rounded">Descargar plantilla Excel</button>
      <button id="btnClearLS" class="px-3 py-2 bg-red-500 text-white rounded">Borrar datos guardados</button>
    </div>
  </div>

  <!-- Contenedor de vistas -->
  <div id="vistaCRM" class="mb-8"></div>
  <div id="vistaCards" class="hidden mb-8"></div>

  <footer class="text-sm text-gray-500">
    Guardado local (LocalStorage). Para sincronizar con Google Sheets, crea un webhook en n8n/Make y pega la URL cuando hagas clic en "Enviar a Google".
  </footer>

</div>

<script>
/* ---------------------------
   UTILIDADES / Estado
   --------------------------- */

const LS_KEY = "crm_expomadera_v1";
let empresas = [];   // datos actuales
let vista = "CRM";   // "CRM" | "CARDS"

function cargarLocal() {
  const raw = localStorage.getItem(LS_KEY);
  if (!raw) return [];
  try { return JSON.parse(raw); } catch(e){ return []; }
}
function guardarLocal() {
  localStorage.setItem(LS_KEY, JSON.stringify(empresas));
}

/* ---------------------------
   FUNCIONES DE LECTURA (SheetJS)
   - permite cargar XLSX y CSV
   --------------------------- */

function readFile(file) {
  const reader = new FileReader();
  reader.onload = function(e) {
    const data = e.target.result;
    let workbook;
    try {
      workbook = XLSX.read(data, { type: 'binary' });
    } catch(err) {
      // si falla como binary, probar como arraybuffer
      const arr = new Uint8Array(data);
      workbook = XLSX.read(arr, { type: 'array' });
    }
    const firstSheetName = workbook.SheetNames[0];
    const worksheet = workbook.Sheets[firstSheetName];
    const json = XLSX.utils.sheet_to_json(worksheet, { defval: "" });
    // Procesar filas y mapear campos esperados
    const mapped = json.map(row => {
      // normalizamos nombres de columna comunes
      const lower = {};
      for (const k of Object.keys(row)) lower[k.toString().toLowerCase().trim()] = row[k];
      // posibles nombres de columna que buscaremos
      const get = (keys) => {
        for (const kk of keys) {
          if (lower[kk] !== undefined) return lower[kk];
        }
        return "";
      }
      const usuario = get(["usuario @","usuario","user","handle","instagram"]) || "";
      const nombre = get(["nombre","empresa","company","name"]) || "";
      const bio = get(["bio","descripcion","descripcion corta","description"]) || "";
      const url = get(["url instagram","instagram","instagram url","insta","url"]) || "";
      const categoria = get(["categoria","sector","rubros","rubro"]) || "";
      const web = get(["web","sitio web","sitio","website"]) || "";
      const telefono = get(["telefono","teléfono","phone","phone number","phone_number"]) || "";
      const whatsapp = get(["whatsapp","wapp","wpp"]) || "";
      const email = get(["email","mail","correo"]) || "";
      const observaciones = get(["observaciones","comentarios","notas"]) || "";
      return {
        usuario: usuario.toString(),
        nombre: nombre.toString(),
        bio: bio.toString(),
        instagram: url.toString(),
        categoria: categoria.toString(),
        web: web.toString(),
        telefono: telefono.toString(),
        whatsapp: whatsapp.toString(),
        email: email.toString(),
        observaciones: observaciones.toString()
      };
    });
    // Unir mapped con los datos actuales (append)
    empresas = mergeAndNormalize(empresas.concat(mapped));
    postLoad();
  };
  // leer como binary string (funciona con xlsx)
  reader.readAsBinaryString(file);
}

function mergeAndNormalize(arr) {
  // normalizamos: quitar duplicados por instagram o nombre
  const seen = new Map();
  for (const r of arr) {
    const key = (r.instagram || "").toString().trim().toLowerCase() || (r.nombre || "").toString().trim().toLowerCase();
    if (!key) continue;
    if (!seen.has(key)) {
      // aseguramos https en instagram url cuando es dominio
      if (r.instagram && !r.instagram.startsWith("http")) {
        if (r.instagram.includes("instagram.com")) r.instagram = r.instagram.startsWith("http") ? r.instagram : "https://" + r.instagram;
        else r.instagram = r.instagram;
      }
      seen.set(key, r);
    } else {
      // combinar campos vacíos
      const cur = seen.get(key);
      for (const k of Object.keys(r)) {
        if ((!cur[k] || cur[k]==="") && r[k]) cur[k] = r[k];
      }
    }
  }
  return Array.from(seen.values());
}

/* ---------------------------
   RENDER CRM (tabla) y Cards
   --------------------------- */

function renderCRM() {
  const container = document.getElementById("vistaCRM");
  if (!empresas.length) {
    container.innerHTML = `<div class="bg-white p-6 rounded shadow text-gray-600">No hay datos cargados. Carga un Excel o CSV usando "Cargar Excel / CSV".</div>`;
    return;
  }
  // tabla header
  let html = `<div class="bg-white rounded shadow overflow-x-auto"><table class="min-w-full"><thead class="bg-gray-50"><tr>
    <th class="p-3 text-left">Usuario @</th>
    <th class="p-3 text-left">Nombre</th>
    <th class="p-3 text-left">Bio</th>
    <th class="p-3 text-left">Instagram</th>
    <th class="p-3 text-left">Categoría</th>
    <th class="p-3 text-left">Web</th>
    <th class="p-3 text-left">Teléfono</th>
    <th class="p-3 text-left">WhatsApp</th>
    <th class="p-3 text-left">Email</th>
    <th class="p-3 text-left">Observaciones</th>
    <th class="p-3 text-left">Acciones</th>
  </tr></thead><tbody>`;
  empresas.forEach((e, i) => {
    html += `<tr class="border-t">
      <td class="p-2"><input value="${escapeHtml(e.usuario||'')}" onchange="updateField(${i},'usuario',this.value)" class="w-40 border p-1 rounded"></td>
      <td class="p-2"><input value="${escapeHtml(e.nombre||'')}" onchange="updateField(${i},'nombre',this.value)" class="w-48 border p-1 rounded"></td>
      <td class="p-2"><input value="${escapeHtml(e.bio||'')}" onchange="updateField(${i},'bio',this.value)" class="w-64 border p-1 rounded"></td>
      <td class="p-2"><input value="${escapeHtml(e.instagram||'')}" onchange="updateField(${i},'instagram',this.value)" class="w-64 border p-1 rounded"></td>
      <td class="p-2"><input value="${escapeHtml(e.categoria||'')}" onchange="updateField(${i},'categoria',this.value)" class="w-40 border p-1 rounded"></td>
      <td class="p-2"><input value="${escapeHtml(e.web||'')}" onchange="updateField(${i},'web',this.value)" class="w-48 border p-1 rounded"></td>
      <td class="p-2"><input value="${escapeHtml(e.telefono||'')}" onchange="updateField(${i},'telefono',this.value)" class="w-36 border p-1 rounded"></td>
      <td class="p-2"><input value="${escapeHtml(e.whatsapp||'')}" onchange="updateField(${i},'whatsapp',this.value)" class="w-36 border p-1 rounded"></td>
      <td class="p-2"><input value="${escapeHtml(e.email||'')}" onchange="updateField(${i},'email',this.value)" class="w-44 border p-1 rounded"></td>
      <td class="p-2"><textarea onchange="updateField(${i},'observaciones',this.value)" class="w-64 border p-1 rounded">${escapeHtml(e.observaciones||'')}</textarea></td>
      <td class="p-2 flex gap-2">
        <button onclick="openInstagram(${i})" class="px-2 py-1 bg-blue-600 text-white rounded">Instagram</button>
        <button onclick="openWhatsApp(${i})" class="px-2 py-1 bg-green-600 text-white rounded">WhatsApp</button>
        <button onclick="openEmail(${i})" class="px-2 py-1 bg-gray-700 text-white rounded">Email</button>
      </td>
    </tr>`;
  });
  html += `</tbody></table></div>`;
  container.innerHTML = html;
}

function renderCards() {
  const container = document.getElementById("vistaCards");
  if (!empresas.length) {
    container.innerHTML = `<div class="bg-white p-6 rounded shadow text-gray-600">No hay datos cargados.</div>`;
    return;
  }
  let html = `<div class="grid grid-cols-1 md:grid-cols-3 gap-4">`;
  empresas.forEach((e, i) => {
    html += `<div class="card bg-white p-4 rounded-lg shadow">
      <h3 class="font-bold text-lg">${escapeHtml(e.nombre || '—')}</h3>
      <p class="text-sm text-gray-500 break-words">${escapeHtml(e.bio || '')}</p>
      <p class="mt-2 text-xs text-gray-500">Instagram: <a href="${escapeAttr(e.instagram||'')}" target="_blank" class="text-blue-600 underline break-words">${escapeHtml(e.instagram||'')}</a></p>
      <p class="mt-1 text-sm"><strong>Categoría:</strong> <input value="${escapeHtml(e.categoria||'')}" onchange="updateField(${i},'categoria',this.value)" class="border p-1 rounded w-full"></p>
      <p class="mt-2"><strong>Tel:</strong> <input value="${escapeHtml(e.telefono||'')}" onchange="updateField(${i},'telefono',this.value)" class="border p-1 rounded w-full"></p>
      <p class="mt-2"><strong>Email:</strong> <input value="${escapeHtml(e.email||'')}" onchange="updateField(${i},'email',this.value)" class="border p-1 rounded w-full"></p>
      <p class="mt-2"><strong>Notas:</strong><textarea onchange="updateField(${i},'observaciones',this.value)" class="border p-1 rounded w-full">${escapeHtml(e.observaciones||'')}</textarea></p>
      <div class="mt-3 flex gap-2">
        <button onclick="openInstagram(${i})" class="flex-1 bg-blue-600 text-white px-3 py-2 rounded">Instagram</button>
        <button onclick="openWhatsApp(${i})" class="flex-1 bg-green-600 text-white px-3 py-2 rounded">WhatsApp</button>
        <button onclick="openEmail(${i})" class="flex-1 bg-gray-700 text-white px-3 py-2 rounded">Email</button>
      </div>
    </div>`;
  });
  html += `</div>`;
  container.innerHTML = html;
}

/* ---------------------------
   UTILIDADES UI / acciones
   --------------------------- */

function escapeHtml(s){ return (s===null||s===undefined) ? '' : s.toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function escapeAttr(s){ return (s===null||s===undefined) ? '' : s.toString().replace(/"/g,'%22'); }

function updateField(i, field, value) {
  empresas[i][field] = value;
  guardarLocal();
  updateCategoriaFilterOptions();
  if (vista === "CRM") renderCRM();
  else renderCards();
}

function openInstagram(i) {
  const url = empresas[i].instagram || empresas[i].instagram;
  if (!url) return alert("No hay URL de Instagram para esta empresa.");
  window.open(url.startsWith("http") ? url : "https://"+url, "_blank");
}

function openWhatsApp(i) {
  const w = empresas[i].whatsapp || empresas[i].telefono || "";
  const num = w.toString().replace(/\D/g,'');
  if (!num) return alert("No hay número disponible para WhatsApp.");
  const text = encodeURIComponent("Hola, te contacto desde Arquitectura.News / ExpoMadera. ¿Podemos coordinar una presentación?");
  window.open(`https://wa.me/${num}?text=${text}`, "_blank");
}

function openEmail(i) {
  const mail = empresas[i].email || "";
  if (!mail) return alert("No hay email registrado.");
  window.location.href = `mailto:${mail}`;
}

/* ---------------------------
   EXPORT / DESCARGA
   --------------------------- */

function exportToXLSX() {
  const wb = XLSX.utils.book_new();
  // clonamos datos a objeto simple
  const data = empresas.map(e => ({
    "Usuario @": e.usuario||"",
    "Nombre": e.nombre||"",
    "Bio": e.bio||"",
    "URL Instagram": e.instagram||"",
    "Categoría": e.categoria||"",
    "Web": e.web||"",
    "Teléfono": e.telefono||"",
    "WhatsApp": e.whatsapp||"",
    "Email": e.email||"",
    "Observaciones": e.observaciones||""
  }));
  const ws = XLSX.utils.json_to_sheet(data);
  XLSX.utils.book_append_sheet(wb, ws, "Empresas");
  XLSX.writeFile(wb, "CRM_ExpoMadera.xlsx");
}

function downloadJSON() {
  const blob = new Blob([JSON.stringify(empresas, null, 2)], { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "CRM_ExpoMadera.json";
  a.click();
}

/* ---------------------------
   WEBHOOK (Enviar a n8n/Make)
   --------------------------- */

async function sendToWebhook() {
  // Cambiá por la URL de tu webhook n8n/Make
  const webhookUrl = prompt("Pega aquí la URL del webhook (n8n/Make):", "WEBHOOK_URL_AQUI");
  if (!webhookUrl) return;
  try {
    const res = await fetch(webhookUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(empresas)
    });
    if (res.ok) alert("Datos enviados correctamente al webhook.");
    else alert("Error al enviar al webhook: " + res.statusText);
  } catch (err) {
    alert("Error al enviar: " + err.message);
  }
}

/* ---------------------------
   FILTROS / BUSCADOR
   --------------------------- */

function applyFilters() {
  const q = document.getElementById("searchInput").value.toLowerCase().trim();
  const cat = document.getElementById("filterCategoria").value;
  let filtered = cargarLocal();
  if (q) {
    filtered = filtered.filter(e => (e.nombre||"").toLowerCase().includes(q) || (e.categoria||"").toLowerCase().includes(q) || (e.instagram||"").toLowerCase().includes(q));
  }
  if (cat) filtered = filtered.filter(e => (e.categoria||"") === cat);
  empresas = filtered;
  render();
}

function updateCategoriaFilterOptions() {
  const all = cargarLocal();
  const cats = Array.from(new Set(all.map(e => (e.categoria||"").trim()).filter(Boolean))).sort();
  const sel = document.getElementById("filterCategoria");
  sel.innerHTML = `<option value="">Filtrar por categoría (todas)</option>`;
  for (const c of cats) sel.innerHTML += `<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`;
}

/* ---------------------------
   UTIL: POST LOAD, RENDER
   --------------------------- */

function postLoad() {
  guardarLocal();
  updateCategoriaFilterOptions();
  render();
}

/* ---------------------------
   Inicialización de UI / eventos
   --------------------------- */

document.getElementById("btnLoad").addEventListener("click", () => document.getElementById("fileInput").click());
document.getElementById("fileInput").addEventListener("change", (ev) => {
  const f = ev.target.files[0];
  if (!f) return;
  readFile(f);
});

document.getElementById("toggleView").addEventListener("click", () => {
  vista = (vista === "CRM") ? "CARDS" : "CRM";
  document.getElementById("toggleView").textContent = (vista === "CRM") ? "Cambiar a Vista Cards" : "Cambiar a Vista CRM";
  render();
});

document.getElementById("searchInput").addEventListener("input", () => applyFilters());
document.getElementById("btnExportXLSX").addEventListener("click", exportToXLSX);
document.getElementById("btnDownloadJSON").addEventListener("click", downloadJSON);
document.getElementById("btnSendWebhook").addEventListener("click", sendToWebhook);

document.getElementById("btnTemplate").addEventListener("click", () => {
  // descargar plantilla Excel vacía
  const wb = XLSX.utils.book_new();
  const sample = [{
    "Usuario @":"",
    "Nombre":"",
    "Bio":"",
    "URL Instagram":"",
    "Categoría":"",
    "Web":"",
    "Teléfono":"",
    "WhatsApp":"",
    "Email":"",
    "Observaciones":""
  }];
  const ws = XLSX.utils.json_to_sheet(sample);
  XLSX.utils.book_append_sheet(wb, ws, "Plantilla");
  XLSX.writeFile(wb, "plantilla_empresas.xlsx");
});

document.getElementById("btnClearLS").addEventListener("click", () => {
  if (!confirm("Borrar todos los datos guardados localmente?")) return;
  localStorage.removeItem(LS_KEY);
  empresas = [];
  render();
});

/* ---------------------------
   FUNCIONES RENDER / INICIO
   --------------------------- */

function render() {
  if (vista === "CRM") {
    document.getElementById("vistaCRM").classList.remove("hidden");
    document.getElementById("vistaCards").classList.add("hidden");
    renderCRM();
  } else {
    document.getElementById("vistaCRM").classList.add("hidden");
    document.getElementById("vistaCards").classList.remove("hidden");
    renderCards();
  }
}

(function init() {
  // cargar local al inicio
  empresas = cargarLocal();
  updateCategoriaFilterOptions();
  render();
})();

</script>
</body>
</html>

Blog