Import initial de Vivier depuis l'hebergement OVH

This commit is contained in:
Yann PILLA committed 2026-10-16 21:52:44 +02:00
commit 839e5e57c5
126 files changed
+22447

No files matched your search

+480
View File
@@ -0,0 +1,480 @@
(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();
}
})();