PageSourceSearch

https://appagendaconsultor.web.app/js/menu-conversor-chamados-os.js?v=6

js appagendaconsultor.web.app collected 2026-10-03 12:17:01 UTC 13,714 bytes, 369 lines download raw bytes

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.