Files

481 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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();
}
})();