From fcc55fc8d829c5ea6b8fa24dc7d80776f75d2b2e Mon Sep 17 00:00:00 2001 From: marsalva Date: Sun, 11 Jan 2026 08:42:58 +0000 Subject: [PATCH] Actualizar worker-multi-estado.js --- worker-multi-estado.js | 171 +++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 84 deletions(-) diff --git a/worker-multi-estado.js b/worker-multi-estado.js index 7fda9e8..a2d1709 100644 --- a/worker-multi-estado.js +++ b/worker-multi-estado.js @@ -1,4 +1,4 @@ -// worker-multi-estado.js (V13 - REDONDEO DE HORA + ANTI-BLOQUEO) +// worker-multi-estado.js (V14 - FINAL: SINTAXIS + IDs + ANTI-DUPLICADOS + REDONDEO) 'use strict'; const { chromium } = require('playwright'); @@ -11,47 +11,40 @@ const CONFIG = { 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 + RESCAN_SECONDS: 60, + DUPLICATE_TIME_MS: 3 * 60 * 1000 // 3 Minutos de memoria para ignorar duplicados }; -// --- DICCIONARIO DE TRADUCCIÓN --- +// --- MEMORIA ANTI-DUPLICADOS --- +const processedServicesCache = new Map(); + +// --- DICCIONARIO DE TRADUCCIÓN CORREGIDO --- const STATE_TRANSLATOR = { - "15": "2", // App 15 -> Web 2 (Visita) - "0": "34", // Web 34 (Rechazado) - "1": "1", // Web 1 (Contacto) - "99": "3" // Web 3 (Presupuesto) - "60": "13" // Web 3 (Presupuesto) + "15": "2", // App: 15 -> Web: 2 (Visita) + "0": "34", // App: 0 -> Web: 34 (Rechazado) + "1": "1", // App: 1 -> Web: 1 (Contacto) + "99": "3", // App: 99 -> Web: 3 (Presupuesto) + "60": "37" // App: 60 -> Web: 37 (Pendiente Instrucciones [60]) - CORREGIDO }; - // --- UTILS --- const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function toServerTimestamp() { return admin.firestore.FieldValue.serverTimestamp(); } -// Convertir HH:MM a segundos (para Multiasistencia) function timeToMultiValue(timeStr) { if (!timeStr) return ""; const [h, m] = timeStr.split(':').map(Number); return String((h * 3600) + (m * 60)); } -// NUEVO: Redondear hora a intervalos de 30 min (ej: 16:45 -> 17:00, 16:10 -> 16:00) +// Redondeo inteligente (16:45 -> 17:00) function roundToNearest30(timeStr) { if (!timeStr) return null; let [h, m] = timeStr.split(':').map(Number); - - if (m < 15) { - m = 0; - } else if (m < 45) { - m = 30; - } else { - m = 0; - h = (h + 1) % 24; // Pasar a la siguiente hora - } - - const hStr = String(h).padStart(2, '0'); - const mStr = String(m).padStart(2, '0'); - return `${hStr}:${mStr}`; + if (m < 15) { m = 0; } + else if (m < 45) { m = 30; } + else { m = 0; h = (h + 1) % 24; } + return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}`; } function extractTimeFromText(text) { @@ -118,7 +111,7 @@ async function loginMulti(page, db) { await page.waitForTimeout(4000); } -// --- HELPERS BROWSER --- +// --- PLAYWRIGHT SETUP --- async function withBrowser(fn) { const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] }); const context = await browser.newContext(); @@ -140,22 +133,24 @@ async function forceUpdate(elementHandle) { async function processChangeState(page, db, jobData) { const serviceNumber = jobData.serviceNumber; - // 1. MAPPING DE DATOS + // MAPPING let rawReason = jobData.reasonValue || jobData.nuevoEstado || jobData.estado; const comment = jobData.comment || jobData.observaciones || jobData.nota; const dateStr = normalizeDate(jobData.dateStr || jobData.fecha); - // Extracción y Redondeo de Hora + // HORA: Extraer y Redondear let rawTime = jobData.timeStr || extractTimeFromText(comment); - let timeStr = roundToNearest30(rawTime); // <--- AQUÍ ESTÁ LA MAGIA (16:45 -> 17:00) + let timeStr = roundToNearest30(rawTime); + // ID TRADUCIDO const reasonValue = STATE_TRANSLATOR[rawReason] || rawReason; - console.log(`🔧 DATOS: ID:${reasonValue} | Fecha:${dateStr} | Hora:${rawTime} -> Redondeada:${timeStr}`); + console.log(`🔧 PROCESANDO: App Estado "${rawReason}" -> Web ID "${reasonValue}"`); + console.log(`⏰ HORA: Original "${rawTime}" -> Redondeada "${timeStr}"`); if (!reasonValue || reasonValue === 'undefined') throw new Error("DATOS FALTANTES: No hay estado válido."); - // 2. NAVEGACIÓN + // NAVEGACIÓN await loginMulti(page, db); const targetUrl = `${CONFIG.MULTI_ACTION_BASE}?reparacion=${serviceNumber}&modo=0&navid=%2Fw3multi%2Ffrepasos_new.php%FDGET%FDrefresh%3D1%FC`; console.log(`📂 Abriendo servicio ${serviceNumber}...`); @@ -164,26 +159,25 @@ async function processChangeState(page, db, jobData) { await page.waitForSelector('select.answer-select', { timeout: 20000 }); await page.waitForTimeout(1000); - // 3. SELECCIONAR MOTIVO + // SELECCIONAR MOTIVO const reasonSel = page.locator('select.answer-select').first(); - const availableOptions = await reasonSel.evaluate(s => Array.from(s.options).map(o => o.value)); + const options = await reasonSel.evaluate(s => Array.from(s.options).map(o => o.value)); - if (!availableOptions.includes(String(reasonValue))) { - // Fallback inteligente: Si enviamos "2" pero no está, intentamos buscar texto - throw new Error(`MOTIVO INVÁLIDO: ID "${reasonValue}" no está disponible.`); + if (!options.includes(String(reasonValue))) { + throw new Error(`MOTIVO INVÁLIDO: El ID "${reasonValue}" no existe en el desplegable.`); } await reasonSel.selectOption(String(reasonValue)); await forceUpdate(await reasonSel.elementHandle()); - // 4. COMENTARIO + // COMENTARIO if (comment) { const commentBox = page.locator('textarea[formcontrolname="comment"]'); await commentBox.fill(comment); await forceUpdate(await commentBox.elementHandle()); } - // 5. FECHA SIGUIENTE ACCIÓN (CON HORA REDONDEADA) + // FECHA SIGUIENTE ACCIÓN (Solo si existe fecha y es válida) if (dateStr) { const actionBlock = page.locator('encastrables-date-hour-field[label="TXTFACCION"]'); if (await actionBlock.count() > 0) { @@ -195,28 +189,21 @@ async function processChangeState(page, db, jobData) { if (timeStr) { const seconds = timeToMultiValue(timeStr); const timeSel = actionBlock.locator('select.answer-select'); - - // Intento de selección seguro - try { - await timeSel.selectOption(seconds); - await forceUpdate(await timeSel.elementHandle()); - console.log(`⏰ Hora cita seleccionada: ${timeStr}`); - } catch(e) { - console.log(`⚠️ No se pudo seleccionar la hora ${timeStr}. Posiblemente no exista en el combo.`); - } + await timeSel.selectOption(seconds).catch(()=>{ console.log('⚠️ No se pudo poner la hora exacta') }); + await forceUpdate(await timeSel.elementHandle()); } } else { - // Fallback genérico + // Fallback const genDate = page.locator('input[type="date"]').first(); await genDate.fill(dateStr); await forceUpdate(await genDate.elementHandle()); } } - // 6. FECHA CONTACTO (AUTOMÁTICA) + // FECHA CONTACTO (AUTOMÁTICA - HOY) const contactBlock = page.locator('encastrables-date-hour-field[label="TXTFCONTACTO"]'); if (await contactBlock.count() > 0 && await contactBlock.isVisible()) { - console.log('📞 Rellenando contacto...'); + console.log('📞 Rellenando contacto (Auto Hoy)...'); const now = getCurrentDateTime(); const cDate = contactBlock.locator('input[type="date"]'); @@ -225,10 +212,8 @@ async function processChangeState(page, db, jobData) { const selects = contactBlock.locator('select.answer-select-time'); if (await selects.count() >= 2) { - // Hora await selects.nth(0).selectOption(now.hourStr).catch(()=>{}); await forceUpdate(await selects.nth(0).elementHandle()); - // Minutos await selects.nth(1).selectOption(now.minStr).catch(()=>{}); await forceUpdate(await selects.nth(1).elementHandle()); } @@ -236,25 +221,19 @@ async function processChangeState(page, db, jobData) { await page.waitForTimeout(2000); - // 7. GUARDAR (CON INTELIGENCIA DE REINTENTO) + // GUARDAR (CON INTELIGENCIA) const btn = page.locator('button.form-container-button-submit'); if (await btn.isDisabled()) { - console.log('⛔ Botón bloqueado. Reintentando activación...'); - // Truco: Click en comentario -> Tab -> Click fuera + console.log('⛔ Botón bloqueado. Reintentando inputs...'); await page.click('textarea[formcontrolname="comment"]'); await page.keyboard.press('Tab'); - await page.click('body'); - await page.waitForTimeout(1500); - - if (await btn.isDisabled()) { - throw new Error(`IMPOSIBLE GUARDAR: El formulario sigue bloqueado (probablemente falta un campo obligatorio o la hora no es válida).`); - } + await page.waitForTimeout(1000); + if (await btn.isDisabled()) throw new Error(`IMPOSIBLE GUARDAR: El formulario sigue bloqueado.`); } - console.log('💾 Guardando...'); await btn.click(); - // 8. ALERTAS + // ALERTAS 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()) { @@ -262,27 +241,24 @@ async function processChangeState(page, db, jobData) { await page.waitForTimeout(3000); } - // 9. RESULTADO + // RESULTADO const screenshot = (await page.screenshot({ fullPage: true, quality: 40, type: 'jpeg' })).toString('base64'); - const finalResult = await page.evaluate(() => { const successEl = document.querySelector('.form-container-success, .bg-success'); const errorEl = document.querySelector('.form-container-error, .bg-danger'); - if (successEl) return { type: 'OK', text: successEl.innerText.trim() }; if (errorEl) return { type: 'ERROR', text: errorEl.innerText.trim() }; - const bodyText = document.body.innerText; - if (bodyText.includes('correctamente') || bodyText.includes('guardado')) return { type: 'OK', text: "Guardado correctamente." }; - - return { type: 'UNKNOWN', text: "No se detectó mensaje." }; + const body = document.body.innerText; + if (body.includes('correctamente') || body.includes('guardado')) return { type: 'OK', text: "Guardado correctamente." }; + return { type: 'UNKNOWN', text: "No se detectó mensaje explícito." }; }); console.log(`🏁 FINAL: ${finalResult.type} - ${finalResult.text}`); return { success: finalResult.type === 'OK', message: finalResult.text, screenshot }; } -// --- GESTIÓN DE COLAS --- +// --- GESTIÓN DE COLAS Y TRANSACCIONES --- async function claimJobById(db, jobId) { const ref = db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId); return await db.runTransaction(async (tx) => { @@ -293,28 +269,24 @@ async function claimJobById(db, jobId) { }); } -async function markJobDone(db, job, result) { - const jobId = job.id; - await db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId).set({ status: 'DONE', result }, { merge: true }); +async function markJobDone(db, job, result, status = 'DONE') { + await db.collection(CONFIG.QUEUE_COLLECTION).doc(job.id).set({ status, result }, { merge: true }); await db.collection(CONFIG.RESULT_COLLECTION).add({ - jobId, ok: true, serviceNumber: job.serviceNumber || '', - reason: job.nuevoEstado || '', - comment: job.observaciones || '', + jobId: job.id, serviceNumber: job.serviceNumber || '', + reason: job.nuevoEstado || '', comment: job.observaciones || '', ...result, createdAt: toServerTimestamp() }); } async function markJobFailed(db, job, err) { - const jobId = job.id; - await db.collection(CONFIG.QUEUE_COLLECTION).doc(jobId).set({ status: 'FAILED', error: { message: err.message } }, { merge: true }); + await db.collection(CONFIG.QUEUE_COLLECTION).doc(job.id).set({ status: 'FAILED', error: { message: err.message } }, { merge: true }); await db.collection(CONFIG.RESULT_COLLECTION).add({ - jobId, ok: false, serviceNumber: job.serviceNumber || '', error: err.message, createdAt: toServerTimestamp() + jobId: job.id, ok: false, serviceNumber: job.serviceNumber || '', error: err.message, createdAt: toServerTimestamp() }); } async function processJob(db, job) { if (!job) return; - console.log(`>>> Procesando Job: ${job.id}`); try { await withBrowser(async (page) => { const res = await processChangeState(page, db, job); @@ -327,19 +299,50 @@ async function processJob(db, job) { } } +// --- LOOP PRINCIPAL (CON ANTI-DUPLICADOS) --- function startWorker(db) { const queue = []; - + const processing = new Set(); + const run = async () => { - while(queue.length) { await processJob(db, await claimJobById(db, queue.shift())); } + while(queue.length) { + const jobId = queue.shift(); + if (processing.has(jobId)) continue; + processing.add(jobId); + + try { + const jobData = await claimJobById(db, jobId); + if (!jobData) { processing.delete(jobId); continue; } + + // FILTRO MEMORIA (3 MINUTOS) + const serviceNum = jobData.serviceNumber; + const lastTime = processedServicesCache.get(serviceNum); + const now = Date.now(); + + if (lastTime && (now - lastTime < CONFIG.DUPLICATE_TIME_MS)) { + console.log(`🚫 DUPLICADO: Servicio ${serviceNum} reciente. Ignorando.`); + await markJobDone(db, jobData, { success: true, message: "Ignorado por duplicado" }, 'SKIPPED'); + continue; + } + processedServicesCache.set(serviceNum, now); // Actualizar memoria + + console.log(`>>> Procesando Job: ${jobId}`); + await processJob(db, jobData); + + } catch (err) { + console.error(`Error loop: ${err.message}`); + } finally { + processing.delete(jobId); + } + } }; db.collection(CONFIG.QUEUE_COLLECTION).where('status', '==', 'PENDING').onSnapshot(s => { s.docChanges().forEach(c => { - if(c.type === 'added') { queue.push(c.doc.id); run(); } + if(c.type === 'added' && !queue.includes(c.doc.id)) { queue.push(c.doc.id); run(); } }); }); - console.log('🚀 Worker Multiasistencia (V13 - HORA REDONDEADA) LISTO.'); + console.log('🚀 Worker Multiasistencia (V14 - MASTER) LISTO.'); } const db = initFirebase();