/* Admin flow v2 — login + dashboard + respondents list + detail + invitations */ const { useState: useStateB, useMemo: useMemoB } = React; const PALETTE = [ "var(--byi-navy-800)", "var(--byi-orange-500)", "var(--byi-cyan-500)", "var(--byi-teal-500)", "var(--byi-coral-500)", "var(--byi-yellow-500)", "var(--byi-purple-500)", "var(--byi-navy-600)" ]; /* ============================================================ Aggregation specific to v2 schema ============================================================ */ function aggV2(respondents) { const count = (arr, key) => { const m = new Map(); arr.forEach(r => { const v = r[key]; if (Array.isArray(v)) v.forEach(x => m.set(x, (m.get(x) || 0) + 1)); else if (v != null && v !== "") m.set(v, (m.get(v) || 0) + 1); }); return [...m.entries()].map(([label, value]) => ({ label, value })).sort((a, b) => b.value - a.value); }; const users = respondents.filter(r => r.isUser); const skillHist = Array.from({ length: 10 }, (_, i) => ({ label: String(i + 1), value: respondents.filter(r => r.skill === i + 1).length })); const avgSkill = respondents.reduce((s, r) => s + r.skill, 0) / Math.max(respondents.length, 1); const dayCounts = Array.from({ length: 21 }, () => 0); respondents.forEach(r => { const days = Math.floor((new Date(2026, 4, 23) - r.submittedAt) / (1000 * 60 * 60 * 24)); const idx = 20 - Math.max(0, Math.min(20, days)); dayCounts[idx] += 1; }); // Themes — simple multi-select count const themesWeighted = (() => { const m = new Map(); respondents.forEach(r => (r.themes || []).forEach(t => m.set(t, (m.get(t) || 0) + 1))); return [...m.entries()].map(([label, value]) => ({ label, value })).sort((a, b) => b.value - a.value); })(); // Headcount per service totals + modules x service matrix const services = new Map(); const moduleServiceMatrix = new Map(); // service -> Map(module -> count) respondents.forEach(r => { if (!r.trainingPlan) return; Object.entries(r.trainingPlan).forEach(([s, plan]) => { services.set(s, (services.get(s) || 0) + (Number(plan.headcount) || 0)); if (!moduleServiceMatrix.has(s)) moduleServiceMatrix.set(s, new Map()); (plan.themes || []).forEach(m => { const ms = moduleServiceMatrix.get(s); ms.set(m, (ms.get(m) || 0) + 1); }); }); }); const headcountByService = [...services.entries()] .map(([label, value]) => ({ label, value })) .sort((a, b) => b.value - a.value); const modulesByService = [...moduleServiceMatrix.entries()] .map(([service, ms]) => ({ service, modules: [...ms.entries()].map(([label, value]) => ({ label, value })).sort((a, b) => b.value - a.value) })); // People-to-train per module: sum headcount of every (respondent × service) where the module is selected const moduleTrainees = (() => { const m = new Map(); respondents.forEach(r => { if (!r.trainingPlan) return; Object.values(r.trainingPlan).forEach(plan => { const h = Number(plan.headcount) || 0; if (h <= 0) return; (plan.themes || []).forEach(t => m.set(t, (m.get(t) || 0) + h)); }); }); return [...m.entries()].map(([label, value]) => ({ label, value })).sort((a, b) => b.value - a.value); })(); // Tool counts grouped by category const toolsByGroup = window.FESPA_DATA.catalog.TOOLS_GROUPS.map(g => ({ group: g.group, items: g.tools.map(t => ({ label: t, value: users.filter(r => r.tools.includes(t)).length })).sort((a, b) => b.value - a.value) })); // Licences: number of orgs with each tool + total licences const licCount = new Map(); const licSeats = new Map(); respondents.forEach(r => { (r.licences || []).forEach(l => { licCount.set(l.tool, (licCount.get(l.tool) || 0) + 1); licSeats.set(l.tool, (licSeats.get(l.tool) || 0) + l.count); }); }); const licencesData = [...licCount.entries()].map(([tool, orgs]) => ({ label: tool, orgs, seats: licSeats.get(tool) || 0 })).sort((a, b) => b.seats - a.seats); return { roles: count(respondents, "role"), sizes: count(respondents, "sizeLabel"), sectors: count(respondents, "sectors"), opcoSalaries: count(respondents, "opcoSalaries"), opcoDirigeant: count(respondents, "opcoDirigeant"), usage: count(respondents, "usage"), domains: count(users, "domains"), benefits: count(users, "benefits"), policy: count(respondents, "policy"), barriers: count(respondents, "barriers"), paidStatus: count(respondents.filter(r => r.isUser), "paidStatus"), wantsFormation: count(respondents, "wantsFormation"), budget: count(respondents.filter(r => r.wantsFormation !== "Non, pas pour l'instant"), "budgetLabel"), decider: count(respondents.filter(r => r.wantsFormation !== "Non, pas pour l'instant"), "decider"), pastTrained: respondents.filter(r => r.pastTraining).length, skillHist, avgSkill, dayCounts, themesWeighted, headcountByService, modulesByService, moduleTrainees, toolsByGroup, licencesData }; } /* ============================================================ CSV export ============================================================ */ function exportRespondentsCSV(respondents, catalog) { const headers = [ "ID", "Soumis le", "Prénom", "Nom", "Email", "Entreprise", "Rôle", "Effectif (tranche)", "Secteurs", "OPCO salariés", "OPCO dirigeant", "Niveau IA (/10)", "Usage IA", "Outils utilisés", "Statut licences payantes", "Outils sous licence (sièges)", "Domaines d'usage", "Bénéfices observés", "Politique IA", "Freins identifiés", "Formation IA déjà suivie", "Détails formation antérieure", "Intérêt formation", "Plan par service (apprenants × modules)", "Total apprenants", "Budget / personne", "Décideur formation", "Atelier témoignage", "Commentaire libre" ]; const escape = (v) => { if (v == null) return ""; const s = String(v); if (s.includes('"') || s.includes(";") || s.includes("\n") || s.includes(",")) { return '"' + s.replace(/"/g, '""') + '"'; } return s; }; const fmtDate = (d) => { const dt = d instanceof Date ? d : new Date(d); return `${String(dt.getDate()).padStart(2,"0")}/${String(dt.getMonth()+1).padStart(2,"0")}/${dt.getFullYear()} ${String(dt.getHours()).padStart(2,"0")}:${String(dt.getMinutes()).padStart(2,"0")}`; }; const rows = respondents.map(r => { const licStr = (r.licences || []).map(l => `${l.tool} (${l.count})`).join(" | "); const planStr = r.trainingPlan ? Object.entries(r.trainingPlan) .filter(([, p]) => Number(p.headcount) > 0 || (p.themes||[]).length > 0) .map(([s, p]) => `${s}: ${p.headcount} apprenant(s)${p.themes && p.themes.length ? " — " + p.themes.join(", ") : ""}`) .join(" | ") : ""; const pastStr = r.pastTraining ? `${r.pastTraining.title} (${r.pastTraining.organism}, ${r.pastTraining.year}, ${r.pastTraining.headcount} pers.)` : ""; return [ r.id, fmtDate(r.submittedAt), r.firstName, r.lastName, r.email, r.company, r.role, r.sizeLabel, (r.sectors || []).join(" | "), (r.opcoSalaries || []).join(" | "), (r.opcoDirigeant || []).join(" | "), r.skill, r.usage, (r.tools || []).join(" | "), r.paidStatus || "", licStr, (r.domains || []).join(" | "), (r.benefits || []).join(" | "), r.policy || "", (r.barriers || []).join(" | "), r.pastTraining ? "Oui" : "Non", pastStr, r.wantsFormation || "", planStr, r.trainTotal || 0, r.budgetLabel || "", r.decider || "", r.willingToShare || "", r.comment || "" ]; }); const sep = ";"; // semicolon: Excel FR friendly const lines = [headers.join(sep), ...rows.map(r => r.map(escape).join(sep))]; const csv = "\uFEFF" + lines.join("\r\n"); // BOM for Excel UTF-8 const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); const today = new Date(); const stamp = `${today.getFullYear()}${String(today.getMonth()+1).padStart(2,"0")}${String(today.getDate()).padStart(2,"0")}`; a.href = url; a.download = `FESPA-enquete-IA-${stamp}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); } window.exportRespondentsCSV = exportRespondentsCSV; /* ============================================================ Login ============================================================ */ function AdminLogin({ onLogin }) { const [email, setEmail] = useStateB("admin@fespa-france.fr"); const [pw, setPw] = useStateB("••••••••"); const [showHint, setShowHint] = useStateB(false); return (
Espace administrateur · accès sécurisé

Suivi de l'enquête
Usage de l'IA — 2026

{ e.preventDefault(); onLogin(); }}> {showHint && (
Un seul couple identifiant / mot de passe par compte FESPA France. Contactez le secrétariat pour une réinitialisation.
)}
); } /* ============================================================ Campaigns — Several surveys per year, each with its own questionnaire ============================================================ */ const FESPA_CAMPAIGNS = [ { id: "ia-2026", name: "Usage de l'IA — Édition 2026", period: "Mai → Juin 2026", startDate: "2026-05-05", endDate: "2026-06-12", status: "open", invited: 416, responded: 80, questionnaireId: "ia-v2", questionsCount: 20, blocks: 5, description: "Cartographier l'adoption de l'IA et identifier les besoins en formation des adhérents.", isPrimary: true }, { id: "conjoncture-q1-2026", name: "Conjoncture filière — T1 2026", period: "Mars → Avril 2026", startDate: "2026-03-01", endDate: "2026-04-15", status: "closed", invited: 416, responded: 248, questionnaireId: "conjoncture", questionsCount: 12, blocks: 3, description: "Baromètre trimestriel : carnet de commandes, prix matières, recrutement." }, { id: "salon-2025", name: "Bilan Salon FESPA Global 2025", period: "Octobre 2025", startDate: "2025-10-15", endDate: "2025-11-15", status: "closed", invited: 198, responded: 142, questionnaireId: "salon-bilan", questionsCount: 15, blocks: 4, description: "Bilan post-salon — satisfaction exposants, contacts générés, intentions 2026." }, { id: "formation-2027", name: "Plan de formation 2027 — Exploration", period: "Septembre → Octobre 2026 (prévue)", startDate: "2026-09-01", endDate: "2026-10-31", status: "draft", invited: 0, responded: 0, questionnaireId: "formation-2027", questionsCount: 8, blocks: 2, description: "Questionnaire en cours de rédaction — orientations pour le plan 2027." } ]; window.FESPA_CAMPAIGNS = FESPA_CAMPAIGNS; const ADMIN_AREAS = [ { id: "survey", label: "Suivi de l'enquête", icon: "📊", tabs: [ ["campaigns", "Campagnes"], ["dashboard", "Vue d'ensemble"], ["respondents", "Répondants"], ["invitations", "Invitations"] ] }, { id: "training", label: "Inscriptions aux formations", icon: "🎓", tabs: [ ["sessions", "Sessions"], ["enrollments", "Inscrits"], ["catalog", "Catalogue"] ] }, { id: "planning", label: "Rétro-planning", icon: "🗓", tabs: [ ["gantt", "Vue planning"], ["milestones", "Jalons clés"] ] } ]; function AdminShell({ area, tab, onArea, onTab, onLogout, campaignId, onCampaignChange, children }) { const current = ADMIN_AREAS.find(a => a.id === area) || ADMIN_AREAS[0]; const currentCampaign = FESPA_CAMPAIGNS.find(c => c.id === campaignId) || FESPA_CAMPAIGNS[0]; const showSwitcher = area === "survey" && tab !== "campaigns"; return (
Console administrateur
AF
Anne F. Direction · FESPA France
{current.label}
{showSwitcher && (
Campagne :
)}
{children}
); } function CampaignStatus({ status }) { const map = { open: ["var(--success)", "Ouverte"], closed: ["var(--byi-navy-300)", "Clôturée"], draft: ["var(--byi-orange-500)", "Brouillon"], scheduled: ["var(--byi-cyan-500)", "Programmée"] }; const [c, l] = map[status] || map.draft; return {l}; } function AdminCampaigns({ onSelectCampaign }) { const [showCreate, setShowCreate] = useStateB(false); return (
Toutes les campagnes

{FESPA_CAMPAIGNS.length} enquêtes — édition annuelle

{FESPA_CAMPAIGNS.map(c => { const rate = c.invited > 0 ? Math.round(c.responded / c.invited * 100) : 0; return (
{c.period}

{c.name}

{c.description}

Questions {c.questionsCount}
Blocs {c.blocks}
Invitations {c.invited > 0 ? c.invited : "—"}
Réponses {c.responded > 0 ? c.responded : "—"}
{c.invited > 0 && (
Taux de réponse {rate}%
)}
{c.status === "draft" ? ( ) : ( )}
); })}
{/* Create campaign modal */} {showCreate && (
setShowCreate(false)}>
e.stopPropagation()}>
Nouvelle campagne

Créer une nouvelle enquête

Une campagne correspond à une enquête diffusée auprès des adhérents. Chaque campagne a son propre questionnaire, sa propre liste d'invités et son propre tableau de résultats.

)}
); } function CampaignClosedNotice({ campaign }) { return (
📋
{campaign.period}

{campaign.name}

{campaign.description}

Statut
Questions{campaign.questionsCount}
Invitations{campaign.invited}
Réponses{campaign.responded} ({Math.round(campaign.responded/Math.max(campaign.invited,1)*100)}%)

{campaign.status === "closed" ? "Cette campagne est clôturée. Les données restent consultables et exportables. Cliquez sur les onglets « Répondants » et « Invitations » pour explorer les détails." : campaign.status === "draft" ? "Cette campagne est en cours de préparation — le questionnaire n'a pas encore été diffusé aux adhérents." : "Campagne en cours."}

); } /* ============================================================ Dashboard ============================================================ */ function AdminDashboard({ data, onOpenRespondent }) { const totals = data.totals; const agg = useMemoB(() => aggV2(data.respondents), [data.respondents]); const users = data.respondents.filter(r => r.isUser); const wantsFormation = data.respondents.filter(r => r.wantsFormation !== "Non, pas pour l'instant"); const satOrder = [ ["Oui, une charte ou politique formalisée", "var(--success)"], ["Oui, des règles informelles", "var(--byi-teal-500)"], ["Non, mais c'est en réflexion", "var(--byi-orange-500)"], ["Non, pas du tout", "var(--byi-coral-500)"] ]; return (
Vue d'ensemble · 23 mai 2026

Enquête « Usage de l'IA » — résultats en direct

● Enquête ouverte
{/* ===== Row 1 — Top KPIs ===== */}
Taux de réponse +9 cette semaine

{totals.responded} questionnaires saisis sur {totals.invited} adhérents invités.

Objectif comité : 60 %. Estimé atteint sous 8 jours.

Volume collaborateurs cumulé
{totals.collaborators.toLocaleString("fr-FR")}
salariés représentés au total
{agg.sizes.map((s, i) => (
{s.label} {s.value}
))}
Niveau IA moyen
{agg.avgSkill.toFixed(1)}/10
déclaré par les répondants
({ ...d, highlight: Number(d.label) === Math.round(agg.avgSkill) }))} height={120} color="var(--byi-navy-100)" accent="var(--byi-orange-500)" />
Courbe des réponses · 21 jours
+{agg.dayCounts.slice(-7).reduce((s, v) => s + v, 0)}
réponses sur les 7 derniers jours
il y a 21 jaujourd'hui
{/* ===== Row 2 — Profil & OPCO ===== */}
Profil — rôle des répondants ({ ...r, color: PALETTE[i % PALETTE.length] }))} height={22} /> Secteur d'activité ({ ...s, color: PALETTE[i % PALETTE.length] }))} height={22} />
OPCO de rattachement

Couverture par OPCO

Pour les salariés
({ ...o, color: i === 0 ? "var(--byi-orange-500)" : "var(--byi-navy-300)" }))} height={18} />
Pour le dirigeant
({ ...o, color: i === 0 ? "var(--byi-orange-500)" : "var(--byi-navy-300)" }))} height={18} />
{/* ===== Row 3 — Adoption + Tools by category ===== */}
Adoption de l'IA dans la filière

Où en sont les entreprises adhérentes ?

({ ...u, color: ["Oui, régulièrement","Oui, occasionnellement","Nous sommes en phase de test"].includes(u.label) ? "var(--byi-orange-500)" : "var(--byi-navy-300)" }))} height={26} />
{Math.round(100 * users.length / data.respondents.length)}%
utilisent déjà l'IA
{Math.round(100 * (agg.usage.find(u => u.label === "Nous sommes en phase de test") || {value:0}).value / data.respondents.length)}%
en phase de test
{Math.round(100 * (agg.usage.find(u => u.label === "Non, mais nous y réfléchissons") || {value:0}).value / data.respondents.length)}%
en réflexion
Outils utilisés · par catégorie

Top 3 par famille d'outils

{users.length} utilisateurs IA
{agg.toolsByGroup.map(g => (
{g.group}
i.value > 0).map((t, i) => ({ ...t, color: i === 0 ? "var(--byi-navy-800)" : "var(--byi-navy-300)" }))} height={16} />
))}
{/* ===== Row 4 — Licences payantes ===== */}
Licences payantes en place

Quels outils sont déjà budgétés dans la filière ?

{data.respondents.filter(r=>(r.licences||[]).length>0).length} entreprises avec licences
Statut licences (parmi utilisateurs IA)
({ ...p, color: p.label.startsWith("Oui") ? "var(--success)" : p.label === "En cours de réflexion" ? "var(--byi-orange-500)" : "var(--byi-navy-300)" }))} height={20} />
Top outils souscrits (sièges cumulés)
{agg.licencesData.slice(0, 8).map(l => ( ))}
OutilEntreprisesSièges totaux
{l.label} {l.orgs} {l.seats}
{/* ===== Row 5 — Intentions de formation (LE bloc clé) ===== */}
Intentions de formation · programmes

Quels sujets de formation IA intéressent les adhérents FESPA France

{totals.peopleToTrain.toLocaleString("fr-FR")}
personnes à former
~{Math.round(totals.trainingRevenue/1000).toLocaleString("fr-FR")} k€ potentiels
{/* Funnel of intent */}
{agg.wantsFormation.map((w, i) => { const pct = Math.round(w.value / data.respondents.length * 100); const isPos = w.label.startsWith("Oui"); return (
{pct}%
{w.label}
{w.value} adhérents
); })}
Thèmes plébiscités (nb de répondants)
({ ...t, color: i < 3 ? "var(--byi-orange-500)" : "var(--byi-orange-300)" }))} height={22} />
Apprenants à former · par module
({ ...t, color: i < 3 ? "var(--accent)" : "var(--byi-orange-300)" }))} height={22} />
Budget par personne
{agg.budget.map((b) => (
{b.label}
x.value))) * 100}%`, background: b.label.includes("Je ne sais pas") ? "var(--byi-navy-300)" : "var(--byi-orange-500)" }}>
{b.value}
))}
Personnes à former · par service
({ ...t, color: PALETTE[i % PALETTE.length] }))} height={18} />
Modules les plus demandés · par service
{agg.modulesByService .filter(ms => ms.modules.length > 0) .slice(0, 5) .map(ms => (
{ms.service}
{ms.modules.slice(0, 3).map((m, i) => ( {m.value} {m.label} ))}
))}
Décideur formation
{agg.decider.map((t) => (
{t.value} {t.label}
))}
  • {wantsFormation.length} adhérents intéressés
  • {totals.peopleToTrain.toLocaleString("fr-FR")} collaborateurs à former
{/* ===== Row 6 — Gouvernance, freins, déjà formés ===== */}
Politique d'usage IA en interne
{satOrder.map(([label, color]) => { const v = (agg.policy.find(p => p.label === label) || {value:0}).value; const pct = Math.round(v / data.respondents.length * 100); return (
{pct}%
{label.replace("Oui, une charte ou politique formalisée","Charte formalisée").replace("Oui, des règles informelles","Règles informelles").replace("Non, mais c'est en réflexion","En réflexion").replace("Non, pas du tout","Aucune")}
); })}
Principaux freins identifiés ({ ...b, color: i < 2 ? "var(--byi-coral-500)" : "var(--byi-navy-300)" }))} height={18} /> Maturité formation

{agg.pastTrained} adhérents ont déjà suivi une formation IA.

{data.respondents.length - agg.pastTrained} restent à former — cible prioritaire de la Commission.

Domaines d'usage
({ ...d, color: PALETTE[i % PALETTE.length] }))} height={14} />
{/* ===== Row 7 — Latest respondents ===== */}
Derniers questionnaires reçus

10 réponses les plus récentes

{[...data.respondents].sort((a,b)=>b.submittedAt-a.submittedAt).slice(0, 10).map(r => ( onOpenRespondent(r.id)}> ))}
Reçu leEntrepriseSecteurEffectif UsageNiveauPers. à former
{formatDate(r.submittedAt)} {r.company} {r.sectors.join(", ")} {r.sizeLabel.replace(" salariés","")} {r.skill}/10 {r.trainTotal > 0 ? {r.trainTotal} : }
); } function UsageDot({ usage }) { const map = { "Oui, régulièrement": ["var(--success)", "Régulier"], "Oui, occasionnellement": ["var(--byi-teal-500)", "Occasionnel"], "Nous sommes en phase de test": ["var(--byi-orange-500)", "Test"], "Non, mais nous y réfléchissons": ["var(--byi-yellow-500)", "Réflexion"], "Non, pas du tout": ["var(--byi-coral-500)", "Non"] }; const [color, label] = map[usage] || ["var(--fg-muted)", usage]; return {label}; } function formatDate(d) { const days = Math.floor((new Date(2026, 4, 23) - d) / (1000 * 60 * 60 * 24)); if (days === 0) return `Aujourd'hui · ${pad(d.getHours())}:${pad(d.getMinutes())}`; if (days === 1) return "Hier"; if (days < 7) return `il y a ${days} j`; return d.toLocaleDateString("fr-FR", { day: "2-digit", month: "short" }); } function pad(n) { return String(n).padStart(2, "0"); } /* ============================================================ Respondents list ============================================================ */ function AdminRespondents({ data, onOpen }) { const [search, setSearch] = useStateB(""); const [filter, setFilter] = useStateB("all"); const rows = data.respondents.filter(r => { const matchSearch = !search || [r.company, r.firstName, r.lastName, r.email, ...(r.sectors||[])].join(" ").toLowerCase().includes(search.toLowerCase()); const matchFilter = filter === "all" || (filter === "users" && r.isUser) || (filter === "non" && !r.isUser) || (filter === "intent" && r.wantsFormation !== "Non, pas pour l'instant" && r.trainTotal > 0) || (filter === "paid" && (r.licences || []).length > 0); return matchSearch && matchFilter; }).sort((a,b)=>b.submittedAt-a.submittedAt); return (
Tous les répondants

{data.respondents.length} questionnaires reçus

setSearch(e.target.value)} />
{[ ["all", `Tous (${data.respondents.length})`], ["users", `Utilisateurs IA (${data.respondents.filter(r=>r.isUser).length})`], ["non", `Non-utilisateurs (${data.respondents.filter(r=>!r.isUser).length})`], ["intent", `Avec intention formation (${data.respondents.filter(r=>r.wantsFormation!=="Non, pas pour l'instant"&&r.trainTotal>0).length})`], ["paid", `Avec licences payantes (${data.respondents.filter(r=>(r.licences||[]).length>0).length})`] ].map(([id, label]) => ( ))}
{rows.map(r => ( onOpen(r.id)}> ))}
ReçuEntrepriseRépondantSecteurEff. OPCO salariésUsage IANiv.Pers.Budget
{formatDate(r.submittedAt)} {r.company} {r.firstName} {r.lastName}
{r.role}
{r.sectors.join(", ")} {r.sizeLabel.replace(" salariés","")} {(r.opcoSalaries || [])[0] || } {r.skill} {r.trainTotal > 0 ? {r.trainTotal} : } {r.budget === "unsure" ? : {r.budgetLabel}}
); } /* ============================================================ Respondent detail ============================================================ */ function AdminRespondentDetail({ data, id, onBack, onNext, onPrev }) { const r = data.respondents.find(x => x.id === id); if (!r) return null; return (
Questionnaire · {r.id}

{r.company}

{r.firstName} {r.lastName} · {r.role} · {r.email}
{r.sectors.map(s => {s})} {r.sizeLabel} OPCO sal. : {r.opcoSalaries.join(", ")} OPCO dir. : {r.opcoDirigeant.join(", ")}
Reçu {formatDate(r.submittedAt)}
Usage actuel (Q5–Q10)
{Array.from({length:10},(_,i)=>( ))} {r.skill}/10
{r.tools.length ?
{r.tools.map(t => {t})}
: Aucun outil renseigné}
{r.paidStatus}
{r.licences.length > 0 && ( {r.licences.map(l => ( ))}
{l.tool}{l.count} sièges
)}
{r.domains.length ?
{r.domains.map(t => {t})}
: }
{r.benefits.length ?
{r.benefits.map(t => {t})}
: }
Gouvernance & freins (Q11–Q12)
{r.policy}
{r.barriers.length ?
{r.barriers.map(t => {t})}
: }
{r.pastTraining ? (
Intitulé{r.pastTraining.title}
Organisme{r.pastTraining.organism}
Effectif formé{r.pastTraining.headcount}
Année{r.pastTraining.year}
) : Aucune formation suivie}
Intention de formation (Q15–Q18)
{r.wantsFormation}
{r.trainTotal > 0 && r.trainingPlan && (
Plan de formation par service (Q16)
{Object.entries(r.trainingPlan) .filter(([, p]) => Number(p.headcount) > 0 || (p.themes||[]).length > 0) .map(([s, p]) => (
{s} {p.headcount} apprenant{p.headcount > 1 ? "s" : ""}
{(p.themes||[]).map(m => {m})} {(p.themes||[]).length === 0 && Aucun module sélectionné}
))}
TOTAL {r.trainTotal} apprenants
)}
Modules sélectionnés (Q16)
{r.themes.map(t => {t})} {r.themes.length === 0 && Aucun module}
Budget / personne (Q17){r.budgetLabel}
Décideur (Q18){r.decider}
Atelier témoignage (Q19){r.willingToShare}
Potentiel estimé{(r.trainTotal * r.budgetMid).toLocaleString("fr-FR")} €
{r.comment && ( Commentaire libre (Q20)

« {r.comment} »

)}
); } function Section({ title, children }) { return (
{title}
{children}
); } /* ============================================================ Invitations ============================================================ */ function AdminInvitations({ data }) { const [showAddModal, setShowAddModal] = useStateB(false); const [showImportModal, setShowImportModal] = useStateB(false); const [addedMembers, setAddedMembers] = useStateB([]); const [newMember, setNewMember] = useStateB({ company: "", contact: "", email: "", phone: "", role: "", region: "Île-de-France" }); const [toast, setToast] = useStateB(null); // ===== Import state ===== const [importStep, setImportStep] = useStateB("upload"); // upload | preview | done const [importRows, setImportRows] = useStateB([]); // [{company, firstName, lastName, email, phone, valid, errors}] const [importFileName, setImportFileName] = useStateB(""); const [importErrorCount, setImportErrorCount] = useStateB(0); const fileInputRef = React.useRef(null); const parseCSV = (text) => { // Robust CSV/TSV parsing — supports ; , or \t delimiters const lines = text.replace(/\r\n?/g, "\n").split("\n").filter(l => l.trim().length > 0); if (lines.length === 0) return []; const detectSep = (line) => { const counts = { ";": (line.match(/;/g) || []).length, ",": (line.match(/,/g) || []).length, "\t": (line.match(/\t/g) || []).length }; return Object.entries(counts).sort((a, b) => b[1] - a[1])[0][0]; }; const sep = detectSep(lines[0]); const parseLine = (l) => { // simple CSV parser respecting double-quoted fields const out = []; let cur = ""; let inQ = false; for (let i = 0; i < l.length; i++) { const c = l[i]; if (inQ) { if (c === '"' && l[i+1] === '"') { cur += '"'; i++; } else if (c === '"') inQ = false; else cur += c; } else { if (c === '"') inQ = true; else if (c === sep) { out.push(cur); cur = ""; } else cur += c; } } out.push(cur); return out.map(x => x.trim()); }; const header = parseLine(lines[0]).map(h => h.toLowerCase()); // find columns by name const findCol = (...names) => { for (const n of names) { const i = header.findIndex(h => h.includes(n)); if (i >= 0) return i; } return -1; }; const colCompany = findCol("société", "societe", "entreprise", "company"); const colFirstName = findCol("prénom", "prenom", "first"); const colLastName = findCol("nom", "last"); const colEmail = findCol("mail", "email", "courriel"); const colPhone = findCol("gsm", "tél", "tel", "phone", "mobile"); const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const rows = []; for (let i = 1; i < lines.length; i++) { const cells = parseLine(lines[i]); const r = { company: colCompany >= 0 ? cells[colCompany] || "" : "", firstName: colFirstName >= 0 ? cells[colFirstName] || "" : "", lastName: colLastName >= 0 ? cells[colLastName] || "" : "", email: colEmail >= 0 ? cells[colEmail] || "" : "", phone: colPhone >= 0 ? cells[colPhone] || "" : "", }; const errors = []; if (!r.company) errors.push("Société manquante"); if (!r.email) errors.push("Email manquant"); else if (!emailRe.test(r.email)) errors.push("Email invalide"); r.valid = errors.length === 0; r.errors = errors; rows.push(r); } return rows; }; const handleFileSelect = (file) => { if (!file) return; setImportFileName(file.name); const reader = new FileReader(); reader.onload = (e) => { const rows = parseCSV(e.target.result); setImportRows(rows); setImportErrorCount(rows.filter(r => !r.valid).length); setImportStep("preview"); }; reader.readAsText(file, "UTF-8"); }; const downloadTemplate = () => { const csv = "\uFEFF" + [ "Société;Prénom;Nom;Email;GSM", "Imprimerie Lemercier;Camille;Rousseau;c.rousseau@lemercier.fr;06 12 34 56 78", "Graphi-Print Solutions;Julien;Martin;j.martin@graphiprint.fr;06 98 76 54 32" ].join("\r\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "FESPA-import-adherents-modele.csv"; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); }; const confirmImport = () => { const valid = importRows.filter(r => r.valid).map(r => ({ company: r.company, contact: `${r.firstName} ${r.lastName}`.trim(), email: r.email, phone: r.phone, role: "Dirigeant", region: "Île-de-France" })); setAddedMembers(arr => [...valid, ...arr]); setToast(`✓ ${valid.length} adhérents importés depuis ${importFileName}`); setShowImportModal(false); setImportStep("upload"); setImportRows([]); setImportFileName(""); setTimeout(() => setToast(null), 5000); }; const closeImport = () => { setShowImportModal(false); setImportStep("upload"); setImportRows([]); setImportFileName(""); setImportErrorCount(0); }; const [showTestModal, setShowTestModal] = useStateB(false); const [testEmail, setTestEmail] = useStateB(""); const [testSubject, setTestSubject] = useStateB("Enquête FESPA France — Usage de l'IA dans votre entreprise"); const [testBody, setTestBody] = useStateB( `Bonjour, Dans le cadre des travaux de la Commission IA de FESPA France, nous lançons une enquête auprès des adhérents sur l'usage de l'IA. Temps estimé : 4 minutes — 20 questions Vos réponses seront analysées de manière anonyme. Répondre au questionnaire : https://enquete.fespa-france.fr/r/[lien-unique] Merci par avance, La Commission IA — FESPA France` ); const buildMailto = () => { if (!testEmail) return "#"; const subject = encodeURIComponent(testSubject); const body = encodeURIComponent(testBody); return `mailto:${encodeURIComponent(testEmail)}?subject=${subject}&body=${body}`; }; const buildGmail = () => { if (!testEmail) return "#"; const subject = encodeURIComponent(testSubject); const body = encodeURIComponent(testBody); return `https://mail.google.com/mail/?view=cm&fs=1&to=${encodeURIComponent(testEmail)}&su=${subject}&body=${body}`; }; const buildOutlookWeb = () => { if (!testEmail) return "#"; const subject = encodeURIComponent(testSubject); const body = encodeURIComponent(testBody); return `https://outlook.live.com/owa/?path=/mail/action/compose&to=${encodeURIComponent(testEmail)}&subject=${subject}&body=${body}`; }; const downloadEml = () => { if (!testEmail) return; const headers = [ `From: "FESPA France — Commission IA" `, `To: ${testEmail}`, `Subject: =?UTF-8?B?${btoa(unescape(encodeURIComponent(testSubject)))}?=`, `Date: ${new Date().toUTCString()}`, `MIME-Version: 1.0`, `Content-Type: text/plain; charset=UTF-8`, `Content-Transfer-Encoding: 8bit`, `` ].join("\r\n"); const eml = headers + "\r\n" + testBody.replace(/\r?\n/g, "\r\n"); const blob = new Blob([eml], { type: "message/rfc822" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `test-fespa-france.eml`; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 1000); setToast(`📄 Fichier .eml téléchargé — double-cliquez dessus pour l'ouvrir dans votre client mail`); setTimeout(() => setToast(null), 6000); }; const copyAll = () => { const txt = `À : ${testEmail}\nObjet : ${testSubject}\n\n${testBody}`; navigator.clipboard.writeText(txt); setToast("✓ Email complet copié dans le presse-papier — collez-le dans Brevo / Mailchimp"); setTimeout(() => setToast(null), 4000); }; const allCompanies = data.catalog.COMPANIES; // Expand to 416 by suffixing the catalog when needed (FESPA has 416 adherents total) const expandedCompanies = (() => { const out = []; const base = allCompanies; const cities = ["Paris", "Lyon", "Toulouse", "Bordeaux", "Lille", "Nantes", "Marseille", "Strasbourg", "Rennes", "Reims", "Tours", "Le Havre", "Grenoble", "Dijon", "Angers", "Clermont", "Nîmes", "Annecy", "Montpellier", "Aix", "Caen"]; for (let i = 0; i < 416; i++) { if (i < base.length) { out.push(base[i]); } else { const c = base[i % base.length]; const city = cities[Math.floor(i / base.length) % cities.length]; out.push(`${c} · ${city}`); } } return out; })(); const baseList = expandedCompanies.map((c, i) => { const responded = i < data.respondents.length ? data.respondents[i] : null; return { company: c, contact: responded ? `${responded.firstName} ${responded.lastName}` : pickContact(i), email: responded ? responded.email : (`contact@${c.toLowerCase().replace(/[^a-z]/g,"").slice(0,12)}.fr`), sentAt: new Date(2026, 4, 23 - 18 - (i % 3)), reminders: i % 4, status: responded ? "responded" : (i % 5 === 0 ? "opened" : (i % 7 === 0 ? "bounced" : "pending")), isNew: false }; }); const list = [ ...addedMembers.map(m => ({ ...m, sentAt: new Date(), reminders: 0, status: "pending", isNew: true })), ...baseList ]; const totals = { sent: list.length, responded: list.filter(l => l.status === "responded").length, opened: list.filter(l => l.status === "opened").length, pending: list.filter(l => l.status === "pending").length, bounced: list.filter(l => l.status === "bounced").length }; const handleAdd = (e) => { e.preventDefault(); if (!newMember.company || !newMember.email) return; setAddedMembers(arr => [{ ...newMember }, ...arr]); setToast(`Adhérent « ${newMember.company} » ajouté · invitation envoyée à ${newMember.email}`); setNewMember({ company: "", contact: "", email: "", phone: "", role: "", region: "Île-de-France" }); setShowAddModal(false); setTimeout(() => setToast(null), 4000); }; return (
Gestion des invitations

Liens personnalisés envoyés aux adhérents

{list.slice(0, 40 + addedMembers.length).map((l, i) => ( ))}
EntrepriseContactEmail Envoyé leRelancesStatut
{l.company} {l.isNew && Nouveau} {l.contact} {l.email} {formatDate(l.sentAt)} {l.reminders > 0 ? `${l.reminders}×` : } {l.status === "responded" ? Voir → : }
{/* ===== Add member modal ===== */} {showAddModal && (
setShowAddModal(false)}>
e.stopPropagation()} onSubmit={handleAdd}>
Nouvel adhérent

Ajouter un adhérent à l'enquête

L'adhérent recevra par email un lien personnalisé pour répondre au questionnaire. Les champs marqués * sont obligatoires.

)} {toast && (
{toast}
)} {/* ===== Test email modal ===== */} {showTestModal && (
setShowTestModal(false)}>
e.stopPropagation()}>
Test d'envoi d'email

Recevez l'email exactement comme l'adhérent

Saisissez votre adresse mail puis cliquez sur « Envoyer le test ». L'email s'ouvrira dans votre client de messagerie (Outlook, Gmail…) avec le sujet, le contenu et le lien personnalisé pré-remplis. Il vous suffit de cliquer sur Envoyer pour le recevoir et vérifier le rendu.

Méthode d'envoi du test

Choisissez le canal qui correspond à votre boîte mail. Le contenu et le destinataire sont pré-remplis automatiquement — il vous suffit de cliquer sur « Envoyer » dans votre messagerie.

💡 Pour l'envoi en masse aux 416 adhérents

Cette fonction est conçue pour tester le rendu. Pour l'envoi réel à l'ensemble du fichier, utilisez un service professionnel (Brevo, Mailchimp, Sarbacane) :

  • Importez le fichier exporté depuis « Exporter liste »
  • Collez le contenu copié ci-dessus comme template
  • Personnalisez avec {`{{prenom}}`} et le lien unique
  • Bénéficiez du suivi d'ouverture / clics / désinscription RGPD
)} {/* ===== Import database modal ===== */} {showImportModal && (
e.stopPropagation()}>
Importer la base d'adhérents

{importStep === "upload" && "Importer un fichier CSV / Excel"} {importStep === "preview" && `Aperçu — ${importRows.length} ligne${importRows.length > 1 ? "s" : ""}`}

{importStep === "upload" && ( <>

Le fichier doit contenir, au minimum, les colonnes suivantes (l'ordre n'a pas d'importance, les en-têtes sont reconnus automatiquement) :

{[ ["Société", "Nom de l'entreprise adhérente", true], ["Prénom", "Prénom du dirigeant / contact", false], ["Nom", "Nom du dirigeant / contact", false], ["Email", "Adresse mail principale", true], ["GSM", "Téléphone mobile", false] ].map(([name, hint, req]) => (
{name} {req && obligatoire}
{hint}
))}
fileInputRef.current && fileInputRef.current.click()} onDragOver={e => { e.preventDefault(); e.currentTarget.classList.add("is-over"); }} onDragLeave={e => e.currentTarget.classList.remove("is-over")} onDrop={e => { e.preventDefault(); e.currentTarget.classList.remove("is-over"); const f = e.dataTransfer.files[0]; if (f) handleFileSelect(f); }}>
📥
Cliquez ou déposez votre fichier ici
Formats acceptés : .csv, .txt (Excel : « Enregistrer sous » → CSV UTF-8)
handleFileSelect(e.target.files[0])} />
Séparateurs reconnus : virgule, point-virgule, tabulation
)} {importStep === "preview" && ( <>
{importRows.length - importErrorCount} lignes valides
{importErrorCount > 0 && (
{importErrorCount} ligne{importErrorCount>1?"s":""} en erreur
)}
{importRows.length} total fichier
📄 {importFileName}
{importRows.slice(0, 50).map((r, i) => ( ))}
SociétéPrénomNom EmailGSMÉtat
{r.valid ? "✓" : "⚠"} {r.company || "—"} {r.firstName || "—"} {r.lastName || "—"} {r.email || "—"} {r.phone || } {r.valid ? Prêt : {r.errors.join(" · ")}}
{importRows.length > 50 && (
… et {importRows.length - 50} autres lignes
)}
)}
)}
); } function StatusChip({ status }) { const map = { responded: ["var(--success)", "Répondu"], opened: ["var(--byi-orange-500)", "Ouvert · pas répondu"], pending: ["var(--byi-navy-300)", "En attente"], bounced: ["var(--byi-coral-500)", "Email invalide"] }; const [c, l] = map[status]; return {l}; } function pickContact(seed) { const FIRST = ["Sophie", "Julien", "Camille", "Antoine", "Marie", "Thomas", "Élodie", "Nicolas"]; const LAST = ["Martin", "Bernard", "Dubois", "Petit", "Durand", "Leroy", "Moreau"]; return `${FIRST[seed % FIRST.length]} ${LAST[seed % LAST.length]}`; } Object.assign(window, { AdminLogin, AdminShell, AdminDashboard, AdminRespondents, AdminRespondentDetail, AdminInvitations, AdminCampaigns, CampaignClosedNotice, ADMIN_AREAS, FESPA_CAMPAIGNS });