1/* 2 * P2P - Menu/atalho robusto para Converter Chamados em OS 3 * V6: mantém o botão fora da tela de login/portal público/primeiro acesso. 4 * Inclua nas páginas principais, antes do </body>: 5 * <script src="js/menu-conversor-chamados-os.js?v=6"></script> 6 */ 7(function () { 8 'use strict'; 9 10 var TARGET_HREF = 'converter-chamados-os.html'; 11 var LABEL = 'Converter Chamados em OS'; 12 var ICON = 'â'; 13 var MENU_LINK_ID = 'menu-converter-chamados-os'; 14 var DASH_CARD_ID = 'card-converter-chamados-os'; 15 var FLOAT_ID = 'atalho-fixo-converter-chamados-os'; 16 var insertedMenu = false; 17 var insertedCard = false; 18 var insertedFloat = false; 19 20 function pathName() { 21 return String(location.pathname || '').toLowerCase(); 22 } 23 24 function onConverterPage() { 25 return pathName().indexOf(TARGET_HREF.toLowerCase()) >= 0; 26 } 27 28 function normalize(text) { 29 return String(text || '') 30 .toLowerCase() 31 .normalize('NFD') 32 .replace(/[\u0300-\u036f]/g, '') 33 .replace(/\s+/g, ' ') 34 .trim(); 35 } 36 37 function textOf(el) { 38 if (!el) return ''; 39 return String(el.textContent || el.innerText || el.getAttribute('title') || el.getAttribute('aria-label') || el.getAttribute('href') || ''); 40 } 41 42 function isVisible(el) { 43 if (!el) return false; 44 var rect = el.getBoundingClientRect(); 45 var style = window.getComputedStyle(el); 46 return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || 1) !== 0; 47 } 48 49 function removeInsertedShortcuts() { 50 [MENU_LINK_ID, DASH_CARD_ID, FLOAT_ID].forEach(function (id) { 51 var el = document.getElementById(id); 52 if (!el) return; 53 var wrapper = el.parentElement; 54 if (wrapper && /^(LI)$/i.test(wrapper.tagName) && wrapper.children.length === 1) wrapper.remove(); 55 else el.remove(); 56 }); 57 } 58 59 function hasLoginFormVisible() { 60 var inputs = Array.prototype.slice.call(document.querySelectorAll('input')); 61 var visibleInputs = inputs.filter(isVisible); 62 var hasPassword = visibleInputs.some(function (i) { return String(i.type || '').toLowerCase() === 'password'; }); 63 var hasUserField = visibleInputs.some(function (i) { 64 var data = normalize([i.name, i.id, i.placeholder, i.getAttribute('aria-label')].join(' ')); 65 return /usuario|user|login|email|e-mail|senha/.test(data); 66 }); 67 var visibleButtons = Array.prototype.slice.call(document.querySelectorAll('button, a, input[type="submit"]')).filter(isVisible); 68 var hasEnterButton = visibleButtons.some(function (b) { 69 var t = normalize(textOf(b) || b.value); 70 return /entrar|acessar|login|definir senha|primeiro acesso|recuperar senha/.test(t); 71 }); 72 return hasPassword && (hasUserField || hasEnterButton); 73 } 74 75 function isPublicOrAuthPage() { 76 if (onConverterPage()) return false; 77 78 var p = pathName(); 79 var file = (p.split('/').pop() || 'index.html').toLowerCase(); 80 81 // Páginas dedicadas de acesso/recuperação não devem receber o atalho. 82 if (/login|entrar|acesso|primeiro-acesso|primeiro_acesso|trocar-senha|alterar-senha|recuperar-senha|reset|senha/.test(file)) { 83 return true; 84 } 85 86 // Em index.html, só bloqueia enquanto o formulário de login estiver realmente visÃvel. 87 if (hasLoginFormVisible()) return true; 88 89 var bodyText = normalize((document.body && document.body.innerText) || ''); 90 var looksLikePortalLogin = /portal p2p|acesso ao sistema|informe seu usuario|informe seu usuário|usuario e senha|usuário e senha/.test(bodyText); 91 if (looksLikePortalLogin && hasLoginFormVisible()) return true; 92 93 return false; 94 } 95 96 function hasLoggedAreaSignal() { 97 var bodyText = normalize((document.body && document.body.innerText) || ''); 98 var hasLogout = /sair|logout|encerrar sessao|encerrar sessão/.test(bodyText); 99 var hasOperationalWords = /dashboard|menu|chamados|ordens de servico|ordens de serviço|relatorios|relatórios|clientes|consultores|operacoes|operações/.test(bodyText); 100 var hasNav = !!document.querySelector('nav, aside, #sidebar, .sidebar, .menu-lateral, .side-menu, [role="navigation"]'); 101 return hasLogout || (hasNav && hasOperationalWords); 102 } 103 104 function shouldRun() { 105 if (!document.body || onConverterPage()) return false;
106 if (isPublicOrAuthPage()) { 107 removeInsertedShortcuts(); 108 return false; 109 } 110 // Evita criar botão em páginas públicas simples. Se o menu ainda estiver carregando, 111 // o observer/timer tentará novamente quando a área logada aparecer. 112 if (!hasLoggedAreaSignal()) { 113 removeInsertedShortcuts(); 114 return false; 115 } 116 return true; 117 } 118 119 function hasMenuLink() { 120 if (document.getElementById(MENU_LINK_ID)) return true; 121 return Array.prototype.some.call(document.querySelectorAll('a[href]'), function (a) { 122 var href = (a.getAttribute('href') || '').toLowerCase(); 123 return href.indexOf(TARGET_HREF.toLowerCase()) >= 0 && a.id !== DASH_CARD_ID && a.id !== FLOAT_ID; 124 }); 125 } 126 127 function createAnchor(reference, mode) { 128 var a = document.createElement('a'); 129 a.id = mode === 'menu' ? MENU_LINK_ID : (mode === 'card' ? DASH_CARD_ID : FLOAT_ID); 130 a.href = TARGET_HREF; 131 a.setAttribute('data-p2p-menu', 'converter-chamados-os'); 132 a.setAttribute('title', LABEL); 133 134 if (reference && typeof reference.className === 'string' && reference.className.trim()) { 135 a.className = reference.className; 136 } else if (mode === 'menu') { 137 a.className = 'menu-item nav-link sidebar-link'; 138 } 139 140 if (mode === 'card') { 141 a.innerHTML = '<div class="p2p-conv-card-icon">' + ICON + '</div><div><strong>' + LABEL + '</strong><small>Gerar uma única OS a partir de vários chamados</small></div>'; 142 } else { 143 a.innerHTML = '<span class="menu-icon" aria-hidden="true">' + ICON + '</span><span class="menu-text">' + LABEL + '</span>'; 144 } 145 146 return a; 147 } 148 149 function referenceTerms() { 150 return [ 151 'central de chamados', 'chamados', 'relatorio de chamados', 'relatório de chamados', 152 'ordens de serviço', 'ordens de servico', 'ordem de serviço', 'ordem de servico', 153 'os', 'operacoes', 'operações', 'reabertura', 'area do cliente', 'área do cliente' 154 ]; 155 } 156 157 function findReferenceLink() { 158 var links = Array.prototype.slice.call(document.querySelectorAll('a[href], button, [role="button"]')); 159 var terms = referenceTerms().map(normalize); 160 161 for (var t = 0; t < terms.length; t++) { 162 for (var i = 0; i < links.length; i++) { 163 var el = links[i]; 164 var txt = normalize(textOf(el)); 165 var href = normalize(el.getAttribute && el.getAttribute('href')); 166 if ((txt && txt.indexOf(terms[t]) >= 0) || (href && href.indexOf(terms[t].replace(/ /g, '-')) >= 0)) { 167 return el; 168 } 169 } 170 } 171 return null; 172 } 173 174 function findBestMenuContainer(reference) { 175 if (reference) { 176 var current = reference; 177 for (var i = 0; i < 6 && current; i++) { 178 if (current.matches && current.matches('ul, ol, nav, aside, .sidebar, .menu, .menu-lateral, .side-menu, .nav-menu, .navigation, [role="navigation"], [data-menu]')) { 179 return current; 180 } 181 current = current.parentElement; 182 } 183 } 184 185 var selectors = [ 186 '#sidebar', '.sidebar', '.app-sidebar', '.menu-lateral', '.side-menu', '.sidebar-menu', 187 '#menu', '.menu', '.nav-menu', '.navigation', '.menu-principal', '.main-menu', 188 'aside nav', 'aside', 'nav', '[data-menu]', '[role="navigation"]' 189 ]; 190 191 for (var s = 0; s < selectors.length; s++) { 192 var candidates = Array.prototype.slice.call(document.querySelectorAll(selectors[s])); 193 for (var c = 0; c < candidates.length; c++) { 194 if (candidates[c].querySelectorAll('a,button,[role="button"]').length) return candidates[c]; 195 } 196 } 197 return null; 198 } 199 200 function wrapLikeReference(link, reference) { 201 if (!reference || !reference.parentElement) return link; 202 var parent = reference.parentElement; 203 var tag = parent.tagName; 204 205 if (tag === 'LI') { 206 var li = document.createElement('li'); 207 li.className = parent.className || 'nav-item menu-item'; 208 li.appendChild(link); 209 return li; 210 } 211 212 if (parent.className && /(^|\s)(menu-item|nav-item|sidebar-item|item|opcao|option)(\s|$)/i.test(parent.className)) { 213 var div = document.createElement(parent.tagName.toLowerCase()); 214 div.className = parent.className; 215 div.appendChild(link); 216 return div; 217 } 218 219 return link; 220 } 221 222 function insertAfter(reference, node) { 223 if (!reference) return false;
224 var base = reference; 225 if (reference.parentElement && /^(LI|DIV)$/i.test(reference.parentElement.tagName) && reference.parentElement.children.length <= 3) { 226 base = reference.parentElement; 227 } 228 if (base.parentNode) { 229 base.parentNode.insertBefore(node, base.nextSibling); 230 return true; 231 } 232 return false; 233 } 234 235 function styleMenuFallback(link) { 236 if (!link) return; 237 link.style.display = link.style.display || 'flex'; 238 link.style.alignItems = link.style.alignItems || 'center'; 239 link.style.gap = link.style.gap || '10px'; 240 link.style.padding = link.style.padding || '10px 12px'; 241 link.style.borderRadius = link.style.borderRadius || '12px'; 242 link.style.textDecoration = link.style.textDecoration || 'none'; 243 } 244 245 function addMenuLink() { 246 if (!shouldRun() || hasMenuLink()) return false; 247 248 var reference = findReferenceLink(); 249 var container = findBestMenuContainer(reference); 250 if (!container && !reference) return false; 251 252 var link = createAnchor(reference && reference.tagName === 'A' ? reference : null, 'menu'); 253 var node = wrapLikeReference(link, reference); 254 255 var ok = false; 256 if (reference) ok = insertAfter(reference, node); 257 if (!ok && container) { 258 container.appendChild(node); 259 ok = true; 260 } 261 262 if (ok) { 263 styleMenuFallback(link); 264 insertedMenu = true; 265 return true; 266 } 267 return false; 268 } 269 270 function addDashboardCard() { 271 if (!shouldRun() || document.getElementById(DASH_CARD_ID)) return false; 272 273 var selectors = [ 274 '#atalhos', '.atalhos', '.quick-actions', '.cards', '.cards-grid', '.grid-cards', 275 '.dashboard-cards', '.dashboard-grid', '#dashboard', '.dashboard', 'main .grid', 'main' 276 ]; 277 278 var container = null; 279 for (var i = 0; i < selectors.length; i++) { 280 var el = document.querySelector(selectors[i]); 281 if (el) { container = el; break; } 282 } 283 if (!container) return false; 284 285 var card = createAnchor(null, 'card'); 286 card.style.display = 'flex'; 287 card.style.alignItems = 'center'; 288 card.style.gap = '12px'; 289 card.style.padding = '16px'; 290 card.style.borderRadius = '16px'; 291 card.style.background = 'linear-gradient(135deg,#0f77d4,#13b5d1)'; 292 card.style.color = '#fff'; 293 card.style.textDecoration = 'none'; 294 card.style.boxShadow = '0 12px 28px rgba(15,35,65,.18)'; 295 card.style.minHeight = '86px'; 296 card.style.margin = '10px 0'; 297 298 var css = document.createElement('style'); 299 css.setAttribute('data-p2p-conversor-css', '1'); 300 css.textContent = '.p2p-conv-card-icon{width:42px;height:42px;border-radius:14px;background:rgba(255,255,255,.18);display:grid;place-items:center;font-weight:900;font-size:22px}.p2p-conv-card-icon+div{display:flex;flex-direction:column;gap:3px}.p2p-conv-card-icon+div small{opacity:.88;font-weight:650}'; 301 if (!document.querySelector('style[data-p2p-conversor-css]')) document.head.appendChild(css); 302 303 container.appendChild(card); 304 insertedCard = true; 305 return true; 306 } 307 308 function addFloatingShortcut(force) { 309 if (!shouldRun() || document.getElementById(FLOAT_ID)) return false; 310 311 var menu = document.getElementById(MENU_LINK_ID); 312 if (!force && menu && isVisible(menu)) return false; 313 314 var a = createAnchor(null, 'float'); 315 a.textContent = LABEL; 316 a.style.position = 'fixed'; 317 a.style.right = '18px'; 318 a.style.bottom = '18px'; 319 a.style.zIndex = '999999'; 320 a.style.padding = '13px 16px'; 321 a.style.borderRadius = '16px'; 322 a.style.background = 'linear-gradient(135deg,#0f172a,#0f77d4)'; 323 a.style.color = '#fff'; 324 a.style.fontFamily = 'Arial, sans-serif'; 325 a.style.fontSize = '13px'; 326 a.style.fontWeight = '800'; 327 a.style.textDecoration = 'none'; 328 a.style.boxShadow = '0 14px 32px rgba(15,23,42,.28)'; 329 a.style.border = '1px solid rgba(255,255,255,.2)'; 330 331 document.body.appendChild(a); 332 insertedFloat = true; 333 return true; 334 } 335 336 function run() { 337 if (!shouldRun()) return; 338 addMenuLink(); 339 addDashboardCard(); 340 341 setTimeout(function () { 342 if (!shouldRun()) return; 343 var menu = document.getElementById(MENU_LINK_ID); 344 var card = document.getElementById(DASH_CARD_ID); 345 if ((!menu || !isVisible(menu)) && (!card || !isVisible(card))) addFloatingShortcut(true); 346 }, 1200); 347 } 348 349 function start() { 350 run(); 351 352 var attempts = 0; 353 var timer = setInterval(function () { 354 attempts += 1; 355 run(); 356 if (attempts >= 30 && (insertedMenu || insertedCard || insertedFloat)) clearInterval(timer); 357 if (attempts >= 80) clearInterval(timer); 358 }, 500); 359 360 if (window.MutationObserver && document.body) { 361 var obs = new MutationObserver(function () { run(); }); 362 obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] }); 363 setTimeout(function () { try { obs.disconnect(); } catch (e) {} }, 45000); 364 } 365 } 366 367 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); 368 else start(); 369})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.