240 lines
12 KiB
HTML
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> |