Files
web/mapeador.html
2026-02-13 08:42:45 +00:00

240 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mapeador de Variables - IntegraRepara</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
.fade-in { animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.scroller::-webkit-scrollbar { width: 6px; }
.scroller::-webkit-scrollbar-thumb { background-color: #cbd5e1; border-radius: 4px; }
</style>
</head>
<body class="bg-gray-100 text-gray-800 font-sans h-screen flex overflow-hidden">
<div id="sidebar-container" class="h-full shrink-0"></div>
<div class="flex-1 flex flex-col h-full min-w-0">
<div id="header-container"></div>
<main class="flex-1 overflow-y-auto p-6 scroller">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<div>
<h2 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
<i data-lucide="map" class="text-purple-600"></i> Mapeador de Variables
</h2>
<p class="text-sm text-gray-500">Conecta los datos del proveedor con tu base de datos interna.</p>
</div>
<div class="flex gap-2 bg-white p-1 rounded-lg shadow-sm border border-gray-200">
<select id="providerSelect" onchange="loadKeys()" class="bg-transparent text-sm font-bold text-gray-700 outline-none px-3 py-2 cursor-pointer">
<option value="multiasistencia">Multiasistencia</option>
<option value="homeserve">HomeServe</option>
</select>
<button onclick="saveMapping()" class="bg-slate-800 hover:bg-slate-700 text-white px-4 py-2 rounded-md text-sm font-bold transition flex items-center gap-2">
<i data-lucide="save" class="w-4 h-4"></i> Guardar
</button>
</div>
</div>
<div class="bg-purple-50 border-l-4 border-purple-500 p-4 mb-6 rounded-r-lg flex items-start gap-3">
<i data-lucide="info" class="w-5 h-5 text-purple-600 mt-0.5"></i>
<div class="text-sm text-purple-800">
<p class="font-bold">¿Cómo funciona esto?</p>
<p>El robot ha escaneado las webs de los proveedores y ha encontrado estas etiquetas.
Si quieres guardar un dato, escribe un nombre corto en <strong>"Tu Variable Interna"</strong> (ej: <code>poliza</code>, <code>f_efecto</code>).
Si no te interesa, marca <strong>Ignorar</strong>.</p>
</div>
</div>
<div class="bg-white rounded-xl shadow border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="text-xs text-gray-500 uppercase bg-gray-50 border-b">
<tr>
<th class="px-6 py-3 w-1/3">Etiqueta Original (Proveedor)</th>
<th class="px-6 py-3 w-1/3">Ejemplo de Valor</th>
<th class="px-6 py-3 w-1/4">Tu Variable Interna</th>
<th class="px-6 py-3 text-center">Ignorar</th>
</tr>
</thead>
<tbody id="mappingTable" class="divide-y divide-gray-100">
<tr><td colspan="4" class="px-6 py-10 text-center text-gray-400">Selecciona un proveedor para cargar datos...</td></tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
<div id="toast" class="fixed bottom-5 right-5 bg-slate-800 text-white px-6 py-3 rounded-lg shadow-xl translate-y-20 opacity-0 transition-all duration-300 z-50 flex items-center gap-3"><span id="toastMsg"></span></div>
<script src="js/layout.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
if (!localStorage.getItem("token")) window.location.href = "index.html";
// Cargar por defecto Multiasistencia
loadKeys();
});
async function loadKeys() {
const provider = document.getElementById('providerSelect').value;
const tbody = document.getElementById('mappingTable');
// Estado de carga
tbody.innerHTML = '<tr><td colspan="4" class="px-6 py-10 text-center text-gray-400 flex flex-col items-center"><i data-lucide="loader-2" class="w-8 h-8 animate-spin mb-2 text-purple-500"></i> Analizando datos descargados...</td></tr>';
lucide.createIcons();
try {
const res = await fetch(`${API_URL}/discovery/keys/${provider}`, {
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
const data = await res.json();
tbody.innerHTML = "";
if (!data.keys || data.keys.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-10 text-center text-gray-400">
<div class="flex flex-col items-center">
<i data-lucide="search-x" class="w-10 h-10 mb-2 opacity-50"></i>
<p class="font-bold">No se encontraron datos</p>
<p class="text-xs">Asegúrate de que el robot ha ejecutado al menos una vez y ha encontrado servicios.</p>
</div>
</td>
</tr>`;
lucide.createIcons();
return;
}
data.keys.forEach(k => {
const tr = document.createElement('tr');
tr.className = "hover:bg-gray-50 transition group";
// Generar un placeholder "slugify" (ej: "Fecha Efecto" -> "fecha_efecto")
let placeholder = k.original.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '') // Quitar caracteres raros
.replace(/\s+/g, '_'); // Espacios a guiones bajos
// Estilo si está ignorado
const isIgnored = k.ignored;
const rowOpacity = isIgnored ? "opacity-50 grayscale" : "";
tr.innerHTML = `
<td class="px-6 py-4 font-medium text-gray-900 break-words ${rowOpacity}">
${k.original}
</td>
<td class="px-6 py-4 ${rowOpacity}">
<div class="text-gray-500 font-mono text-xs bg-gray-100 p-2 rounded border border-gray-200 max-w-xs truncate" title="${k.sample}">
${k.sample}
</div>
</td>
<td class="px-6 py-4 ${rowOpacity}">
<input type="text"
class="target-input w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none text-purple-700 font-bold placeholder-gray-300 transition"
placeholder="${placeholder}"
value="${k.mappedTo || ''}"
data-original="${k.original}"
${isIgnored ? 'disabled' : ''}>
</td>
<td class="px-6 py-4 text-center">
<input type="checkbox"
class="ignore-check w-5 h-5 text-purple-600 rounded border-gray-300 focus:ring-purple-500 cursor-pointer"
${isIgnored ? 'checked' : ''}
onchange="toggleRow(this)">
</td>
`;
tbody.appendChild(tr);
});
} catch (e) {
console.error(e);
tbody.innerHTML = '<tr><td colspan="4" class="px-6 py-4 text-center text-red-500">Error cargando datos. Revisa la consola.</td></tr>';
}
}
// Efecto visual al marcar "Ignorar"
function toggleRow(checkbox) {
const row = checkbox.closest('tr');
const input = row.querySelector('.target-input');
const cell1 = row.cells[0];
const cell2 = row.cells[1];
const cell3 = row.cells[2];
if (checkbox.checked) {
input.disabled = true;
cell1.classList.add('opacity-50', 'grayscale');
cell2.classList.add('opacity-50', 'grayscale');
cell3.classList.add('opacity-50', 'grayscale');
} else {
input.disabled = false;
cell1.classList.remove('opacity-50', 'grayscale');
cell2.classList.remove('opacity-50', 'grayscale');
cell3.classList.remove('opacity-50', 'grayscale');
}
}
async function saveMapping() {
const provider = document.getElementById('providerSelect').value;
const rows = document.querySelectorAll('#mappingTable tr');
const mappings = [];
// Recorremos la tabla para sacar los datos
rows.forEach(row => {
const input = row.querySelector('.target-input');
const checkbox = row.querySelector('.ignore-check');
// Solo guardamos filas válidas (que tengan input)
if (input) {
const original = input.getAttribute('data-original');
const target = input.value.trim(); // Lo que escribió el usuario
const ignored = checkbox.checked;
// Guardamos si: está ignorado O tiene un nombre asignado
if (ignored || target.length > 0) {
mappings.push({
original: original,
target: target,
ignored: ignored
});
}
}
});
if (mappings.length === 0) {
showToast("No has configurado ninguna variable.", true);
return;
}
try {
const res = await fetch(`${API_URL}/discovery/save`, {
method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ provider, mappings })
});
if (res.ok) {
showToast("✅ Configuración guardada correctamente");
} else {
const err = await res.json();
showToast("Error: " + err.error, true);
}
} catch (e) { showToast("Error de conexión", true); }
}
function showToast(msg, isError = false) {
const t = document.getElementById('toast'), m = document.getElementById('toastMsg');
t.className = `fixed bottom-5 right-5 px-6 py-3 rounded-lg shadow-xl transition-all duration-300 z-50 flex items-center gap-3 ${isError ? 'bg-red-600' : 'bg-slate-800'} text-white font-medium`;
m.innerText = msg; t.classList.remove('translate-y-20', 'opacity-0');
setTimeout(() => t.classList.add('translate-y-20', 'opacity-0'), 3000);
}
</script>
</body>
</html>