// ==UserScript== // @name DOC_NRAP & AB_NRAP portale trasporto v4.5 WORKING. // @namespace http://tampermonkey.net/ // @version 4.5 // @description Automazione DOC_NRAP e AB_NRAP con gestione modale di conferma // @author Tu // @match *://www.ilportaledeltrasporto.it/nove/gestioneistanzanove/spa/* // @grant GM_xmlhttpRequest // @connect localhost // @run-at document-end // ==/UserScript== (function() { 'use strict'; // --- VARIABILI DI STATO --- let TELAIO = ''; let VALORE2 = '0'; let VALORE3 = ''; let TIPO_ATTUALE = ''; let taskAttivo = false; let downloadInCorso = false; let completatoLocalmente = false; const ENDPOINT_GET = 'http://localhost/hook/dati2.php'; // --- HELPER FUNCTIONS --- // Funzione per discriminare lo stato delle schede/tessere function getTessereStatus() { let stati = { Tess1: "Sconosciuto", Tess2: "Sconosciuto", Tess3: "Sconosciuto" }; const cardContainers = document.querySelectorAll('.CardComponent.d-flex.col'); // Se non ci sono tessere, esci silenziosamente senza fare log di errore if (cardContainers.length === 0) { return null; // Restituiamo null per capire che non siamo nella dashboard } cardContainers.forEach((container, index) => { const article = container.querySelector('article.Card'); const h2 = container.querySelector('.cardText h2'); if (article && h2) { const titolo = h2.innerText.trim().toLowerCase(); let statoRilevato = "Sconosciuto"; // Verifica lo stato dalle classi dell'elemento article if (article.classList.contains('Active')) statoRilevato = "Attivo"; else if (article.classList.contains('Locked')) statoRilevato = "Bloccato"; else if (article.classList.contains('Completed')) statoRilevato = "Completato"; // Mappatura precisa basata sul testo dell'H2 if (titolo.includes("verifica documentazione")) { stati.Tess1 = statoRilevato; } else if (titolo.includes("visita e prova")) { stati.Tess2 = statoRilevato; } else if (titolo.includes("abilitazione telaio")) { stati.Tess3 = statoRilevato; } // console.log(`[TM] Rilevata Tessera: "${h2.innerText.trim()}" | Stato: ${statoRilevato}`); } }); return stati; } function getTessereStatus_old() { let stati = { Tess1: "Sconosciuto", Tess2: "Sconosciuto", Tess3: "Sconosciuto" }; // Seleziona i contenitori basandosi sulla classe che hai indicato const cardContainers = document.querySelectorAll('.CardComponent.d-flex.col'); if (cardContainers.length === 0) { console.warn("[TM] Nessun contenitore CardComponent trovato."); return stati; } cardContainers.forEach((container, index) => { const article = container.querySelector('article.Card'); const h2 = container.querySelector('.cardText h2'); if (article && h2) { const titolo = h2.innerText.trim().toLowerCase(); let statoRilevato = "Sconosciuto"; // Verifica lo stato dalle classi dell'elemento article if (article.classList.contains('Active')) statoRilevato = "Attivo"; else if (article.classList.contains('Locked')) statoRilevato = "Bloccato"; else if (article.classList.contains('Completed')) statoRilevato = "Completato"; // Mappatura precisa basata sul testo dell'H2 if (titolo.includes("verifica documentazione")) { stati.Tess1 = statoRilevato; } else if (titolo.includes("visita e prova")) { stati.Tess2 = statoRilevato; } else if (titolo.includes("abilitazione telaio")) { stati.Tess3 = statoRilevato; } // console.log(`[TM] Rilevata Tessera: "${h2.innerText.trim()}" | Stato: ${statoRilevato}`); } }); return stati; } // Pulizia stringhe: rimuove ogni spazio (inizio, fine, centro) e porta in maiuscolo function cleanString(str) { if (!str) return ""; return str.toString().replace(/\s+/g, '').toUpperCase(); } // Click sicuro per elementi React function clickReactSafe(element) { if (!element || typeof element.dispatchEvent !== 'function') return; const opts = { bubbles: true, cancelable: true, view: window.unsafeWindow || window }; try { element.dispatchEvent(new MouseEvent('mousedown', opts)); element.dispatchEvent(new MouseEvent('mouseup', opts)); element.click(); } catch (e) { element.dispatchEvent(new Event('mousedown', { bubbles: true })); element.dispatchEvent(new Event('mouseup', { bubbles: true })); element.click(); } } /** * Premere il pulsante "Procedi" della tessera ennesima (1-based). * @param {number} n - Indice della tessera (1 per la prima, 2 per la seconda, ecc.) */ function Procedi(n) { console.log(`[TM] Test Procedi(${n}) avviato...`); // CORREZIONE: Le classi devono essere unite dai punti senza spazi // perché appartengono allo stesso elemento div const cardContainers = document.querySelectorAll('.CardComponent.d-flex.col'); if (cardContainers.length === 0) { console.error("[TM] ERRORE: Nessun contenitore trovato. Verificare il selettore .CardComponent.d-flex.col"); return; } console.log(`[TM] Tessere trovate in pagina: ${cardContainers.length}`); // Controllo indice if (n < 1 || n > cardContainers.length) { console.error(`[TM] ERRORE: Indice ${n} non valido. Tessere disponibili: ${cardContainers.length}`); return; } // Selezioniamo la tessera corretta (n-1) const targetCard = cardContainers[n - 1]; console.log(`[TM] Opero sulla tessera #${n}:`, targetCard.querySelector('h2')?.innerText); // Cerchiamo il pulsante Procedi // Nota: nell'HTML inviato la classe del footer è "cardFooter" (minuscolo/maiuscolo come nel DOM) const btnProcedi = Array.from(targetCard.querySelectorAll('button')) .find(b => b.textContent.trim().toUpperCase() === 'PROCEDI'); if (!btnProcedi) { console.error(`[TM] Pulsante 'Procedi' non trovato nella tessera #${n}.`); return; } if (btnProcedi.disabled) { console.warn(`[TM] Il pulsante della tessera #${n} è DISABILITATO.`); return; } console.log(`[TM] Click sulla tessera #${n}...`); clickReactSafe(btnProcedi); }; // Input sicuro per elementi React function setInputReactSafe(element, value) { if (!element) return; // Determina se è un input o una textarea per usare il prototipo corretto const prototype = element.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const setter = Object.getOwnPropertyDescriptor(prototype, 'value').set; element.focus(); setter.call(element, value); // Trigger degli eventi per far capire a React che il testo è cambiato element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); element.dispatchEvent(new Event('blur', { bubbles: true })); } function setInputReactSafe_old(input, value) { if (!input) return; const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; input.focus(); setter.call(input, value); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } // Gestione della modale di conferma per AB_NRAP function gestisciModaleConferma() { const modale = document.querySelector('.ModalsPortal__container.mx-auto'); if (modale) { const btnConfermaModale = Array.from(modale.querySelectorAll('button')) .find(b => { const spans = b.querySelectorAll('span'); return Array.from(spans).some(s => s.textContent.trim() === "Conferma"); }); if (btnConfermaModale) { console.log("[TM] Pulsante 'Conferma' trovato nella modale. Clicco..."); clickReactSafe(btnConfermaModale); // Attendiamo che la modale si chiuda e il sistema elabori prima di segnare fine task setTimeout(segnaComeCompletato, 3000); return true; } } return false; } // --- LOGICA SERVER --- function avviaDownloadDocumenti() { if (downloadInCorso) return; const tuttiIBottoni = Array.from(document.querySelectorAll('button.DocumentsSection__file-btn')); if (tuttiIBottoni.length === 0) return; downloadInCorso = true; const titoliGiaVisti = new Set(); let delayDoc = 0; tuttiIBottoni.forEach((btn, index, array) => { const riga = btn.closest('tr'); const titolo = riga ? riga.innerText.trim().split('\t')[0] : btn.innerText.trim(); if (!titoliGiaVisti.has(titolo)) { titoliGiaVisti.add(titolo); setTimeout(() => { console.log(`[TM] 1. Avvio Download: ${titolo}`); clickReactSafe(btn); // --- SEQUENZA MODALE --- // STEP A: Click Esporta File (dopo 1.2 secondi) setTimeout(() => { const btnEsporta = Array.from(document.querySelectorAll('button.btn-primary.btn-xs')); if (btnEsporta) { console.log(`[TM] 2. Click Esporta File per: ${titolo}`); clickReactSafe(btnEsporta); } // STEP B: Click Chiudi Modale (dopo altri 1.5 secondi dal click esporta) setTimeout(() => { const btnChiudiModale = document.querySelector('button.ModalContent__header--btn'); if (btnChiudiModale) { console.log(`[TM] 3. Chiusura modale per: ${titolo}`); clickReactSafe(btnChiudiModale); } }, 1500); }, 1200); // ----------------------- if (index === array.length - 1) { setTimeout(segnaComeCompletato, 5000); } }, delayDoc); delayDoc += 3000; } }); } function fetchDati() { GM_xmlhttpRequest({ method: "GET", url: ENDPOINT_GET, onload: function(response) { try { const data = JSON.parse(response.responseText); const targetHome = 'https://www.ilportaledeltrasporto.it/nove/gestioneistanzanove/spa/le-mie-istanze'; // Cerca questo blocco dentro fetchDati() e sostituiscilo: if (data.tipo === 'RESET_NRAP') { console.log("[TM] Ricevuto comando RESET. Fermo i task e avviso il server..."); // 1. Impostiamo lo stato su RESET così segnaComeCompletato() sa cosa inviare TIPO_ATTUALE = 'RESET_NRAP'; TELAIO = ''; VALORE2 = '0'; VALORE3 = ''; // 2. Chiamiamo la funzione che già possiedi per notificare il server segnaComeCompletato(); // 3. Opzionale: Torna in home solo se non ci sei già if (window.location.href !== targetHome) { window.location.href = targetHome; } return; } if ((data.tipo === 'DOC_NRAP' || data.tipo === 'AB_NRAP') && data.task === 'fare') { if (TELAIO !== '' && TELAIO !== data.valore1) { TELAIO = data.valore1; window.location.href = targetHome; return; } if (TELAIO !== data.valore1 || TIPO_ATTUALE !== data.tipo) { TELAIO = data.valore1; VALORE2 = data.valore2; VALORE3 = data.valore3; TIPO_ATTUALE = data.tipo; completatoLocalmente = false; console.log(`[TM] Ricevuto task ${TIPO_ATTUALE} per: ${TELAIO}`); } if (!completatoLocalmente) { taskAttivo = true; eseguiAutomazione(); } // Dentro l'else di fetchDati (quando data.task NON è 'fare') } else { if (taskAttivo) { console.log("[TM] Nessun task attivo dal server, rilascio il controllo manuale."); } taskAttivo = false; completatoLocalmente = false; // Rimuovi o commenta questa riga se vuoi evitare loop continui } } catch (e) { console.error("[TM] Errore parsing dati", e); } } }); } function segnaComeCompletato() { if (completatoLocalmente) return; completatoLocalmente = true; taskAttivo = false; const payload = { tipo: TIPO_ATTUALE, task: "COMPLETATO", valore1: TELAIO, valore2: VALORE2, valore3: VALORE3 }; GM_xmlhttpRequest({ method: "POST", url: ENDPOINT_GET, data: JSON.stringify(payload), headers: { "Content-Type": "application/json" }, onload: function() { console.log(`[TM] Task ${TIPO_ATTUALE} completato sul server.`); downloadInCorso = false; } }); } // --- AUTOMAZIONE PAGINE --- function eseguiAutomazione() { // Se il task non è attivo o abbiamo già finito su questa pagina, esci subito if (!taskAttivo || completatoLocalmente) return; const url = window.location.href; // --- 1. Pagina di Ricerca --- if (url.includes('le-mie-istanze')) { const input = document.getElementById('numeroTelaio'); const btnCerca = Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Cerca')); const btnTabella = document.querySelector('button.action-button-table'); if (input && TELAIO !== '' && cleanString(input.value) !== cleanString(TELAIO)) { setInputReactSafe(input, TELAIO); setTimeout(() => { if(btnCerca) clickReactSafe(btnCerca); }, 800); } if (btnTabella) setTimeout(() => clickReactSafe(btnTabella), 1500); return; // Fine logica ricerca } // --- 2. Gestione Dashboard vs Dettaglio --- // Recuperiamo lo stato delle tessere. getTessereStatus() ora deve restituire null // se non trova i contenitori (come spiegato prima), evitando log di errore. const situazione = getTessereStatus(); // CASO A: Siamo nella Dashboard (Tessere visibili) if (situazione !== null && !url.includes('dettaglio-istanza') && !url.includes('abilitazione-telaio')) { const btnProcedi = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Procedi'); if (btnProcedi && !btnProcedi.disabled) { if (TIPO_ATTUALE === 'DOC_NRAP' && situazione.Tess1 === "Attivo") { console.log("[TM] Dashboard: Apro Tessera 1 (Documentazione)"); setTimeout(() => { Procedi(1); }, 1000); } else if (TIPO_ATTUALE === 'AB_NRAP' && situazione.Tess3 === "Attivo") { console.log("[TM] Dashboard: Apro Tessera 3 (Abilitazione)"); setTimeout(() => { Procedi(3); }, 1000); } } return; // Esci: abbiamo gestito la dashboard } // CASO B: Siamo nella Pagina di Dettaglio/Compilazione (Abilitazione o Download) if (url.includes('dettaglio-istanza') || url.includes('abilitazione-telaio')) { // Sotto-caso: Compilazione Dati (AB_NRAP) if (TIPO_ATTUALE === 'AB_NRAP') { const inputCodImmat = document.getElementById('Codice immatricolazione'); const inputNota = document.getElementById('Nota*') || document.getElementById('Nota'); if (inputCodImmat && inputNota) { console.log("[TM] Pagina compilazione rilevata. Inserisco dati..."); // Inserimento dati con logica React setInputReactSafe(inputCodImmat, VALORE2); setInputReactSafe(inputNota, VALORE3); // Forza il focus out per "fissare" il valore in React inputCodImmat.dispatchEvent(new Event('blur', { bubbles: true })); inputNota.dispatchEvent(new Event('blur', { bubbles: true })); console.log("[TM] Compilazione eseguita. Ora l'Observer si ferma."); // BLOCCA L'OBSERVER: impostando a true, questa intera funzione // si fermerà al primo IF nei cicli successivi. completatoLocalmente = true; } } // Sotto-caso: Download Documenti (DOC_NRAP) else if (TIPO_ATTUALE === 'DOC_NRAP') { if (downloadInCorso) return; if (parseInt(VALORE2) > 0) { console.log("[TM] Pagina documenti rilevata. Avvio download..."); avviaDownloadDocumenti(); } else { console.log("[TM] Nessun documento richiesto (Valore2=0). Chiudo."); segnaComeCompletato(); } } } } function eseguiAutomazione_old() { if (!taskAttivo || completatoLocalmente) return; const url = window.location.href; // 1. Pagina di Ricerca if (url.includes('le-mie-istanze')) { const input = document.getElementById('numeroTelaio'); const btnCerca = Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Cerca')); const btnTabella = document.querySelector('button.action-button-table'); if (input && TELAIO !== '' && cleanString(input.value) !== cleanString(TELAIO)) { setInputReactSafe(input, TELAIO); setTimeout(() => { if(btnCerca) clickReactSafe(btnCerca); }, 800); } if (btnTabella) setTimeout(() => clickReactSafe(btnTabella), 1500); //if (btnTabella) console.log("Pulsante Tabella trovato! esegui il comando a mano"); } // --- 2. Pagina Dashboard (Tessere) --- if (url.includes('/abilitazione/') && !url.includes('dettaglio-istanza')) { const situazione = getTessereStatus(); const btnProcedi = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Procedi'); if (btnProcedi && !btnProcedi.disabled) { if (TIPO_ATTUALE === 'DOC_NRAP' && situazione.Tess1 === "Attivo") { setTimeout(() => { Procedi(1); }, 1000); } else if (TIPO_ATTUALE === 'AB_NRAP' && situazione.Tess3 === "Attivo") { setTimeout(() => { Procedi(3); }, 1000); } } } // --- 3. Pagina di Abilitazione (Dettaglio) --- if (url.includes('/abilitazione/') && url.includes('/abilitazione-telaio')) { if (TIPO_ATTUALE === 'AB_NRAP') { const inputCodImmat = document.getElementById('Codice immatricolazione'); // Cerca la nota sia con che senza asterisco const inputNota = document.getElementById('Nota*') || document.getElementById('Nota'); console.log("[TM] Pagina di abilitazione rilevata. Compilo..."); if (inputCodImmat && inputNota) { console.log("[TM] Pagina di abilitazione rilevata. Compilo..."); //setInputReactSafe(inputCodImmat, VALORE2); //setInputReactSafe(inputNota, VALORE3); console.log("[TM] Dati inseriti. Ora mi fermo e ti lascio il controllo."); // IMPORTANTE: Segniamo come completato localmente solo la "scrittura" // così l'Observer smette di chiamare eseguiAutomazione() per questa pagina. completatoLocalmente = true; } } if (TIPO_ATTUALE === 'DOC_NRAP') { if (downloadInCorso) return; if (parseInt(VALORE2) > 0) { avviaDownloadDocumenti(); } else { segnaComeCompletato(); } } } } function eseguiAutomazione2() { if (!taskAttivo || completatoLocalmente) return; const situazione = getTessereStatus(); // GUARDIA: Se tutte le tessere sono "Bloccato" o "Sconosciuto", // probabilmente React sta ancora caricando. Fermati e aspetta il prossimo ciclo. if (situazione.Tess1 === "Bloccato" && situazione.Tess2 === "Bloccato") { return; } const url = window.location.href; // 1. Pagina di Ricerca if (url.includes('le-mie-istanze')) { const input = document.getElementById('numeroTelaio'); const btnCerca = Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Cerca')); const btnTabella = document.querySelector('button.action-button-table'); if (input && TELAIO !== '' && cleanString(input.value) !== cleanString(TELAIO)) { setInputReactSafe(input, TELAIO); setTimeout(() => { if(btnCerca) clickReactSafe(btnCerca); }, 800); } if (btnTabella) setTimeout(() => clickReactSafe(btnTabella), 1500); //if (btnTabella) console.log("Pulsante Tabella trovato! esegui il comando a mano"); } // 2. Pagina Intermedia (Abilitazione / Dashboard) if (url.includes('/abilitazione/') && !url.includes('dettaglio-istanza')) { const btnProcedi = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Procedi'); if (btnProcedi && !btnProcedi.disabled) { // --- LOGICA SELETTIVA --- if (TIPO_ATTUALE === 'DOC_NRAP') { console.log("Stato tessere al click:", situazione); if (situazione.Tess1 === "Attivo" && situazione.Tess3 === "Bloccato") { console.log("[TM] DOC_NRAP: Tessera 1 pronta. Procedo apertura documenti..."); setTimeout(() => { Procedi(1); }, 1000); } else if (situazione.Tess1 === "Completato"){ console.warn("[TM] DOC_NRAP: Vista documentazione"); } else if (situazione.Tess1 === "Bloccato"){ console.warn("[TM] DOC_NRAP: Documentazione bloccata, attendere"); } } else if (TIPO_ATTUALE === 'AB_NRAP') { // Logica specifica per AB_NRAP (es. solo se Tess3 è Attiva) if (situazione.Tess3 === "Attivo") { console.log("[TM] DOC_NRAP: Tessera 3 Ok si procede a validazione..."); setTimeout(() => { Procedi(3); }, 1000); } } } } // 3. Pagina Dettaglio Istanza if (url.includes('dettaglio-istanza')) { const situazioneInDettaglio = getTessereStatus(); // CASO A: Operazione AB_NRAP (Solo inserimento dati, NO clic automatico) if (TIPO_ATTUALE === 'AB_NRAP') { const inputCodImmat = document.getElementById('Codice immatricolazione'); const inputNota = document.getElementById('Nota*'); let datiInseriti = false; // 1. Inserimento Codice Immatricolazione (Valore2) if (inputCodImmat.value !== VALORE2) { console.log("[TM] Inserisco Codice Immatricolazione:", VALORE2); setInputReactSafe(inputCodImmat, VALORE2); datiInseriti = true; } // 2. Inserimento Nota (Valore3) if (inputNota.value !== VALORE3) { console.log("[TM] Inserisco Nota:", VALORE3); setInputReactSafe(inputNota, VALORE3); datiInseriti = true; } // 3. Feedback visivo e stop if (datiInseriti) { console.log("[TM] Campi compilati. Attendo interazione manuale per il tasto Conferma."); // Non facciamo nulla: il ciclo MutationObserver continuerà a girare // ma non troverà discrepanze nei valori e non farà clic. } } // CASO B: Operazione DOC_NRAP (Download) else if (TIPO_ATTUALE === 'DOC_NRAP') { if (downloadInCorso) return; // Se sto già scaricando, NON rientrare nella funzione if (parseInt(VALORE2) > 0) { avviaDownloadDocumenti(); //console.log("Avvio download documenti. Stato attuale tessere:", situazione); } else { console.log("[TM] Valore2 <= 1, chiusura task senza download."); segnaComeCompletato(); } } } } // --- INITIALIZATION --- const observer = new MutationObserver(() => { if (taskAttivo) eseguiAutomazione(); }); observer.observe(document.body, { childList: true, subtree: true }); setInterval(fetchDati, 2500); fetchDati(); })();