estados-multi/worker-multi-estado.js

272 lines
10 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
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: Formulario bloqueado.`);
}
}
console.log('💾 Click en Guardar...');
await btn.click();
// 8. GESTIÓN DE ALERTAS Y CONFIRMACIÓN
console.log('👀 Esperando mensajes o confirmaciones...');
// Esperamos un poco para que salga el Popup si tiene que salir
await page.waitForTimeout(2000);
// Buscamos el botón "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 SMS DETECTADA. Pulsando "Sí"...');
await confirmBtn.click();
// Esperamos a que la acción se procese tras el "Sí"
await page.waitForTimeout(3000);
} else {
console.log(' No hubo alerta de SMS (o se procesó rápido).');
}
// 9. VERIFICACIÓN FINAL (LO QUE PEDISTE)
// Esperamos explícitamente a que aparezca un mensaje de éxito o error
try {
await page.waitForSelector('.form-container-success, .form-container-error, encastrables-success-error-message', { timeout: 10000 });
} catch (e) {
console.log("⏳ Tiempo de espera agotado buscando mensaje. Analizando pantalla...");
}
// Leemos el texto exacto
const finalResult = await page.evaluate(() => {
// Selector verde (Exito)
const successEl = document.querySelector('.form-container-success') || document.querySelector('.bg-success');
// Selector rojo (Error)
const errorEl = document.querySelector('.form-container-error') || document.querySelector('.bg-danger');
// Selector genérico
const msgEl = document.querySelector('encastrables-success-error-message');
const infoMsg = document.querySelector('.form-container-message-info');
if (successEl && successEl.innerText.length > 2) return { type: 'OK', text: successEl.innerText };
if (errorEl && errorEl.innerText.length > 2) return { type: 'ERROR', text: errorEl.innerText };
if (msgEl && msgEl.innerText.length > 2) return { type: 'INFO', text: msgEl.innerText };
if (infoMsg && infoMsg.innerText.length > 2) return { type: 'INFO', text: infoMsg.innerText };
return { type: 'UNKNOWN', text: document.body.innerText.substring(0, 200) }; // Fallback
});
console.log(`🏁 RESULTADO FINAL: [${finalResult.type}] "${finalResult.text.replace(/\n/g, ' ')}"`);
// Validamos según tu petición
if (finalResult.type === 'OK' || finalResult.text.includes('correctamente')) {
return { success: true, message: finalResult.text };
} else if (finalResult.type === 'ERROR') {
throw new Error(`WEB ERROR: ${finalResult.text}`);
} else {
// Si no es ni verde ni rojo claro, devolvemos lo que vemos para que tú juzgues
return { success: true, warning: `No vi mensaje verde, pero tampoco rojo. Pantalla dice: ${finalResult.text.substring(0,100)}...` };
}
}
// --- 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: ${res.message || 'OK'}`);
});
} 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 (V4 - VERIFICACION ESTRICTA) LISTO.');
}
const db = initFirebase();
startWorker(db);