343 lines
19 KiB
HTML
343 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Laboratorio Robot HomeServe</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; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
.input-modern { @apply w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-semibold text-slate-700 outline-none transition-all focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100; }
|
|
.label-modern { @apply block text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1.5 ml-1; }
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-slate-50 text-slate-800 font-sans antialiased h-screen flex flex-col overflow-hidden">
|
|
|
|
<div class="bg-white border-b border-slate-200 p-4 flex justify-between items-center shrink-0">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-blue-600 text-white rounded-xl flex items-center justify-center shadow-lg"><i data-lucide="bot" class="w-5 h-5"></i></div>
|
|
<h1 class="text-xl font-black text-slate-800 tracking-tight">Laboratorio API Robot</h1>
|
|
</div>
|
|
<a href="servicios.html" class="text-xs font-bold text-slate-500 hover:text-blue-600 flex items-center gap-2 bg-slate-100 px-4 py-2 rounded-lg transition-colors"><i data-lucide="arrow-left" class="w-4 h-4"></i> Volver al Panel</a>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto p-8 no-scrollbar">
|
|
<div class="max-w-5xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-8 fade-in">
|
|
|
|
<div class="bg-white p-8 rounded-[2rem] shadow-sm border border-slate-200 h-fit relative overflow-hidden">
|
|
<div class="flex justify-between items-start mb-6 border-b border-slate-100 pb-4">
|
|
<h2 class="font-black text-lg text-slate-800 flex items-center gap-2">
|
|
<i data-lucide="send" class="text-blue-500 w-5 h-5"></i> Lanzar Petición
|
|
</h2>
|
|
<div id="credStatus" class="flex items-center gap-2 px-3 py-1.5 bg-slate-100 text-slate-500 border border-slate-200 rounded-full text-[9px] font-black uppercase tracking-widest">
|
|
<i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i> Comprobando acceso...
|
|
</div>
|
|
</div>
|
|
|
|
<form id="robotForm" class="space-y-5" onsubmit="sendToRobot(event)">
|
|
<div>
|
|
<label class="label-modern">Nº Siniestro / Expediente HomeServe</label>
|
|
<input type="text" id="r_siniestro" placeholder="Ej: 12345678" class="input-modern" required>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="label-modern">Nuevo Estado (Código HS)</label>
|
|
<select id="r_estado" class="input-modern cursor-pointer" required>
|
|
<option value="">Seleccionar nuevo estado...</option>
|
|
|
|
<optgroup label="Estados Principales">
|
|
<option value="307">CITADO (307 - Espera de Prof. por fecha de inicio)</option>
|
|
<option value="303">PRESUPUESTO (303 - Espera de Cliente por aceptación)</option>
|
|
<option value="345">TERMINADO (345 - En realización pendiente Terminar)</option>
|
|
</optgroup>
|
|
|
|
<optgroup label="En espera de Profesional">
|
|
<option value="313">313 - Por secado de cala, pintura o parquet</option>
|
|
<option value="318">318 - Por confirmación del Siniestro</option>
|
|
<option value="319">319 - Por material</option>
|
|
<option value="320">320 - Por espera de otro gremio</option>
|
|
<option value="321">321 - Por presupuesto/valoración</option>
|
|
<option value="323">323 - Por mejora del tiempo</option>
|
|
<option value="336">336 - Por avería en observación</option>
|
|
<option value="342">342 - Pendiente cobro franquicia</option>
|
|
</optgroup>
|
|
|
|
<optgroup label="En espera de Cliente / Perjudicado">
|
|
<option value="326">326 - Por pago de Factura Contado/Franquicia</option>
|
|
<option value="348">348 - Por indicaciones (Cliente)</option>
|
|
<option value="352">352 - Por indicaciones (Perjudicado)</option>
|
|
</optgroup>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="label-modern">Fecha Próx. Cita (Opcional)</label>
|
|
<input type="text" id="r_fecha" placeholder="DD/MM/AAAA" class="input-modern text-center" pattern="\d{2}/\d{2}/\d{4}">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="label-modern">Observaciones (OBLIGATORIO para el Robot)</label>
|
|
<textarea id="r_obs" rows="3" placeholder="Describe qué ha pasado con el siniestro..." class="input-modern resize-none" required></textarea>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 pt-2">
|
|
<input type="checkbox" id="r_info" class="w-5 h-5 rounded border-slate-300 text-blue-600 cursor-pointer">
|
|
<label for="r_info" class="text-xs font-bold text-slate-600 cursor-pointer uppercase tracking-wider">Marcar "Informado al cliente"</label>
|
|
</div>
|
|
|
|
<button type="submit" id="btnLaunch" disabled class="w-full bg-slate-900 text-white font-black py-4 rounded-xl text-xs uppercase tracking-widest shadow-lg transition-all flex items-center justify-center gap-2 mt-4 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<i data-lucide="rocket" class="w-4 h-4"></i> Esperando credenciales...
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="bg-slate-900 p-8 rounded-[2rem] shadow-xl border border-slate-800 text-white flex flex-col h-[650px]">
|
|
<div class="flex justify-between items-center border-b border-slate-700 pb-4 mb-4 shrink-0">
|
|
<h2 class="font-black text-lg text-slate-100 flex items-center gap-2">
|
|
<i data-lucide="activity" class="text-emerald-400 w-5 h-5"></i> Monitor DB Server
|
|
</h2>
|
|
<div class="flex items-center gap-2 px-3 py-1 bg-emerald-950 text-emerald-400 border border-emerald-800 rounded-full text-[10px] font-black uppercase tracking-widest">
|
|
<div class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></div> Polling Activo
|
|
</div>
|
|
</div>
|
|
|
|
<div id="logContainer" class="flex-1 bg-slate-950 rounded-xl border border-slate-800 p-5 font-mono text-xs overflow-y-auto space-y-3 no-scrollbar">
|
|
<div class="text-slate-500 italic">Esperando peticiones...</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div id="loginBlocker" class="fixed inset-0 bg-slate-900 z-[200] flex flex-col items-center justify-center text-white hidden">
|
|
<i data-lucide="lock" class="w-16 h-16 text-rose-500 mb-4"></i>
|
|
<h2 class="text-2xl font-black tracking-tight mb-2">Laboratorio Bloqueado</h2>
|
|
<p class="text-slate-400 mb-8 text-center max-w-md">Para realizar pruebas en el robot necesitas iniciar sesión con tu cuenta de administrador.</p>
|
|
|
|
<form onsubmit="loginLab(event)" class="bg-slate-800 p-8 rounded-3xl w-full max-w-sm space-y-5 shadow-2xl border border-slate-700">
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5 ml-1">Email o Teléfono</label>
|
|
<input type="text" id="labEmail" class="w-full bg-slate-900 border border-slate-700 px-4 py-3 rounded-xl text-sm font-semibold text-white outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1.5 ml-1">Contraseña</label>
|
|
<input type="password" id="labPass" class="w-full bg-slate-900 border border-slate-700 px-4 py-3 rounded-xl text-sm font-semibold text-white outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all" required>
|
|
</div>
|
|
<button type="submit" id="btnLabLogin" class="w-full bg-blue-600 hover:bg-blue-500 px-6 py-4 rounded-xl font-black uppercase tracking-widest text-xs transition-colors mt-2">
|
|
Desbloquear Laboratorio
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' || window.location.protocol === 'file:'
|
|
? 'https://integrarepara-api.integrarepara.es' // Forzamos que siempre apunte al servidor real si estás en tu PC
|
|
: 'https://integrarepara-api.integrarepara.es';
|
|
|
|
let knownJobs = {};
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
lucide.createIcons();
|
|
|
|
// 1. COMPROBAR SESIÓN
|
|
const token = localStorage.getItem("token");
|
|
if (!token) {
|
|
document.getElementById('loginBlocker').classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
// Si ya hay token, arrancamos directo
|
|
startLab(token);
|
|
});
|
|
|
|
// Lógica para iniciar sesión directamente desde aquí
|
|
async function loginLab(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('btnLabLogin');
|
|
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin mx-auto"></i>';
|
|
lucide.createIcons();
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
email: document.getElementById('labEmail').value,
|
|
password: document.getElementById('labPass').value
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
localStorage.setItem("token", data.token);
|
|
document.getElementById('loginBlocker').classList.add('hidden');
|
|
startLab(data.token);
|
|
} else {
|
|
alert(data.error || "Credenciales incorrectas.");
|
|
btn.innerHTML = 'Desbloquear Laboratorio';
|
|
}
|
|
} catch (err) {
|
|
alert("Error conectando con el servidor.");
|
|
btn.innerHTML = 'Desbloquear Laboratorio';
|
|
}
|
|
}
|
|
|
|
async function startLab(token) {
|
|
await checkHomeServeCredentials(token);
|
|
addLog("Conectado a la Base de Datos. Escuchando la cola...", "system");
|
|
setInterval(fetchQueueStatus, 3000);
|
|
fetchQueueStatus();
|
|
}
|
|
|
|
// =====================================
|
|
// VERIFICAR CREDENCIALES
|
|
// =====================================
|
|
async function checkHomeServeCredentials(token) {
|
|
try {
|
|
const res = await fetch(`${API_URL}/providers/credentials`, {
|
|
headers: { "Authorization": `Bearer ${token}` }
|
|
});
|
|
const data = await res.json();
|
|
|
|
const credBadge = document.getElementById('credStatus');
|
|
const btnLaunch = document.getElementById('btnLaunch');
|
|
|
|
if (data.ok && data.credentials) {
|
|
const hsCreds = data.credentials.find(c => c.provider === 'homeserve');
|
|
|
|
if (hsCreds && hsCreds.status === 'active') {
|
|
credBadge.className = "flex items-center gap-2 px-3 py-1.5 bg-emerald-50 text-emerald-600 border border-emerald-200 rounded-full text-[9px] font-black uppercase tracking-widest";
|
|
credBadge.innerHTML = `<i data-lucide="check-circle" class="w-3 h-3"></i> H-Serve: ${hsCreds.username}`;
|
|
|
|
btnLaunch.disabled = false;
|
|
btnLaunch.className = "w-full bg-blue-600 hover:bg-blue-500 text-white font-black py-4 rounded-xl text-xs uppercase tracking-widest shadow-lg shadow-blue-500/30 transition-all active:scale-95 flex items-center justify-center gap-2 mt-4";
|
|
btnLaunch.innerHTML = '<i data-lucide="rocket" class="w-4 h-4"></i> Inyectar en DB Local';
|
|
|
|
} else {
|
|
credBadge.className = "flex items-center gap-2 px-3 py-1.5 bg-rose-50 text-rose-600 border border-rose-200 rounded-full text-[9px] font-black uppercase tracking-widest";
|
|
credBadge.innerHTML = `<i data-lucide="alert-triangle" class="w-3 h-3"></i> Faltan Credenciales`;
|
|
|
|
addLog("⚠️ ATENCIÓN: El robot no podrá funcionar porque no has configurado tu usuario y contraseña de HomeServe en la pestaña de Proveedores.", "warning");
|
|
|
|
btnLaunch.disabled = false;
|
|
btnLaunch.className = "w-full bg-rose-600 hover:bg-rose-500 text-white font-black py-4 rounded-xl text-xs uppercase tracking-widest shadow-lg shadow-rose-500/30 transition-all active:scale-95 flex items-center justify-center gap-2 mt-4";
|
|
btnLaunch.innerHTML = '<i data-lucide="rocket" class="w-4 h-4"></i> Inyectar de todos modos';
|
|
}
|
|
}
|
|
lucide.createIcons();
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
// =====================================
|
|
// LÓGICA DEL MONITOR Y COLA
|
|
// =====================================
|
|
function addLog(message, type = "info") {
|
|
const container = document.getElementById('logContainer');
|
|
const time = new Date().toLocaleTimeString();
|
|
let color = "text-slate-300";
|
|
let icon = "👉";
|
|
|
|
if (type === "success") { color = "text-emerald-400"; icon = "✅"; }
|
|
else if (type === "error") { color = "text-red-400"; icon = "❌"; }
|
|
else if (type === "warning") { color = "text-amber-400"; icon = "⚠️"; }
|
|
else if (type === "system") { color = "text-blue-400"; icon = "🤖"; }
|
|
|
|
const div = document.createElement('div');
|
|
div.className = `flex items-start gap-2 ${color} fade-in`;
|
|
div.innerHTML = `<span class="opacity-50 shrink-0">[${time}]</span> <span class="shrink-0">${icon}</span> <span class="break-words">${message}</span>`;
|
|
|
|
if (container.innerText.includes("Esperando peticiones")) container.innerHTML = '';
|
|
|
|
container.prepend(div);
|
|
}
|
|
|
|
async function sendToRobot(e) {
|
|
e.preventDefault();
|
|
|
|
const btn = document.getElementById('btnLaunch');
|
|
const originalContent = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Guardando en cola...';
|
|
lucide.createIcons();
|
|
|
|
const payload = {
|
|
service_number: document.getElementById('r_siniestro').value.trim(),
|
|
new_status: document.getElementById('r_estado').value,
|
|
appointment_date: document.getElementById('r_fecha').value.trim(),
|
|
observation: document.getElementById('r_obs').value.trim(),
|
|
inform_client: document.getElementById('r_info').checked
|
|
};
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/robot/queue`, {
|
|
method: 'POST',
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": `Bearer ${localStorage.getItem("token")}`
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if(data.ok) {
|
|
addLog(`Job [#${data.jobId}] - Petición encolada para el siniestro ${payload.service_number}`, "info");
|
|
document.getElementById('robotForm').reset();
|
|
} else {
|
|
addLog(`Error del servidor: ${data.error}`, "error");
|
|
}
|
|
} catch (err) {
|
|
addLog(`Error de red al enviar petición: ${err.message}`, "error");
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalContent;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
async function fetchQueueStatus() {
|
|
try {
|
|
const res = await fetch(`${API_URL}/robot/queue`, {
|
|
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok && data.jobs) {
|
|
const jobs = data.jobs.reverse();
|
|
|
|
jobs.forEach(job => {
|
|
const prevStatus = knownJobs[job.id];
|
|
|
|
if (prevStatus !== job.status) {
|
|
|
|
if (!prevStatus && job.status !== 'PENDING') {
|
|
knownJobs[job.id] = job.status;
|
|
return;
|
|
}
|
|
|
|
if (job.status === 'RUNNING') {
|
|
addLog(`Job [#${job.id}] - 🏃♂️ El robot ha arrancado el Siniestro ${job.service_number}...`, "system");
|
|
}
|
|
else if (job.status === 'DONE') {
|
|
addLog(`Job [#${job.id}] - ✅ Siniestro ${job.service_number} guardado con éxito en HomeServe.`, "success");
|
|
}
|
|
else if (job.status === 'FAILED') {
|
|
addLog(`Job [#${job.id}] - ❌ Fallo en Siniestro ${job.service_number}: ${job.error_msg || 'Error desconocido'}`, "error");
|
|
}
|
|
|
|
knownJobs[job.id] = job.status;
|
|
}
|
|
});
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |