estados-multi/worker-multi-estado.js

270 lines
9.9 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 (V5 - CON FOTO DE DIAGNÓSTICO)
'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(); }
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(() => {}); }
}
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
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
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 {
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 bloqueado.`);
}
}
console.log('💾 Click en Guardar...');
await btn.click();
// 8. GESTIÓN DE ALERTAS (EL MOMENTO CRÍTICO)
// Esperamos más tiempo para asegurar que el popup carga
await page.waitForTimeout(3000);
const confirmBtn = page.locator('button.form-container-button-submit-toast').filter({ hasText: 'Sí' });
if (await confirmBtn.count() > 0 && await confirmBtn.isVisible()) {
console.log('🚨 ALERTA SMS DETECTADA. Pulsando "Sí"...');
await confirmBtn.click();
// ¡OJO! Esperar a que el botón DESAPAREZCA (Confirmación de que el click funcionó)
try {
await confirmBtn.waitFor({ state: 'hidden', timeout: 5000 });
console.log('✅ Botón "Sí" ha desaparecido (Click exitoso).');
} catch(e) {
console.log('⚠️ El botón "Sí" sigue ahí. Forzando segundo click...');
await confirmBtn.click(); // Re-intentar
}
// Espera larga para el guardado real del servidor
await page.waitForTimeout(5000);
} else {
console.log(' No saltó la alerta de SMS (o ya se procesó).');
}
// 9. VERIFICACIÓN FINAL + CAPTURA DE PANTALLA
// Hacemos una captura ahora mismo para ver qué demonios hay en la pantalla
const screenshotBuffer = await page.screenshot({ fullPage: true, quality: 50, type: 'jpeg' });
const screenshotBase64 = screenshotBuffer.toString('base64');
// Analizamos texto
const finalResult = await page.evaluate(() => {
const successEl = document.querySelector('.form-container-success') || document.querySelector('.bg-success');
const errorEl = document.querySelector('.form-container-error') || document.querySelector('.bg-danger');
if (successEl && successEl.innerText.length > 2) return { type: 'OK', text: successEl.innerText };
if (errorEl && errorEl.innerText.length > 2) return { type: 'ERROR', text: errorEl.innerText };
return { type: 'UNKNOWN', text: document.body.innerText.substring(0, 300) };
});
console.log(`🏁 RESULTADO: [${finalResult.type}]`);
// Devolvemos el resultado y LA FOTO
return {
success: (finalResult.type === 'OK' || finalResult.text.includes('correctamente')),
message: finalResult.text,
screenshot: screenshotBase64 // <-- LA PRUEBA DEL DELITO
};
}
// --- 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) {
// Guardamos la foto en el log
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, // Aquí va la screenshot
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 (V5 - CON FOTO) LISTO.');
}
const db = initFirebase();
startWorker(db);