estados-multi/worker-multi-estado.js

254 lines
9.3 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();
}
// --- 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 ${serviceNumber}...`);
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...');
// 3. 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 (Usamos .first() para evitar error de doble fecha)
if (dateStr) {
const dateInput = page.locator('input[type="date"]').first();
await dateInput.fill(dateStr);
await forceUpdate(await dateInput.elementHandle());
await page.click('body');
}
// 6. HORA (Usamos XPath para buscar por valor)
if (timeStr) {
const secondsValue = timeToMultiValue(timeStr);
const timeSelectHandle = await page.$(`xpath=//select[.//option[@value="${secondsValue}"]]`);
if (timeSelectHandle) {
await timeSelectHandle.selectOption(secondsValue);
await forceUpdate(timeSelectHandle);
} else {
// Fallback
const allSelects = await page.$$('select');
if (allSelects.length > 1) {
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');
if (await btn.isDisabled()) {
console.log('⛔ Botón deshabilitado. Intentando reactivar...');
await page.click('textarea[formcontrolname="comment"]');
await page.keyboard.press('Tab');
await page.waitForTimeout(1000);
if (await btn.isDisabled()) {
throw new Error(`IMPOSIBLE GUARDAR: El formulario sigue bloqueado.`);
}
}
console.log('💾 Click en Guardar...');
await btn.click();
// --- 8. DETECCIÓN DE POPUP "SÍ" (NUEVO CÓDIGO) ---
// Esperamos 2.5s para ver si salta la alerta roja
await page.waitForTimeout(2500);
// Buscamos el botón usando la clase específica que me diste y el texto "Sí"
const confirmBtn = page.locator('button.form-container-button-submit-toast').filter({ hasText: 'Sí' });
if (await confirmBtn.count() > 0 && await confirmBtn.isVisible()) {
console.log('🚨 ¡ALERTA DETECTADA! (Mensaje informativo de SMS).');
console.log('👉 Pulsando botón "Sí"...');
await confirmBtn.click();
await page.waitForTimeout(3000); // Esperar guardado real
} else {
console.log(' No apareció alerta de SMS (o ya se guardó).');
}
// ----------------------------------------------------
// 9. VERIFICAR RESULTADO FINAL
await page.waitForTimeout(2000);
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 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 (V3 - CONFIRMACIÓN SÍ) LISTO.');
}
const db = initFirebase();
startWorker(db);