<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
Enregistré automatiquement
Académie de Guyane · Économie-Gestion voie professionnelle

Convocation à une formation

Renseignez la session, collez la liste des stagiaires, puis téléchargez la convocation au format de votre choix. La saisie est conservée dans ce navigateur.

⚠️ <form id="formationForm" onsubmit="return false" autocomplete="off">
01

La formation

02

Les stagiaires

Coller une liste

Un stagiaire par ligne. Collez directement une ou deux colonnes copiées depuis Excel, un courriel (champ « À » ou « Cc ») ou un annuaire. Si le nom est absent, il est déduit d'une adresse de la forme prenom.nom@… : pensez à le vérifier. Les doublons sont ignorés. Vous pouvez aussi coller plusieurs lignes directement dans un champ de la liste.

⚠️ <button type="button" class="btn btn-p" onclick="importerDepuisZone()">Ajouter à la liste ⚠️ <button type="button" class="btn btn-n" onclick="viderZone()">Vider la zone
⚠️ <button type="button" class="btn btn-s btn-sm" onclick="ajouterStagiaire('', '', true)">Ajouter une ligne ⚠️ <button type="button" class="btn btn-n btn-sm" onclick="trierParNom()">Trier A → Z ⚠️ <button type="button" class="btn btn-n btn-sm" onclick="supprimerLignesVides()">Supprimer les lignes vides ⚠️ <button type="button" class="btn btn-n btn-sm" onclick="copierAdresses()">Copier les adresses ⚠️ <button type="button" class="btn btn-n btn-sm" onclick="reinitialiser()">Réinitialiser

03

Télécharger la convocation

⚠️ <button type="button" class="btn btn-s" id="btnWord" onclick="exportWord()">Word (.doc) ⚠️ <button type="button" class="btn btn-s" id="btnPdf" onclick="exportPDF()">PDF ⚠️ <button type="button" class="btn btn-s" id="btnExcel" onclick="exportExcel()">Excel (.xlsx) ⚠️ <button type="button" class="btn btn-s" id="btnIcs" onclick="exportICS()">Agenda (.ics)

Le fichier .ics s'importe dans Framagenda, Outlook, Google Agenda ou tout autre calendrier. Les exports PDF et Excel nécessitent une connexion à Internet.

⚠️ <button type="button" class="btn btn-n" id="btnApercu" onclick="toggleApercu()">Afficher l'aperçu
04

Aperçu

<script>
/* ==== PURE:START (fonctions sans accès au DOM) ==== */
const EMAIL_RE = /[A-Za-z0-9._%+'\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g;
const EMAIL_VALIDE = /^[A-Za-z0-9._%+'\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$/;

function esc(valeur) {
return String(valeur == null ? '' : valeur)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function escMultiligne(valeur) { return esc(valeur).replace(/\r?\n/g, '<br>'); }

function nettoyerNom(texte) {
return texte
.replace(/mailto:/gi, ' ')
.replace(/[<>"';\t,()\[\]]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}

function majuscule(mot) { return mot ? mot.charAt(0).toUpperCase() + mot.slice(1).toLowerCase() : ''; }

/* prenom.nom@domaine -> NOM Prénom (déduction : à vérifier) */
function deduireNom(email) {
const local = email.split('@')[0];
const morceaux = local.split(/[._\-]+/).filter(m => m && !/^\d+$/.test(m));
if (morceaux.length < 2 || morceaux[0].length < 2) return ''; // une initiale seule ne suffit pas
const prenom = majuscule(morceaux[0]);
const nom = morceaux.slice(1).join(' ').toUpperCase();
return nom + ' ' + prenom;
}

function traiterSegment(segment, sortie) {
const emails = segment.match(EMAIL_RE) || [];
if (emails.length === 0) {
const nom = nettoyerNom(segment);
if (nom) sortie.push({ nom: nom, email: '', deduit: false });
return;
}
const email = emails[0];
let nom = nettoyerNom(segment.replace(email, ' '));
let deduit = false;
if (!nom) { nom = deduireNom(email); deduit = !!nom; }
sortie.push({ nom: nom, email: email, deduit: deduit });
}

function parserListe(texte) {
const sortie = [];
const lignes = String(texte || '').replace(/\r/g, '').split('\n');
lignes.forEach(ligne => {
if (!ligne.trim()) return;
const emails = ligne.match(EMAIL_RE) || [];
if (emails.length <= 1) { traiterSegment(ligne, sortie); return; }
// Plusieurs adresses sur une même ligne : "Nom <a@b>; Nom <c@d>" ou "a@b, c@d"
let parties = ligne.split(';');
if (!parties.every(p => (p.match(EMAIL_RE) || []).length <= 1)) {
parties = ligne.split(/[,\s]+/);
}
parties.forEach(p => { if (p.trim()) traiterSegment(p, sortie); });
});
return sortie;
}

function cle(s) { return s.email ? s.email.toLowerCase() : 'nom:' + s.nom.toLowerCase(); }

function formatDate(dateStr) {
if (!dateStr) return '';
const [a, m, j] = dateStr.split('-').map(Number);
return new Date(a, m - 1, j).toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
}

/* ICS */
function echapperICS(s) {
return String(s == null ? '' : s)
.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\r?\n/g, '\\n');
}
function plierICS(ligne) {
const enc = new TextEncoder();
let sortie = '', courant = '', taille = 0;
for (const c of ligne) {
const o = enc.encode(c).length;
if (taille + o > 75) { sortie += courant + '\r\n'; courant = ' '; taille = 1; }
courant += c; taille += o;
}
return sortie + courant;
}
function analyserHoraires(horaires) {
if (!horaires) return null;
const m = horaires.match(/(\d{1,2})\s*[h:]\s*(\d{0,2})\s*(?:-|–|—|à)\s*(\d{1,2})\s*[h:]\s*(\d{0,2})/i);
if (!m) return null;
const p = x => String(x || '0').padStart(2, '0');
return { debut: p(m[1]) + p(m[2]) + '00', fin: p(m[3]) + p(m[4]) + '00' };
}
function jourSuivant(dateStr) {
const [a, m, j] = dateStr.split('-').map(Number);
return new Date(Date.UTC(a, m - 1, j + 1)).toISOString().slice(0, 10).replace(/-/g, '');
}
function construireICS(d) {
const base = d.date.replace(/-/g, '');
const h = analyserHoraires(d.horaires);

let desc = 'Formation Éco-Gestion voie professionnelle\n';
desc += 'Formateur(s) : ' + d.formateurs + '\n';
if (d.dispositif) desc += 'Dispositif : ' + d.dispositif + '\n';
if (d.module) desc += 'Module : ' + d.module + '\n';
if (d.horaires) desc += 'Horaires : ' + d.horaires + '\n';
if (d.observations) desc += 'Observations : ' + d.observations + '\n';
desc += '\nStagiaires convoqués (' + d.stagiaires.length + ') :\n';
d.stagiaires.forEach((s, i) => { desc += (i + 1) + '. ' + (s.nom || '-') + (s.email ? ' (' + s.email + ')' : '') + '\n'; });

let titre = 'Formation : ' + d.theme;
if (d.dispositif) titre += ' [' + d.dispositif + ']';
if (d.module) titre += ' - ' + d.module;

const uid = 'formation-' + Date.now() + '-' + Math.random().toString(36).slice(2, 11) + '@ac-guyane.fr';
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');

const l = [
'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Academie Guyane//Formation Eco-Gestion//FR',
'CALSCALE:GREGORIAN', 'METHOD:PUBLISH',
'BEGIN:VTIMEZONE', 'TZID:America/Cayenne',
'BEGIN:STANDARD', 'DTSTART:19700101T000000', 'TZOFFSETFROM:-0300', 'TZOFFSETTO:-0300', 'TZNAME:-03', 'END:STANDARD',
'END:VTIMEZONE',
'BEGIN:VEVENT', 'UID:' + uid, 'DTSTAMP:' + stamp
];
if (h) {
l.push('DTSTART;TZID=America/Cayenne:' + base + 'T' + h.debut);
l.push('DTEND;TZID=America/Cayenne:' + base + 'T' + h.fin);
} else {
l.push('DTSTART;VALUE=DATE:' + base);
l.push('DTEND;VALUE=DATE:' + jourSuivant(d.date));
}
l.push('SUMMARY:' + echapperICS(titre));
l.push('DESCRIPTION:' + echapperICS(desc));
l.push('LOCATION:' + echapperICS(d.lieu));
l.push('END:VEVENT', 'END:VCALENDAR');
return l.map(plierICS).join('\r\n') + '\r\n';
}
/* ==== PURE:END ==== */

/* ---------- Interface ---------- */
let timerSauvegarde = null;
let timerToast = null;

function $(id) { return document.getElementById(id); }

document.addEventListener('DOMContentLoaded', () => {
chargerDonneesLocales();
if (!document.querySelector('.row')) ajouterStagiaire();
$('formationForm').addEventListener('input', () => {
clearTimeout(timerSauvegarde);
timerSauvegarde = setTimeout(sauvegarderLocalement, 700);
});
});

function showMessage(texte, type) {
const t = $('toast');
t.textContent = texte;
t.className = 'toast ' + (type === 'error' ? 'error' : type === 'info' ? 'info' : '');
t.style.display = 'block';
clearTimeout(timerToast);
timerToast = setTimeout(() => { t.style.display = 'none'; }, 5000);
}

/* Persistance locale */
function sauvegarderLocalement() {
try {
localStorage.setItem('formationData', JSON.stringify(lireFormulaire()));
const ind = $('saveIndicator');
ind.classList.add('visible');
setTimeout(() => ind.classList.remove('visible'), 1600);
} catch (e) { /* stockage indisponible : l'outil reste utilisable */ }
}

function chargerDonneesLocales() {
try {
const brut = localStorage.getItem('formationData');
if (!brut) return;
const d = JSON.parse(brut);
['theme', 'formateurs', 'dispositif', 'module', 'date', 'horaires', 'lieu', 'observations']
.forEach(k => { $(k).value = d[k] || ''; });
(d.stagiaires || []).forEach(s => ajouterStagiaire(s.nom, s.email));
} catch (e) { /* aucune donnée exploitable */ }
}

/* Lecture du formulaire */
function lireStagiaires(garderVides) {
const liste = [];
document.querySelectorAll('.row').forEach(row => {
const nom = row.querySelector('.in-nom').value.trim();
const email = row.querySelector('.in-mail').value.trim();
if (garderVides || nom || email) liste.push({ nom, email });
});
return liste;
}

function lireFormulaire() {
return {
theme: $('theme').value.trim(),
formateurs: $('formateurs').value.trim(),
dispositif: $('dispositif').value.trim(),
module: $('module').value.trim(),
date: $('date').value,
horaires: $('horaires').value.trim(),
lieu: $('lieu').value.trim(),
observations: $('observations').value.trim(),
stagiaires: lireStagiaires(false)
};
}

/* Lignes de stagiaires (construites par le DOM : aucune injection de HTML) */
function ajouterStagiaire(nom, email, focus) {
const row = document.createElement('div');
row.className = 'row';

const num = document.createElement('span');
num.className = 'row-num';

const inNom = document.createElement('input');
inNom.type = 'text'; inNom.className = 'in-nom'; inNom.placeholder = 'DUPONT Marie';
inNom.setAttribute('aria-label', 'Nom et prénom'); inNom.value = nom || '';

const inMail = document.createElement('input');
inMail.type = 'text'; inMail.className = 'in-mail'; inMail.placeholder = 'marie.dupont@ac-guyane.fr';
inMail.setAttribute('inputmode', 'email'); inMail.setAttribute('aria-label', 'Adresse courriel'); inMail.value = email || '';

const suppr = document.createElement('button');
suppr.type = 'button'; suppr.className = 'btn-x'; suppr.textContent = '×';
suppr.setAttribute('aria-label', 'Supprimer cette ligne');
suppr.addEventListener('click', () => { row.remove(); renumeroter(); sauvegarderLocalement(); });

[inNom, inMail].forEach(champ => champ.addEventListener('paste', surCollage));
inMail.addEventListener('input', () => marquerInvalide(inMail));
inNom.addEventListener('input', mettreAJourCompteur);
inMail.addEventListener('input', mettreAJourCompteur);

row.append(num, inNom, inMail, suppr);
$('liste').appendChild(row);
marquerInvalide(inMail);
renumeroter();
if (focus) inNom.focus();
}

function marquerInvalide(champ) {
const v = champ.value.trim();
champ.classList.toggle('invalide', v !== '' && !EMAIL_VALIDE.test(v));
}

function renumeroter() {
document.querySelectorAll('.row').forEach((row, i) => { row.querySelector('.row-num').textContent = i + 1; });
mettreAJourCompteur();
}

function mettreAJourCompteur() {
const l = lireStagiaires(false);
const sansMail = l.filter(s => !s.email).length;
const invalides = l.filter(s => s.email && !EMAIL_VALIDE.test(s.email)).length;
let html = '<strong>' + l.length + '</strong>stagiaire' + (l.length > 1 ? 's' : '');
if (sansMail) html += '<span class="warn">' + sansMail + ' sans courriel</span>';
if (invalides) html += '<span class="warn">' + invalides + ' adresse' + (invalides > 1 ? 's' : '') + ' à vérifier</span>';
$('compteurZone').innerHTML = html;
}

function reconstruireListe(stagiaires) {
$('liste').innerHTML = '';
stagiaires.forEach(s => ajouterStagiaire(s.nom, s.email));
if (!document.querySelector('.row')) ajouterStagiaire();
renumeroter();
}

/* Import par collage */
function importerListe(texte) {
const trouves = parserListe(texte);
if (trouves.length === 0) { showMessage('Aucun stagiaire reconnu dans le texte collé.', 'error'); return 0; }

const existants = lireStagiaires(false);
const vus = new Set(existants.map(cle));
let ajoutes = 0, doublons = 0, deduits = 0, sansMail = 0;
const aAjouter = [];
trouves.forEach(s => {
const k = cle(s);
if (vus.has(k)) { doublons++; return; }
vus.add(k);
aAjouter.push(s);
ajoutes++;
if (s.deduit) deduits++;
if (!s.email) sansMail++;
});

reconstruireListe(existants.concat(aAjouter));
sauvegarderLocalement();

let msg = ajoutes + ' stagiaire' + (ajoutes > 1 ? 's' : '') + ' ajouté' + (ajoutes > 1 ? 's' : '') + '.';
if (doublons) msg += ' ' + doublons + ' doublon' + (doublons > 1 ? 's' : '') + ' ignoré' + (doublons > 1 ? 's' : '') + '.';
if (deduits) msg += ' ' + deduits + ' nom' + (deduits > 1 ? 's' : '') + ' déduit' + (deduits > 1 ? 's' : '') + ' de l\'adresse, à vérifier.';
if (sansMail) msg += ' ' + sansMail + ' sans courriel.';
showMessage(msg, ajoutes ? 'ok' : 'info');
return ajoutes;
}

function importerDepuisZone() {
const zone = $('zoneCollage');
if (!zone.value.trim()) { showMessage('La zone de collage est vide.', 'info'); return; }
if (importerListe(zone.value)) zone.value = '';
}
function viderZone() { $('zoneCollage').value = ''; $('zoneCollage').focus(); }

function surCollage(e) {
const texte = (e.clipboardData || window.clipboardData).getData('text');
if (!/[\n\t]/.test(texte.trim())) return; // collage simple : comportement normal
e.preventDefault();
importerListe(texte);
}

/* Outils de liste */
function trierParNom() {
const l = lireStagiaires(false);
if (l.length === 0) { showMessage('Aucun stagiaire à trier.', 'info'); return; }
const norm = s => (s.nom || s.email).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
l.sort((a, b) => norm(a).localeCompare(norm(b), 'fr'));
reconstruireListe(l);
sauvegarderLocalement();
showMessage('Liste triée par ordre alphabétique.', 'ok');
}

function supprimerLignesVides() {
const total = document.querySelectorAll('.row').length;
const l = lireStagiaires(false);
const retirees = total - l.length;
reconstruireListe(l);
sauvegarderLocalement();
showMessage(retirees ? retirees + ' ligne(s) vide(s) supprimée(s).' : 'Aucune ligne vide.', retirees ? 'ok' : 'info');
}

function reinitialiser() {
if (!confirm('Réinitialiser le formulaire ? Toutes les données saisies seront effacées.')) return;
$('formationForm').reset();
$('zoneCollage').value = '';
$('liste').innerHTML = '';
ajouterStagiaire();
try { localStorage.removeItem('formationData'); } catch (e) {}
showMessage('Formulaire réinitialisé.', 'info');
}

function copierAdresses() {
const adresses = lireStagiaires(false).map(s => s.email).filter(Boolean);
if (adresses.length === 0) { showMessage('Aucune adresse à copier.', 'info'); return; }
const texte = adresses.join('; ');
const ok = () => showMessage(adresses.length + ' adresse(s) copiée(s), prêtes à coller dans le champ « À » d\'un courriel.', 'ok');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(texte).then(ok, () => copieSecours(texte, ok));
} else { copieSecours(texte, ok); }
}
function copieSecours(texte, ok) {
const ta = document.createElement('textarea');
ta.value = texte; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); ok(); } catch (e) { showMessage('Copie impossible : sélectionnez les adresses manuellement.', 'error'); }
document.body.removeChild(ta);
}

/* Validation et aperçu */
function valider() {
const d = lireFormulaire();
if (!d.theme || !d.formateurs || !d.date || !d.lieu) {
showMessage('Veuillez remplir les champs obligatoires (*) : thème, formateur(s), date et lieu.', 'error');
return null;
}
if (d.stagiaires.length === 0) { showMessage('Veuillez ajouter au moins un stagiaire.', 'error'); return null; }
return d;
}

function toggleApercu() {
const section = $('apercuSection');
section.classList.toggle('visible');
const visible = section.classList.contains('visible');
$('btnApercu').textContent = visible ? 'Masquer l\'aperçu' : 'Afficher l\'aperçu';
if (visible) majApercu();
}

function majApercu() {
const d = lireFormulaire();
let h = '<div class="doc-kicker">Académie de Guyane</div>'
+ '<div class="doc-title">Convocation à une formation</div>'
+ '<div class="doc-sub">Économie-Gestion voie professionnelle</div>'
+ '<div class="doc-rule"></div>'
+ '<h3>' + esc(d.theme || 'Thème de la formation') + '</h3>'
+ '<p><b>Formateur(s) :</b> ' + esc(d.formateurs || 'non renseigné') + '</p>'
+ (d.dispositif ? '<p><b>Dispositif :</b> ' + esc(d.dispositif) + '</p>' : '')
+ (d.module ? '<p><b>Module :</b> ' + esc(d.module) + '</p>' : '')
+ '<p><b>Date :</b> ' + esc(formatDate(d.date) || 'non renseignée') + '</p>'
+ '<p><b>Horaires :</b> ' + esc(d.horaires || 'non précisés') + '</p>'
+ '<p><b>Lieu :</b> ' + esc(d.lieu || 'non renseigné') + '</p>'
+ (d.observations ? '<p><b>Observations :</b> ' + escMultiligne(d.observations) + '</p>' : '')
+ '<h4>Stagiaires convoqués (' + d.stagiaires.length + ')</h4>'
+ '<table><thead><tr><th>N°</th><th>Nom et prénom</th><th>Adresse courriel</th></tr></thead><tbody>';
d.stagiaires.forEach((s, i) => {
h += '<tr><td>' + (i + 1) + '</td><td>' + esc(s.nom || '-') + '</td><td>' + esc(s.email || '-') + '</td></tr>';
});
h += '</tbody></table>';
$('apercuContenu').innerHTML = h;
}

/* Téléchargement */
function telecharger(blob, nom) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = nom;
document.body.appendChild(a); a.click(); document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function nomFichier(d, ext) { return 'formation_' + (d.date || 'convocation') + '.' + ext; }

function avecBouton(idBtn, action) {
const d = valider();
if (!d) return;
const btn = $(idBtn);
const libelle = btn.innerHTML;
btn.innerHTML = '<span class="loader"></span> Génération';
btn.disabled = true;
try { action(d); }
catch (err) { console.error(err); showMessage('Erreur : ' + err.message, 'error'); }
finally { btn.innerHTML = libelle; btn.disabled = false; }
}

/* Word */
function exportWord() {
avecBouton('btnWord', d => {
let lignes = '';
d.stagiaires.forEach((s, i) => {
const fond = i % 2 ? ' background:#F4F2ED;' : '';
lignes += '<tr>'
+ '<td style="border:1px solid #C8C6BC; padding:5px 8px; text-align:center;' + fond + '">' + (i + 1) + '</td>'
+ '<td style="border:1px solid #C8C6BC; padding:5px 8px;' + fond + '">' + esc(s.nom || '-') + '</td>'
+ '<td style="border:1px solid #C8C6BC; padding:5px 8px;' + fond + '">' + esc(s.email || '-') + '</td></tr>';
});
const info = (lib, val) => '<p style="margin:0 0 4pt 0;"><b>' + lib + ' :</b> ' + val + '</p>';

const html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">'
+ '<head><meta charset="utf-8"><title>Convocation</title>'
+ '<!--[if gte mso 9]><xml><w:WordDocument><w:View>Print</w:View><w:Zoom>100</w:Zoom></w:WordDocument></xml><![endif]-->'
+ '<style>@page { margin: 1.27cm; } body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; color: #1C1C1A; } table { border-collapse: collapse; width: 100%; }</style>'
+ '</head><body>'
+ '<p style="text-align:center; font-size:9pt; letter-spacing:2pt; color:#7A7A72; margin:0;">ACADÉMIE DE GUYANE</p>'
+ '<p style="text-align:center; font-size:20pt; font-weight:bold; margin:4pt 0 0 0;">Convocation à une formation</p>'
+ '<p style="text-align:center; font-size:12pt; font-style:italic; color:#0F6E56; margin:0 0 8pt 0;">Économie-Gestion voie professionnelle</p>'
+ '<hr style="border:0; border-top:1pt solid #1C1C1A; margin:0 0 12pt 0;">'
+ '<p style="font-size:14pt; font-weight:bold; margin:0 0 8pt 0;">' + esc(d.theme) + '</p>'
+ info('Formateur(s)', esc(d.formateurs))
+ (d.dispositif ? info('Dispositif', esc(d.dispositif)) : '')
+ (d.module ? info('Module', esc(d.module)) : '')
+ info('Date', esc(formatDate(d.date)))
+ info('Horaires', esc(d.horaires || 'non précisés'))
+ info('Lieu', esc(d.lieu))
+ (d.observations ? info('Observations', escMultiligne(d.observations)) : '')
+ '<p style="font-size:12pt; font-weight:bold; margin:16pt 0 6pt 0;">Stagiaires convoqués (' + d.stagiaires.length + ')</p>'
+ '<table><thead><tr>'
+ '<th style="width:8%; border:1px solid #0F6E56; background:#0F6E56; color:#‌FFFFFF; padding:6px 8px; text-align:center;">N°</th>'
+ '<th style="width:42%; border:1px solid #0F6E56; background:#0F6E56; color:#‌FFFFFF; padding:6px 8px; text-align:left;">Nom et prénom</th>'
+ '<th style="width:50%; border:1px solid #0F6E56; background:#0F6E56; color:#‌FFFFFF; padding:6px 8px; text-align:left;">Adresse courriel</th>'
+ '</tr></thead><tbody>' + lignes + '</tbody></table>'
+ '<p style="margin-top:14pt; text-align:right; font-size:9pt; color:#7A7A72; font-style:italic;">Document généré le ' + new Date().toLocaleDateString('fr-FR') + '</p>'
+ '</body></html>';

telecharger(new Blob(['\ufeff', html], { type: 'application/msword' }), nomFichier(d, 'doc'));
showMessage('Document Word téléchargé.', 'ok');
});
}

/* PDF */
function exportPDF() {
avecBouton('btnPdf', d => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ unit: 'mm', format: 'a4' });
const G = [15, 110, 86], INK = [28, 28, 26], MU = [122, 122, 114];
const gauche = 18, droite = 192, largeur = droite - gauche;
let y = 22;

const pied = () => {
doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.setTextColor(...MU);
doc.text('Académie de Guyane · Économie-Gestion voie professionnelle', gauche, 288);
doc.text('Généré le ' + new Date().toLocaleDateString('fr-FR'), droite, 288, { align: 'right' });
};

doc.setFont('helvetica', 'bold'); doc.setFontSize(8); doc.setTextColor(...MU);
doc.text('ACADÉMIE DE GUYANE', 105, y, { align: 'center', charSpace: 0.6 });
y += 9;
doc.setFont('times', 'bold'); doc.setFontSize(23); doc.setTextColor(...INK);
doc.text('Convocation à une formation', 105, y, { align: 'center' });
y += 7;
doc.setFont('times', 'italic'); doc.setFontSize(13); doc.setTextColor(...G);
doc.text('Économie-Gestion voie professionnelle', 105, y, { align: 'center' });
y += 7;
doc.setDrawColor(...INK); doc.setLineWidth(0.4); doc.line(gauche, y, droite, y);
y += 11;

doc.setFont('times', 'bold'); doc.setFontSize(15); doc.setTextColor(...INK);
const th = doc.splitTextToSize(d.theme, largeur);
doc.text(th, gauche, y);
y += 7 * th.length + 3;

const infos = [['Formateur(s)', d.formateurs]];
if (d.dispositif) infos.push(['Dispositif', d.dispositif]);
if (d.module) infos.push(['Module', d.module]);
infos.push(['Date', formatDate(d.date)], ['Horaires', d.horaires || 'non précisés'], ['Lieu', d.lieu]);
if (d.observations) infos.push(['Observations', d.observations]);

doc.setFontSize(10.5);
infos.forEach(([lib, val]) => {
doc.setFont('helvetica', 'bold'); doc.setTextColor(...INK);
doc.text(lib + ' :', gauche, y);
doc.setFont('helvetica', 'normal');
const l = doc.splitTextToSize(val, largeur - 34);
doc.text(l, gauche + 34, y);
y += 5.5 * l.length + 1;
});
y += 7;

doc.setFont('helvetica', 'bold'); doc.setFontSize(10); doc.setTextColor(...MU);
doc.text('STAGIAIRES CONVOQUÉS (' + d.stagiaires.length + ')', gauche, y, { charSpace: 0.4 });
y += 4;

const xN = gauche, xNom = gauche + 13, xMail = gauche + 90;
const entete = () => {
doc.setFillColor(...G); doc.rect(gauche, y, largeur, 8, 'F');
doc.setFont('helvetica', 'bold'); doc.setFontSize(9); doc.setTextColor(255, 255, 255);
doc.text('N°', xN + 3, y + 5.4);
doc.text('Nom et prénom', xNom + 2, y + 5.4);
doc.text('Adresse courriel', xMail + 2, y + 5.4);
y += 8;
};
entete();

doc.setFont('helvetica', 'normal'); doc.setFontSize(9.5);
d.stagiaires.forEach((s, i) => {
const nom = doc.splitTextToSize(s.nom || '-', 73);
const mail = doc.splitTextToSize(s.email || '-', droite - xMail - 4);
const hauteur = Math.max(nom.length, mail.length) * 4.6 + 3.4;
if (y + hauteur > 280) { pied(); doc.addPage(); y = 20; entete(); doc.setFont('helvetica', 'normal'); doc.setFontSize(9.5); }
if (i % 2) { doc.setFillColor(244, 242, 237); doc.rect(gauche, y, largeur, hauteur, 'F'); }
doc.setDrawColor(224, 222, 216); doc.setLineWidth(0.2); doc.line(gauche, y + hauteur, droite, y + hauteur);
doc.setTextColor(...INK);
doc.text(String(i + 1), xN + 3, y + 5);
doc.text(nom, xNom + 2, y + 5);
doc.text(mail, xMail + 2, y + 5);
y += hauteur;
});
pied();

doc.save(nomFichier(d, 'pdf'));
showMessage('Document PDF téléchargé.', 'ok');
});
}

/* Excel */
function exportExcel() {
avecBouton('btnExcel', d => {
const lignes = [
['Convocation à une formation — Économie-Gestion voie professionnelle'],
['Académie de Guyane'],
[],
['Thème', d.theme],
['Formateur(s)', d.formateurs],
['Dispositif', d.dispositif || ''],
['Module', d.module || ''],
['Date', formatDate(d.date)],
['Horaires', d.horaires || 'non précisés'],
['Lieu', d.lieu],
['Observations', d.observations || ''],
[],
['Stagiaires convoqués'],
['N°', 'Nom et prénom', 'Adresse courriel']
];
d.stagiaires.forEach((s, i) => lignes.push([i + 1, s.nom || '', s.email || '']));
lignes.push([], ['Total : ' + d.stagiaires.length + ' stagiaire(s)'], ['Document généré le ' + new Date().toLocaleDateString('fr-FR')]);

const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(lignes);
ws['!cols'] = [{ wch: 16 }, { wch: 36 }, { wch: 48 }];
ws['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }, { s: { r: 12, c: 0 }, e: { r: 12, c: 2 } }];
XLSX.utils.book_append_sheet(wb, ws, 'Formation');
XLSX.writeFile(wb, nomFichier(d, 'xlsx'));
showMessage('Document Excel téléchargé.', 'ok');
});
}

/* Agenda */
function exportICS() {
avecBouton('btnIcs', d => {
telecharger(new Blob([construireICS(d)], { type: 'text/calendar;charset=utf-8' }), nomFichier(d, 'ics'));
showMessage('Fichier agenda téléchargé : importez-le dans Framagenda ou tout autre calendrier.', 'ok');
});
}
</script>