(function () { 'use strict'; var csrf = document.querySelector('meta[name="csrf"]').content; var textes = JSON.parse(document.querySelector('meta[name="textes"]').content); // ------------------------------------------------------------------------- // Menu des écrans étroits // ------------------------------------------------------------------------- var boutonMenu = document.querySelector('.menu-bouton'); if (boutonMenu) { var menu = document.getElementById('menu'); boutonMenu.addEventListener('click', function () { var ouvert = menu.classList.toggle('ouvert'); boutonMenu.setAttribute('aria-expanded', ouvert ? 'true' : 'false'); }); } // Sous-menus (Gestion, compte, CV IT Club) : un seul ouvert à la fois, fermés au clic ailleurs et à la touche Échap var sousMenus = Array.prototype.slice.call(document.querySelectorAll('.menu-plus, .menu-deroulant')); sousMenus.forEach(function (m) { m.addEventListener('toggle', function () { if (m.open) { sousMenus.forEach(function (autre) { if (autre !== m) { autre.open = false; } }); } }); }); document.addEventListener('click', function (e) { sousMenus.forEach(function (m) { if (m.open && !m.contains(e.target)) { m.open = false; } }); }); document.addEventListener('keydown', function (e) { if (e.key !== 'Escape') { return; } sousMenus.forEach(function (m) { if (m.open) { m.open = false; m.querySelector('summary').focus(); } }); }); // ------------------------------------------------------------------------- // Envoi long : le bouton annonce l'attente et empêche un second envoi // ------------------------------------------------------------------------- document.querySelectorAll('form[data-attente]').forEach(function (form) { form.addEventListener('submit', function () { var bouton = form.querySelector('button[type="submit"]'); bouton.disabled = true; bouton.textContent = form.dataset.attenteBouton || textes.analyseEnCours; var p = document.createElement('p'); p.className = 'flash flash-info'; p.setAttribute('role', 'status'); p.textContent = form.dataset.attente; form.appendChild(p); }); }); // ------------------------------------------------------------------------- // Kanban : glisser-déposer entre colonnes // ------------------------------------------------------------------------- var kanban = document.querySelector('.kanban'); if (kanban && window.Sortable) { var dialogue = document.getElementById('dialogue-motif'); var annonce = document.getElementById('annonce'); var compter = function () { kanban.querySelectorAll('.colonne').forEach(function (col) { col.querySelector('.compteur').textContent = col.querySelectorAll('.fiche').length; }); }; var annuler = function (evt, message) { evt.from.insertBefore(evt.item, evt.from.children[evt.oldIndex] || null); compter(); if (message) { annonce.textContent = message; annonce.hidden = false; } }; var envoyer = function (evt, motif) { var corps = new URLSearchParams(); corps.set('csrf', csrf); corps.set('id', evt.item.dataset.id); corps.set('statut', evt.to.dataset.statut); corps.set('ordre', Array.prototype.map.call(evt.to.children, function (li) { return li.dataset.id; }).join(',')); if (motif) { corps.set('motif', motif); } fetch(kanban.dataset.url, { method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: corps, credentials: 'same-origin' }) .then(function (r) { return r.json().then(function (j) { return { ok: r.ok && j.ok, erreur: j.erreur }; }); }) .then(function (res) { if (!res.ok) { annuler(evt, res.erreur || textes.echecEnregistrement); return; } annonce.hidden = true; var ancien = evt.item.querySelector('.motif'); if (ancien) { ancien.remove(); } if (motif) { var p = document.createElement('p'); p.className = 'motif'; p.textContent = motif; evt.item.appendChild(p); } }) .catch(function () { annuler(evt, textes.connexionPerdue); }); }; var demanderMotif = function (evt) { var zone = dialogue.querySelector('textarea'); zone.value = ''; dialogue.returnValue = ''; dialogue.addEventListener('close', function () { var motif = zone.value.trim(); if (dialogue.returnValue === 'valider' && motif) { envoyer(evt, motif); } else { annuler(evt); } }, { once: true }); dialogue.showModal(); }; dialogue.querySelector('button[value="annuler"]').addEventListener('click', function () { dialogue.close('annuler'); }); kanban.querySelectorAll('.cartes').forEach(function (liste) { Sortable.create(liste, { group: 'kanban', animation: 150, // Sur écran tactile, un appui bref fait défiler la page ; un appui maintenu saisit la carte delay: 180, delayOnTouchOnly: true, disabled: kanban.dataset.modifiable !== '1', ghostClass: 'fantome', chosenClass: 'saisie', dragClass: 'glisse', onStart: function () { kanban.classList.add('en-glisse'); }, onEnd: function (evt) { kanban.classList.remove('en-glisse'); if (evt.from === evt.to && evt.oldIndex === evt.newIndex) { return; } compter(); if (evt.to.dataset.statut === 'non_retenu' && evt.from !== evt.to) { demanderMotif(evt); } else { envoyer(evt, null); } } }); }); } // ------------------------------------------------------------------------- // Kanban : filtre instantané des cartes // ------------------------------------------------------------------------- var filtre = document.getElementById('filtre-kanban'); if (filtre && kanban) { var sansAccent = function (t) { return t.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); }; var texte = document.getElementById('filtre-texte'); var seniorite = document.getElementById('filtre-seniorite'); var filtrer = function () { var mots = sansAccent(texte.value).split(/\s+/).filter(Boolean); kanban.querySelectorAll('.colonne').forEach(function (col) { var visibles = 0; col.querySelectorAll('.fiche').forEach(function (fiche) { var contenu = sansAccent(fiche.textContent); var garde = (!seniorite.value || fiche.dataset.seniorite === seniorite.value) && mots.every(function (m) { return contenu.indexOf(m) !== -1; }); fiche.classList.toggle('cachee', !garde); if (garde) { visibles++; } }); col.querySelector('.compteur').textContent = visibles; }); }; filtre.addEventListener('submit', function (e) { e.preventDefault(); }); texte.addEventListener('input', filtrer); seniorite.addEventListener('change', filtrer); } // ------------------------------------------------------------------------- // Import en lot : les CV en attente sont analysés l'un après l'autre, une requête par CV // ------------------------------------------------------------------------- var lot = document.getElementById('lot'); if (lot && lot.dataset.auto === '1') { var enAttente = Array.prototype.slice.call(lot.querySelectorAll('tr[data-etat="en_attente"]')); var avancement = document.getElementById('lot-avancement'); var total = enAttente.length; enAttente.forEach(function (tr) { var b = tr.querySelector('.bouton-analyse'); if (b) { b.hidden = true; } }); var analyserSuivant = function () { var tr = enAttente.shift(); if (!tr) { if (total) { if (avancement) { avancement.textContent = textes.lotTermine; } window.location.reload(); } return; } var badge = tr.querySelector('.etat-lot'); var detail = tr.querySelector('.detail-lot'); badge.textContent = textes.analyseEnCours; badge.className = 'badge badge-lot-analyse etat-lot'; var corps = new URLSearchParams(); corps.set('csrf', csrf); corps.set('id', tr.dataset.id); fetch(lot.dataset.url, { method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: corps, credentials: 'same-origin' }) .then(function (r) { return r.json(); }) .then(function (j) { var etat = j.etat || 'erreur'; tr.dataset.etat = etat; badge.className = 'badge badge-lot-' + etat + ' etat-lot'; badge.textContent = etat === 'pret' ? textes.lotPret : (etat === 'erreur' ? textes.lotErreur : textes.lotEnAttente); if (j.candidat) { tr.querySelector('.candidat-lot').textContent = j.candidat; } if (j.erreur) { detail.textContent = j.erreur; detail.hidden = false; } }) .catch(function () { badge.className = 'badge badge-lot-erreur etat-lot'; badge.textContent = textes.lotErreur; detail.textContent = textes.connexionPerdue; detail.hidden = false; }) .then(analyserSuivant); }; analyserSuivant(); } // ------------------------------------------------------------------------- // Messages : fermeture d'un clic ; une confirmation s'efface seule après 6 secondes // ------------------------------------------------------------------------- document.querySelectorAll('main > .flash[role="status"]').forEach(function (flash) { var partir = function () { flash.classList.add('part'); window.setTimeout(function () { flash.remove(); }, 260); }; var texte = document.createElement('span'); while (flash.firstChild) { texte.appendChild(flash.firstChild); } var fermer = document.createElement('button'); fermer.type = 'button'; fermer.className = 'fermer'; fermer.setAttribute('aria-label', textes.fermer); fermer.textContent = '×'; fermer.addEventListener('click', partir); flash.appendChild(texte); flash.appendChild(fermer); flash.classList.add('fermable'); if (flash.classList.contains('flash-succes')) { window.setTimeout(partir, 6000); } }); // Photo introuvable : les initiales la remplacent document.querySelectorAll('img.avatar').forEach(function (img) { var remplacer = function () { var s = document.createElement('span'); s.className = img.className + ' avatar-vide'; s.setAttribute('aria-hidden', 'true'); s.textContent = img.dataset.initiales || ''; img.replaceWith(s); }; if (img.complete && img.naturalWidth === 0) { remplacer(); } else { img.addEventListener('error', remplacer); } }); // Bouton d'impression (aide) document.querySelectorAll('[data-imprimer]').forEach(function (b) { b.addEventListener('click', function () { window.print(); }); }); // ------------------------------------------------------------------------- // Confirmation avant une action irréversible // ------------------------------------------------------------------------- document.querySelectorAll('form[data-confirmer]').forEach(function (form) { form.addEventListener('submit', function (e) { if (!window.confirm(form.dataset.confirmer)) { e.preventDefault(); } }); }); // ------------------------------------------------------------------------- // Fiche profil : le motif n'apparaît que pour « Non retenu » // ------------------------------------------------------------------------- document.querySelectorAll('select[data-motif]').forEach(function (select) { var champ = document.getElementById(select.dataset.motif); var saisie = champ.querySelector('input'); var basculer = function () { var refus = select.value === 'non_retenu'; champ.hidden = !refus; saisie.required = refus; }; select.addEventListener('change', basculer); basculer(); }); // ------------------------------------------------------------------------- // Fiche profil : onglets Profil, Suivi, Missions, Dossier // L'adresse garde l'onglet (#suivi) ou une section (#relances) : un retour après // enregistrement, un lien de la page Suivi ou un rechargement rouvre le bon onglet. // ------------------------------------------------------------------------- var barreOnglets = document.querySelector('[data-onglets]'); if (barreOnglets) { var onglets = Array.prototype.slice.call(barreOnglets.querySelectorAll('[data-onglet]')); var panneaux = onglets.map(function (o) { return document.getElementById(o.dataset.onglet); }); document.body.classList.add('avec-onglets'); barreOnglets.setAttribute('role', 'tablist'); onglets.forEach(function (o, i) { o.setAttribute('role', 'tab'); o.setAttribute('aria-controls', panneaux[i].id); panneaux[i].setAttribute('role', 'tabpanel'); panneaux[i].setAttribute('tabindex', '-1'); }); var activer = function (n) { onglets.forEach(function (o, i) { var choisi = i === n; o.setAttribute('aria-selected', choisi ? 'true' : 'false'); o.tabIndex = choisi ? 0 : -1; panneaux[i].hidden = !choisi; }); }; // Ancre → onglet à ouvrir et élément à montrer. Une ancre inconnue ne change rien. var cibleDe = function (ancre) { var id = decodeURIComponent((ancre || '').replace(/^#/, '')); if (!id) { return null; } var el = document.getElementById(id); var panneau = el && el.closest('.panneau'); return panneau ? { n: panneaux.indexOf(panneau), el: el, section: el !== panneau } : null; }; // Le panneau commence sous la barre collée : on n'y descend que si la barre est déjà collée var montrer = function (cible) { if (cible.section) { cible.el.scrollIntoView({ block: 'start' }); var replie = cible.el.closest('details'); if (replie) { replie.open = true; } } else if (barreOnglets.getBoundingClientRect().top <= parseFloat(getComputedStyle(barreOnglets).top) + 1) { cible.el.scrollIntoView({ block: 'start' }); } }; var aller = function (ancre, defiler) { var cible = cibleDe(ancre); if (!cible) { return false; } activer(cible.n); if (defiler) { montrer(cible); } return true; }; if (!aller(window.location.hash, true)) { activer(0); } // Clic sur un onglet ou sur un lien interne de la fiche (ligne « À faire ») document.addEventListener('click', function (e) { var lien = e.target.closest('a[href^="#"]'); if (!lien || e.defaultPrevented || e.ctrlKey || e.metaKey || e.shiftKey) { return; } var ancre = lien.getAttribute('href'); if (!cibleDe(ancre)) { return; } e.preventDefault(); if (window.history.replaceState) { window.history.replaceState(null, '', ancre); } aller(ancre, true); }); window.addEventListener('hashchange', function () { aller(window.location.hash, true); }); // Clavier : flèches, Début et Fin d'un onglet à l'autre ; Espace ouvre l'onglet barreOnglets.addEventListener('keydown', function (e) { var i = onglets.indexOf(document.activeElement); if (i === -1) { return; } var n = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: onglets.length - 1 }[e.key]; if (e.key === ' ') { e.preventDefault(); onglets[i].click(); return; } if (n === undefined) { return; } e.preventDefault(); n = (n + onglets.length) % onglets.length; onglets[n].focus(); onglets[n].click(); }); } // ------------------------------------------------------------------------- // Filtres : une liste déroulante changée relance la recherche, sans bouton à cliquer. // Sur écran étroit, les filtres des Profils se replient derrière un bouton « Filtres ». // ------------------------------------------------------------------------- document.querySelectorAll('form[data-auto]').forEach(function (form) { form.querySelectorAll('select').forEach(function (select) { select.addEventListener('change', function () { if (form.requestSubmit) { form.requestSubmit(); } else { form.submit(); } }); }); form.querySelectorAll('[data-auto-masquer]').forEach(function (b) { b.hidden = true; }); }); document.querySelectorAll('form[data-repli]').forEach(function (form) { var listes = Array.prototype.slice.call(form.querySelectorAll('select')); var actifs = listes.filter(function (s) { return s.selectedIndex > 0; }).length; var bouton = document.createElement('button'); bouton.type = 'button'; bouton.className = 'secondaire bouton-filtres'; bouton.textContent = textes.filtres + (actifs ? ' (' + actifs + ')' : ''); var replier = function (replie) { form.classList.toggle('replie', replie); bouton.setAttribute('aria-expanded', replie ? 'false' : 'true'); }; bouton.addEventListener('click', function () { replier(!form.classList.contains('replie')); }); var recherche = form.querySelector('label.large'); form.insertBefore(bouton, recherche ? recherche.nextSibling : form.firstChild); replier(actifs === 0); }); // ------------------------------------------------------------------------- // Double authentification : le QR code se dessine à partir de l'adresse otpauth:// // La clé reste écrite en clair à côté, pour une saisie à la main. // ------------------------------------------------------------------------- document.querySelectorAll('[data-qr]').forEach(function (zone) { if (!window.qrcode) { zone.hidden = true; return; } var qr = window.qrcode(0, 'M'); qr.addData(zone.dataset.qr); qr.make(); zone.innerHTML = qr.createSvgTag({ cellSize: 5, margin: 3, scalable: true }); }); // Copier un lien ou des codes dans le presse-papiers document.querySelectorAll('[data-copier]').forEach(function (bouton) { bouton.addEventListener('click', function () { var texte = bouton.dataset.copier; var fait = function () { var avant = bouton.textContent; bouton.textContent = bouton.dataset.copie || textes.copie; window.setTimeout(function () { bouton.textContent = avant; }, 2000); }; if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(texte).then(fait); } else { var zone = document.createElement('textarea'); zone.value = texte; document.body.appendChild(zone); zone.select(); try { document.execCommand('copy'); fait(); } catch (e) { /* le texte reste sélectionnable à l'écran */ } zone.remove(); } }); }); // ------------------------------------------------------------------------- // Traduction des profils en arrière-plan : une par requête, tant que la page reste ouverte. // Une erreur arrête la série ; la page Comptes la montre au gestionnaire. // ------------------------------------------------------------------------- var fileTraduction = document.querySelector('meta[name="traductions"]'); if (fileTraduction && parseInt(fileTraduction.content, 10) > 0) { var traduireSuivante = function () { var corps = new URLSearchParams(); corps.set('csrf', csrf); fetch(fileTraduction.dataset.url, { method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: corps, credentials: 'same-origin' }) .then(function (r) { return r.json(); }) .then(function (j) { if (j.fait && j.reste > 0) { window.setTimeout(traduireSuivante, 1500); } }) .catch(function () { /* nouvel essai au prochain chargement de page */ }); }; window.setTimeout(traduireSuivante, 2500); } // ------------------------------------------------------------------------- // Modifier la fiche : la date n'apparaît que pour « Date précise » ; // le permis de travail ne concerne que les profils sans nationalité UE, EEE ou suisse // ------------------------------------------------------------------------- var choixDispo = document.querySelector('select[name="disponibilite_code"]'); if (choixDispo) { var champDate = document.getElementById('champ-disponible-le'); var montrerDate = function () { var date = choixDispo.value === 'date'; champDate.hidden = !date; champDate.querySelector('input').required = date; }; choixDispo.addEventListener('change', montrerDate); montrerDate(); } var nationalites = document.querySelectorAll('select[data-nationalite]'); var blocPermis = document.getElementById('bloc-permis'); if (nationalites.length && blocPermis) { var libres = (blocPermis.dataset.libres || '').split(','); var nonRequis = document.getElementById('permis-non-requis'); var majPermis = function () { var codes = Array.prototype.map.call(nationalites, function (s) { return s.value; }).filter(Boolean); var libre = codes.some(function (c) { return libres.indexOf(c) !== -1; }); blocPermis.hidden = libre; nonRequis.hidden = !libre; }; nationalites.forEach(function (s) { s.addEventListener('change', majPermis); }); majPermis(); } })();