/* Admin — Modules Formations + Rétro-planning */
const { useState: useStateC, useMemo: useMemoC } = React;
/* ============================================================
TRAINING MODULES (catalog, sessions, enrollments)
============================================================ */
const TRAINING_CATALOG = [
{ code: "FIA-101", title: "Prise en main des outils IA (ChatGPT, Copilot)",
duration: 7, level: "Débutant", price: 950 },
{ code: "FIA-102", title: "IA générative pour la création visuelle (Firefly, Midjourney)",
duration: 14, level: "Intermédiaire", price: 1850 },
{ code: "FIA-103", title: "IA pour le marketing & la rédaction",
duration: 7, level: "Intermédiaire", price: 1100 },
{ code: "FIA-104", title: "Automatiser ses tâches administratives avec l'IA",
duration: 7, level: "Débutant", price: 990 },
{ code: "FIA-201", title: "RGPD, droits d'auteur & responsabilité IA",
duration: 4, level: "Tous niveaux", price: 750 },
{ code: "FIA-202", title: "Construire une politique IA pour son entreprise",
duration: 4, level: "Dirigeant", price: 850 },
{ code: "FIA-203", title: "IA pour la relation client et le commerce",
duration: 7, level: "Intermédiaire", price: 1100 }
];
const CITIES = ["Paris", "Lyon", "Toulouse", "Bordeaux", "Lille", "Distanciel"];
const TRAINERS = ["Marc Dubois", "Sophie Levrier", "Karim Benali", "Julie Mercier", "Antoine Cazenave"];
function buildSessions() {
// Build mock sessions for each catalog item with bookings driven by FESPA_DATA
const data = window.FESPA_DATA;
const respondents = data.respondents.filter(r => r.trainingPlan);
const SESS_DAYS = [
"2026-06-12", "2026-06-26", "2026-07-08", "2026-09-15", "2026-09-29",
"2026-10-13", "2026-10-27", "2026-11-10", "2026-11-24", "2026-12-08"
];
let _r = 12345;
const rng = () => { _r = (_r * 1664525 + 1013904223) >>> 0; return _r / 4294967296; };
const pick = (a) => a[Math.floor(rng() * a.length)];
const sessions = [];
let id = 1;
TRAINING_CATALOG.forEach((mod, mi) => {
// 2 sessions per module
[0, 1].forEach(s => {
const date = SESS_DAYS[(mi * 2 + s) % SESS_DAYS.length];
const city = pick(CITIES);
const trainer = pick(TRAINERS);
const capacity = 12 + Math.floor(rng() * 8); // 12-19
sessions.push({
id: `S${String(id).padStart(3,"0")}`,
moduleCode: mod.code,
moduleTitle: mod.title,
date,
city,
format: city === "Distanciel" ? "Distanciel" : "Présentiel",
trainer,
capacity,
enrolled: [], // filled below
status: "open"
});
id++;
});
});
// Build enrollments — every respondent with intent matches into modules based on themes
let enrId = 1;
respondents.forEach(r => {
Object.entries(r.trainingPlan || {}).forEach(([service, plan]) => {
const h = Number(plan.headcount) || 0;
if (h <= 0) return;
(plan.themes || []).forEach(t => {
// find the catalog item whose title best matches the theme
const mod = TRAINING_CATALOG.find(c => themeMatchesModule(t, c.title));
if (!mod) return;
const candidateSessions = sessions.filter(s => s.moduleCode === mod.code);
if (candidateSessions.length === 0) return;
// distribute h learners across both sessions, weighted toward first
let remaining = h;
candidateSessions.forEach((sess, si) => {
if (remaining <= 0) return;
const free = sess.capacity - sess.enrolled.length;
if (free <= 0) return;
const take = Math.min(free, si === 0 ? Math.ceil(h * 0.6) : remaining);
for (let k = 0; k < take && remaining > 0; k++) {
sess.enrolled.push({
id: `E${String(enrId++).padStart(4,"0")}`,
company: r.company,
service,
respondentId: r.id,
status: rng() < 0.7 ? "confirmed" : (rng() < 0.6 ? "pending" : "waiting"),
registeredAt: new Date(2026, 4, 1 + Math.floor(rng() * 22))
});
remaining--;
}
});
});
});
});
// Mark sessions as full
sessions.forEach(s => {
if (s.enrolled.length >= s.capacity) s.status = "full";
else if (s.enrolled.length >= s.capacity * 0.8) s.status = "almost_full";
});
return { sessions, catalog: TRAINING_CATALOG };
}
function themeMatchesModule(theme, modTitle) {
const t = theme.toLowerCase();
const m = modTitle.toLowerCase();
if (t.includes("prise en main") && m.includes("prise en main")) return true;
if (t.includes("création graphique") && m.includes("création")) return true;
if (t.includes("marketing") && m.includes("marketing")) return true;
if (t.includes("automatisation") && m.includes("automatiser")) return true;
if (t.includes("juridique") && m.includes("rgpd")) return true;
if (t.includes("politique") && m.includes("politique")) return true;
if (t.includes("relation client") && m.includes("relation client")) return true;
return false;
}
function getTrainingData() {
if (!window.__FESPA_TRAINING__) window.__FESPA_TRAINING__ = buildSessions();
return window.__FESPA_TRAINING__;
}
function formatSessionDate(iso) {
const d = new Date(iso);
return d.toLocaleDateString("fr-FR", { day: "2-digit", month: "short", year: "numeric" });
}
/* ---------- Sessions overview ---------- */
function AdminTrainingSessions({ data }) {
const { sessions, catalog } = getTrainingData();
const totals = {
sessions: sessions.length,
seatsTotal: sessions.reduce((s, x) => s + x.capacity, 0),
seatsTaken: sessions.reduce((s, x) => s + x.enrolled.length, 0),
full: sessions.filter(s => s.status === "full").length,
confirmed: sessions.reduce((s, x) => s + x.enrolled.filter(e => e.status === "confirmed").length, 0)
};
const fillRate = totals.seatsTaken / Math.max(totals.seatsTotal, 1);
return (
Sessions de formation
Calendrier 2026 — sessions ouvertes
● Inscriptions ouvertes
Taux de remplissage global
{Math.round(fillRate*100)}%
{totals.seatsTaken} apprenants inscrits sur {totals.seatsTotal} places ouvertes.
Objectif comité : 75 % de remplissage.
Sessions ouvertes
{totals.sessions}
{totals.full} complètes · {sessions.filter(s=>s.status==="almost_full").length} bientôt pleines
Inscrits confirmés
{totals.confirmed}
sur {totals.seatsTaken} pré-inscrits
Modules au catalogue
{catalog.length}
2 sessions ouvertes par module
| Code | Module | Date | Lieu |
Formateur | Places | Remplissage | Statut | |
{sessions
.sort((a,b) => new Date(a.date) - new Date(b.date))
.map(s => (
{s.moduleCode} {s.id} |
{s.moduleTitle} |
{formatSessionDate(s.date)} |
{s.city}
|
{s.trainer} |
{s.enrolled.length} / {s.capacity} |
|
|
→ |
))}
);
}
function SessionStatus({ status }) {
const map = {
open: ["var(--success)", "Ouverte"],
almost_full: ["var(--byi-orange-500)", "Bientôt pleine"],
full: ["var(--byi-coral-500)", "Complète"],
closed: ["var(--fg-muted)", "Clôturée"]
};
const [c, l] = map[status] || map.open;
return {l};
}
/* ---------- Enrollments list ---------- */
function AdminTrainingEnrollments({ data }) {
const { sessions } = getTrainingData();
const [search, setSearch] = useStateC("");
const [filter, setFilter] = useStateC("all");
const all = [];
sessions.forEach(s => {
s.enrolled.forEach(e => {
all.push({ ...e, sessionId: s.id, moduleCode: s.moduleCode, moduleTitle: s.moduleTitle,
date: s.date, city: s.city, trainer: s.trainer });
});
});
const rows = all.filter(e => {
const matchSearch = !search ||
[e.company, e.moduleTitle, e.moduleCode, e.service].join(" ").toLowerCase().includes(search.toLowerCase());
const matchFilter = filter === "all" || e.status === filter;
return matchSearch && matchFilter;
});
return (
Inscrits aux formations
{all.length} apprenants pré-inscrits
| Inscrit le | Apprenant / Entreprise | Service |
Module | Session | Lieu | Statut | |
{rows.slice(0, 80).map(e => (
| {new Date(e.registeredAt).toLocaleDateString("fr-FR")} |
{e.company}
Réf. répondant {e.respondentId}
|
{e.service} |
{e.moduleCode}
{e.moduleTitle}
|
{formatSessionDate(e.date)} |
{e.city} |
|
→ |
))}
);
}
function EnrollStatus({ status }) {
const map = {
confirmed: ["var(--success)", "Confirmé"],
pending: ["var(--byi-orange-500)", "En attente"],
waiting: ["var(--byi-yellow-500)", "Liste d'attente"],
cancelled: ["var(--byi-coral-500)", "Désisté"]
};
const [c, l] = map[status] || map.pending;
return {l};
}
/* ---------- Catalog ---------- */
function AdminTrainingCatalog({ data }) {
const { sessions, catalog } = getTrainingData();
// Match with enquête data: count number of intent learners per module
const respondents = data.respondents.filter(r => r.trainingPlan);
const moduleIntent = catalog.map(mod => {
let learners = 0;
let companies = new Set();
respondents.forEach(r => {
Object.values(r.trainingPlan || {}).forEach(plan => {
const h = Number(plan.headcount) || 0;
if (h <= 0) return;
if ((plan.themes || []).some(t => themeMatchesModule(t, mod.title))) {
learners += h;
companies.add(r.company);
}
});
});
const modSessions = sessions.filter(s => s.moduleCode === mod.code);
const enrolled = modSessions.reduce((s, x) => s + x.enrolled.length, 0);
const seats = modSessions.reduce((s, x) => s + x.capacity, 0);
return { ...mod, learners, companies: companies.size, enrolled, seats, sessions: modSessions.length };
});
return (
Catalogue des modules
{catalog.length} modules de formation IA
{moduleIntent.map(m => (
{m.code}
{m.title}
{m.level}
Durée{m.duration}h
Prix HT / pers.{m.price} €
Sessions{m.sessions}
Places{m.enrolled}/{m.seats}
Intentions issues de l'enquête
{m.learners} apprenants
· {m.companies} entreprises
Couverture inscription : {Math.round(m.enrolled / Math.max(m.learners,1) * 100)}% des intentions
))}
);
}
/* ============================================================
PLANNING — Gantt + jalons
============================================================ */
const PLANNING_PHASES = [
{ id: "P1", label: "Diffusion de l'enquête", owner: "Comm. FESPA France", start: "2026-05-05", end: "2026-06-05", status: "in_progress", color: "var(--byi-cyan-500)" },
{ id: "P2", label: "Relances & consolidation", owner: "Comm. IA", start: "2026-05-26", end: "2026-06-12", status: "in_progress", color: "var(--byi-orange-500)" },
{ id: "P3", label: "Analyse des résultats", owner: "BoostYourIA", start: "2026-06-08", end: "2026-06-26", status: "todo", color: "var(--byi-purple-500)" },
{ id: "P4", label: "Conception du programme", owner: "BoostYourIA · FESPA France", start: "2026-06-22", end: "2026-07-17", status: "todo", color: "var(--byi-teal-500)" },
{ id: "P5", label: "Validation OPCO & financement", owner: "FESPA France / OPCO", start: "2026-07-13", end: "2026-08-07", status: "todo", color: "var(--byi-navy-600)" },
{ id: "P6", label: "Communication aux adhérents", owner: "Comm. FESPA France", start: "2026-08-25", end: "2026-09-11", status: "todo", color: "var(--byi-coral-500)" },
{ id: "P7", label: "Inscriptions", owner: "FESPA France", start: "2026-09-15", end: "2026-10-31", status: "todo", color: "var(--byi-orange-500)" },
{ id: "P8", label: "Sessions de formation", owner: "Formateurs", start: "2026-09-22", end: "2026-12-18", status: "todo", color: "var(--success)" },
{ id: "P9", label: "Bilan & retour aux adhérents", owner: "Comm. IA", start: "2026-12-15", end: "2027-01-30", status: "todo", color: "var(--byi-navy-800)" }
];
const PLANNING_MILESTONES = [
{ date: "2026-05-23", label: "Aujourd'hui", tone: "now" },
{ date: "2026-06-12", label: "Clôture de l'enquête", tone: "key" },
{ date: "2026-06-26", label: "Restitution intermédiaire — Comité FESPA France", tone: "key" },
{ date: "2026-07-17", label: "Programme de formation arrêté", tone: "key" },
{ date: "2026-08-07", label: "Accords OPCO obtenus", tone: "key" },
{ date: "2026-09-15", label: "Ouverture des inscriptions", tone: "main" },
{ date: "2026-09-22", label: "Démarrage des premières sessions", tone: "main" },
{ date: "2026-12-18", label: "Dernière session de l'année", tone: "key" },
{ date: "2027-01-30", label: "Restitution finale et lancement vague 2", tone: "main" }
];
function AdminPlanningGantt() {
// Compute the timeline range
const startBound = new Date("2026-05-01");
const endBound = new Date("2027-02-15");
const totalDays = (endBound - startBound) / (1000 * 60 * 60 * 24);
// Months for header
const months = [];
let cur = new Date(startBound);
while (cur < endBound) {
months.push(new Date(cur));
cur = new Date(cur.getFullYear(), cur.getMonth() + 1, 1);
}
const today = new Date("2026-05-23");
const todayPct = ((today - startBound) / (1000 * 60 * 60 * 24)) / totalDays * 100;
const pct = (date) => ((new Date(date) - startBound) / (1000 * 60 * 60 * 24)) / totalDays * 100;
return (
Rétro-planning
Calendrier global Commission IA 2026
● En cours
{[
["Diffusion / comm.", "var(--byi-cyan-500)"],
["Analyse & contenu", "var(--byi-purple-500)"],
["Financement OPCO", "var(--byi-navy-600)"],
["Inscriptions", "var(--byi-orange-500)"],
["Sessions", "var(--success)"]
].map(([l, c]) => (
{l}
))}
Phase
{months.map((m, i) => {
const next = i+1 < months.length ? months[i+1] : endBound;
const left = ((m - startBound) / (1000*60*60*24)) / totalDays * 100;
const width = ((next - m) / (1000*60*60*24)) / totalDays * 100;
return (
{m.toLocaleDateString("fr-FR", { month: "short", year: "2-digit" })}
);
})}
{PLANNING_PHASES.map(p => {
const left = pct(p.start);
const right = pct(p.end);
const width = right - left;
return (
{new Date(p.start).toLocaleDateString("fr-FR", { day: "2-digit", month: "short" })}
{" → "}
{new Date(p.end).toLocaleDateString("fr-FR", { day: "2-digit", month: "short" })}
);
})}
Aujourd'hui · 23 mai
);
}
function AdminPlanningMilestones() {
return (
Jalons clés
Les dates à ne pas manquer
{PLANNING_MILESTONES.map((m, i) => {
const d = new Date(m.date);
return (
-
{d.toLocaleDateString("fr-FR", { day: "2-digit" })}
{d.toLocaleDateString("fr-FR", { month: "short" })}
{d.getFullYear()}
{m.label}
{m.tone === "now" ? "Position actuelle" : m.tone === "main" ? "Jalon majeur" : "Jalon"}
);
})}
);
}
Object.assign(window, {
AdminTrainingSessions, AdminTrainingEnrollments, AdminTrainingCatalog,
AdminPlanningGantt, AdminPlanningMilestones
});