estados-multi/worker-multi-estado.js

256 lines
9.5 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);
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();
}
// --- HELPERS AVANZADOS ---
// Esta función es la clave: obliga a la web a reconocer el cambio
async function triggerEvents(page, selector) {
await page.evaluate((sel) => {
const el = document.querySelector(sel);
if (el) {
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
}
}, selector);
}
// --- 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(() => {}); }
}
// --- 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 ${serviceNumber}...`);
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60000 });
// Esperar formulario
await page.waitForSelector('select.answer-select', { timeout: 20000 });
await page.waitForTimeout(2000); // Esperar a que Angular "se asiente"
console.log('📝 Rellenando formulario (Modo Fuerza Bruta)...');
// 3. MOTIVO (SELECT)
// Seleccionamos y forzamos el evento 'change'
const reasonSel = 'select.answer-select';
await page.selectOption(reasonSel, String(reasonValue));
await triggerEvents(page, reasonSel);
await page.waitForTimeout(500);
// 4. COMENTARIO
if (comment) {
const commentSel = 'textarea[formcontrolname="comment"]';
await page.fill(commentSel, comment);
await triggerEvents(page, commentSel);
}
// 5. FECHA
if (dateStr) {
const dateSel = 'input[type="date"]';
await page.fill(dateSel, dateStr);
await triggerEvents(page, dateSel);
// Truco: hacer click fuera para validar
await page.click('body');
}
// 6. HORA
if (timeStr) {
const secondsValue = timeToMultiValue(timeStr);
// Buscamos el select de hora. Puede ser el segundo select de la página.
// Usamos una estrategia de búsqueda por valor para ser precisos.
const foundTime = await page.evaluate((val) => {
const selects = Array.from(document.querySelectorAll('select'));
for (const s of selects) {
// Si tiene la opción con ese valor (ej: 28800)
if (s.querySelector(`option[value="${val}"]`)) {
s.value = val;
s.dispatchEvent(new Event('change', { bubbles: true }));
s.dispatchEvent(new Event('blur', { bubbles: true }));
return true;
}
}
return false;
}, secondsValue);
if (!foundTime) console.log(`⚠️ Advertencia: No encontré donde poner la hora ${timeStr}`);
}
await page.waitForTimeout(2000);
// 7. GUARDAR - VERIFICACIÓN DE ESTADO DEL BOTÓN
const btnSelector = 'button.form-container-button-submit';
const btn = page.locator(btnSelector);
// Comprobar si está habilitado
if (await btn.isDisabled()) {
console.log('⛔ El botón Guardar sigue deshabilitado. Intentando "despertar" el formulario...');
// Intentar hacer focus/blur en el comentario otra vez
await page.focus('textarea[formcontrolname="comment"]');
await page.keyboard.press('Tab');
await page.waitForTimeout(1000);
if (await btn.isDisabled()) {
throw new Error("IMPOSIBLE GUARDAR: El formulario no valida los datos (botón gris). Revisa si la fecha/hora son correctas.");
}
}
console.log('💾 Click en Guardar...');
await btn.click();
// 8. VERIFICAR RESULTADO
// Esperamos a ver si sale mensaje de éxito o error
await page.waitForTimeout(3000);
// Buscamos mensajes en la pantalla
const message = await page.evaluate(() => {
// En tu HTML vi esta etiqueta: <encastrables-success-error-message>
const msgEl = document.querySelector('encastrables-success-error-message');
const errorEl = document.querySelector('.form-container-error');
const successEl = document.querySelector('.form-container-success');
if (errorEl) return `ERROR WEB: ${errorEl.innerText}`;
if (successEl) return `EXITO WEB: ${successEl.innerText}`;
if (msgEl && msgEl.innerText.trim().length > 0) return `MSG: ${msgEl.innerText}`;
return null;
});
if (message && message.includes('ERROR')) {
throw new Error(message);
}
console.log(` Resultado pantalla: ${message || 'Sin mensaje explícito (probablemente OK)'}`);
// Si la URL cambió, es buena señal
const currentUrl = page.url();
return { success: true, finalUrl: currentUrl, webMessage: message };
}
// --- WORKER LOOP ---
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 (FUERZA BRUTA) LISTO.');
}
const db = initFirebase();
startWorker(db);