estados-multi/worker-multi-estado.js

249 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// worker-multi-estado.js
'use strict';
const { chromium } = require('playwright');
const admin = require('firebase-admin');
// --- CONFIGURACIÓN ---
const CONFIG = {
QUEUE_COLLECTION: 'multiasistencia_cambios_estado',
RESULT_COLLECTION: 'multiasistencia_cambios_estado_log',
MULTI_LOGIN: "https://web.multiasistencia.com/w3multi/acceso.php",
MULTI_ACTION_BASE: "https://web.multiasistencia.com/w3multi/fechaccion.php",
NAV_TIMEOUT: 60000,
RESCAN_SECONDS: 60
};
// --- UTILS ---
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function toServerTimestamp() { return admin.firestore.FieldValue.serverTimestamp(); }
// Conversor de Hora
function timeToMultiValue(timeStr) {
if (!timeStr) return "";
const [h, m] = timeStr.split(':').map(Number);
// Formato interno de Multi: segundos desde medianoche
return String((h * 3600) + (m * 60));
}
// --- FIREBASE INIT ---
function initFirebase() {
if (!process.env.FIREBASE_PRIVATE_KEY) throw new Error('Missing FIREBASE_PRIVATE_KEY');
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
}),
});
}
return admin.firestore();
}
// --- LOGIN ---
async function loginMulti(page, db) {
let user = "", pass = "";
const doc = await db.collection("providerCredentials").doc("multiasistencia").get();
if (doc.exists) { user = doc.data().user; pass = doc.data().pass; }
if (!user) throw new Error("Faltan credenciales en providerCredentials/multiasistencia");
console.log('🔐 Login en Multiasistencia...');
await page.goto(CONFIG.MULTI_LOGIN, { timeout: 60000, waitUntil: 'domcontentloaded' });
const userFilled = await page.evaluate((u) => {
const el = document.querySelector('input[name="usuario"]') || document.querySelector('input[type="text"]');
if (el) { el.value = u; el.dispatchEvent(new Event('input', { bubbles: true })); return true; }
return false;
}, user);
if (!userFilled) await page.fill('input[name="usuario"]', user);
await page.fill('input[type="password"]', pass);
await page.click('input[type="submit"]');
await page.waitForTimeout(4000);
}
// --- PLAYWRIGHT SETUP ---
async function withBrowser(fn) {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
const context = await browser.newContext();
const page = await context.newPage();
try { return await fn(page); } finally { await browser.close().catch(() => {}); }
}
// --- HELPERS DE EVENTOS ---
async function forceUpdate(elementHandle) {
if (elementHandle) {
await elementHandle.evaluate(el => {
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
});
}
}
// --- LÓGICA PRINCIPAL ---
async function processChangeState(page, db, jobData) {
const { serviceNumber, reasonValue, comment, dateStr, timeStr } = jobData;
// 1. LOGIN
await loginMulti(page, db);
// 2. IR AL SERVICIO
const targetUrl = `${CONFIG.MULTI_ACTION_BASE}?reparacion=${serviceNumber}&modo=0&navid=%2Fw3multi%2Ffrepasos_new.php%FDGET%FDrefresh%3D1%FC`;
console.log(`📂 Abriendo servicio 27867185...`); // Log limpio para depurar
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
// Esperar formulario
await page.waitForSelector('select.answer-select', { timeout: 20000 });
await page.waitForTimeout(1500);
console.log('📝 Rellenando formulario (Modo Preciso)...');
// 3. MOTIVO (Primer select de la página suele ser el motivo)
const reasonSel = page.locator('select.answer-select').first();
await reasonSel.selectOption(String(reasonValue));
await forceUpdate(await reasonSel.elementHandle());
// 4. COMENTARIO
if (comment) {
const commentBox = page.locator('textarea[formcontrolname="comment"]');
await commentBox.fill(comment);
await forceUpdate(await commentBox.elementHandle());
}
// 5. FECHA
if (dateStr) {
const dateInput = page.locator('input[type="date"]');
await dateInput.fill(dateStr);
await forceUpdate(await dateInput.elementHandle());
// Click fuera para asegurar validación de fecha
await page.click('body');
}
// 6. HORA (El punto crítico)
if (timeStr) {
const secondsValue = timeToMultiValue(timeStr); // ej: "28800" para 08:00
console.log(`🕒 Intentando poner hora: ${timeStr} (Valor interno: ${secondsValue})`);
// ESTRATEGIA INFALIBLE: Buscar el select que contiene esa opción específica usando XPath
// "Búscame un <select> que tenga dentro una <option> con value='28800'"
const timeSelectHandle = await page.$(`xpath=//select[.//option[@value="${secondsValue}"]]`);
if (timeSelectHandle) {
await timeSelectHandle.selectOption(secondsValue);
await forceUpdate(timeSelectHandle);
console.log('✅ Hora seleccionada correctamente.');
} else {
console.log(`⚠️ ERROR GRAVE: No encuentro ningún desplegable que tenga la opción de valor "${secondsValue}".`);
// Fallback desesperado: intentar ponerlo en el segundo select que encuentre
const allSelects = await page.$$('select');
if (allSelects.length > 1) {
console.log('⚠️ Intentando fallback en el segundo select...');
await allSelects[1].selectOption(secondsValue).catch(() => {});
await forceUpdate(allSelects[1]);
}
}
}
await page.waitForTimeout(2000);
// 7. GUARDAR
const btn = page.locator('button.form-container-button-submit');
// Verificación final antes de clickar
if (await btn.isDisabled()) {
console.log('⛔ Botón deshabilitado. Intentando reactivación final...');
// A veces hacer click en el comentario ayuda a que Angular se entere
await page.click('textarea[formcontrolname="comment"]');
await page.keyboard.press('Tab');
await page.waitForTimeout(1000);
if (await btn.isDisabled()) {
throw new Error(`IMPOSIBLE GUARDAR: Formulario incompleto. ¿La hora ${timeStr} es válida para este servicio?`);
}
}
console.log('💾 Click en Guardar...');
await btn.click();
// 8. VERIFICAR RESULTADO
await page.waitForTimeout(4000);
// Captura de mensajes de éxito/error de la web
const message = await page.evaluate(() => {
const err = document.querySelector('.form-container-error');
const ok = document.querySelector('.form-container-success');
const toast = document.querySelector('encastrables-success-error-message');
if (err && err.innerText) return `ERROR WEB: ${err.innerText}`;
if (ok && ok.innerText) return `EXITO WEB: ${ok.innerText}`;
if (toast && toast.innerText) return `MSG: ${toast.innerText}`;
return null;
});
console.log(` Estado final: ${message || 'Sin mensaje (Redirección correcta)'}`);
if (message && message.includes('ERROR')) throw new Error(message);
return { success: true, finalUrl: page.url() };
}
// --- WORKER ---
async function claimJobById(db, jobId) {
const ref = db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId);
return await db.runTransaction(async (tx) => {
const snap = await tx.get(ref);
if (!snap.exists || snap.data().status !== 'PENDING') return null;
tx.set(ref, { status: 'RUNNING', claimedAt: toServerTimestamp() }, { merge: true });
return { id: jobId, ...snap.data() };
});
}
async function markJobDone(db, jobId, result) {
await db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId).set({ status: 'DONE', result }, { merge: true });
await db.collection(CONFIG.RESULT_COLLECTION).add({ jobId, ok: true, ...result, createdAt: toServerTimestamp() });
}
async function markJobFailed(db, jobId, err) {
await db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId).set({
status: 'FAILED',
error: { message: err.message }
}, { merge: true });
await db.collection(CONFIG.RESULT_COLLECTION).add({ jobId, ok: false, error: err.message, createdAt: toServerTimestamp() });
}
async function processJob(db, job) {
console.log(`>>> Procesando Job: ${job.id}`);
try {
await withBrowser(async (page) => {
const res = await processChangeState(page, db, {
serviceNumber: job.serviceNumber,
reasonValue: job.reasonValue,
comment: job.comment,
dateStr: job.dateStr,
timeStr: job.timeStr
});
await markJobDone(db, job.id, res);
console.log(`✅ Job ${job.id} Completado.`);
});
} catch (err) {
console.error(`❌ Job ${job.id} Falló:`, err.message);
await markJobFailed(db, job.id, err);
}
}
function startWorker(db) {
const queue = [];
const run = async () => {
while(queue.length) { await processJob(db, await claimJobById(db, queue.shift())); }
};
db.collection(CONFIG.QUEUE_COLLECTION).where('status', '==', 'PENDING').onSnapshot(s => {
s.docChanges().forEach(c => { if(c.type==='added') { queue.push(c.doc.id); run(); } });
});
console.log('🚀 Worker Multiasistencia Estados (BÚSQUEDA XPATH) LISTO.');
}
const db = initFirebase();
startWorker(db);