Du premier brevet au diplome professionnel — decouvrez toutes nos formations federales et inscrivez-vous directement en ligne.
Parcours
Dans l'ordre, sans raccourci : chaque étape ouvre la suivante. Pour le CQP et le CCP, il faut vous inscrire deux fois : ① sur le catalogue de formation de la FFCK (Dendréo) et ② auprès du CRCK, sur ce site. Le MFPC est géré directement par le CRCK : une seule inscription, sur ce site (pas sur Dendréo).
Le point de départ du parcours.
Encadrer et certifier les Pagaies Couleurs, de la blanche à la bleue.
Le diplôme professionnel.
Compléter le CQP pour encadrer en eau vive.
Le MFPC est un diplôme fédéral qui atteste de la capacité à encadrer en autonomie pédagogique et en sécurité, de la Pagaie Blanche à la Pagaie Bleue (art. 1 et 2).
Les UC 1, 2 et 3 sont communes à tous. L’UC 4 comporte deux options :
Elle fonctionne en alternance :
Catalogue
Cliquez sur le bouton de la formation souhaitee pour acceder directement au formulaire d'inscription.
Il anime des seances d'initiation au canoe-kayak de la Pagaie Blanche a la Pagaie Verte. Organise par : les comites departementaux.
Eau Calme (EC) : encadre sur tous plans d'eau calme — Eau Vive (EV) : encadre en riviere jusqu'a la classe III. Formation organisee par le Comite Regional en poles Nord et Sud.
Premier diplome professionnel FFCK inscrit au RNCP. Permet l'enseignement contre remuneration. Encadre sur tout type de plan d'eau interieurs, dans le respect de la reglementation locale — au sein de riviere en eau vive de classe 1, ou en mer dans la bande des 300m.
La mise en oeuvre de la formation CQP Moniteur de Canoe-Kayak et Sports de Pagaie en Eau Calme est organisee par les CRCK, en parallele des Moniteurs Federaux Pagaies Couleurs (MFPC) en cours de formation.
Planning
Retrouvez toutes les dates. Mode gestionnaire actif : cliquez sur un mois pour ajouter ou supprimer des evenements. ☁ Synchronise avec Google Sheets
Personnes ressources
5 etapes simples — votre inscription est transmise directement par e-mail au CRCK NA, avec une copie pour vous.
Selectionnez votre formation
L'inscription au CQP EC se fait directement aupres de la FFCK, sur sa plateforme officielle. Vous pouvez neanmoins continuer a remplir ce formulaire pour informer le CRCK de votre demarche.
S'inscrire sur catalogue-ffck.dendreo.comType de formation MFPC
Souhaitez-vous aussi vous inscrire au CQP et/ou au CCP ? *
Réponse obligatoire. Si vous cochez le CQP et/ou le CCP, vos informations sont recopiées automatiquement dans leurs dossiers : vous ne remplissez qu’une seule fiche.
Option(s) CCP souhaitée(s) *
Option(s) CCP souhaitee(s)
Veuillez cocher au moins une option.
Informations personnelles
Tuteur pedagogique
Participant(e) mineur(e) — Autorisation parentale
Télécharger / imprimer la fiche d'autorisation parentale
Cochez ce dont vous disposez — les manquants ne bloquent pas l'inscription
Vous pouvez joindre ici vos pieces (licence, PSC1, certificat medical, attestation de natation, etc.) — PDF ou photo, 8 Mo max par fichier. Ce n'est pas obligatoire pour valider l'inscription ; vous pourrez aussi les envoyer plus tard par e-mail.
Sante et autorisation d'image
Relisez avant de confirmer
| Formation | — |
| Pole | — |
| Nom complet | — |
| Ne(e) le / Sexe | — |
| Licence FFCK | — |
| Club | — |
| Telephone | — |
| — | |
| Tuteur | — |
| PC Eau Calme | — |
| PC Eau Vive | — |
| PSC1 | — |
| Attestation natation | — |
| Licence jointe | — |
Votre demande a bien ete transmise. Une copie vous a ete adressee.
Un doute sur une date, un lieu, les pièces à fournir ? Écrivez-nous, nous vous répondons par e-mail.
Astuce : pour que le bouton ✉ "Convoquer" retrouve les bons inscrits, incluez un mot-cle de la formation dans le titre (ex. "MFPC", "CQP EC", "CCP EV").
Reserve aux gestionnaires du CRCK NA
Modifications appliquees immediatement et sauvegardees
Modifiez ici les dates et couts affiches sur chaque carte du catalogue public (AMFPC, MFPC EC/EV, CQP, CCP), ou ajoutez de nouvelles formations. Pensez a cliquer sur "Enregistrer et appliquer les modifications" en bas de cette fenetre pour valider vos changements.
Ajoutez, modifiez ou supprimez les dates affichees dans le calendrier public.
Pour chaque formation (et chaque pôle), indiquez la dernière date où l'on peut s'inscrire (incluse) et, si vous le souhaitez, une courte description de la session (dates, lieu). Après cette date, le site bloque l'inscription sur ce pôle et renvoie le candidat vers l'autre session si elle est encore ouverte. Laissez la date vide : pas de clôture.
Ajoutez librement une ou plusieurs cartes de presentation au catalogue "Nos formations" (titre, description, prerequis, dates, cout, lien).
Ajoutez, modifiez ou retirez librement les contacts affiches sur le site (visible par tous les visiteurs des l'enregistrement).
Vue d'ensemble immediate de toutes les inscriptions, avec les pieces/prerequis manquants en rouge.
Chargement...
Date | Formation | Pole | Nom | Prenom | DDN | Sexe | Licence | Club | Email | Telephone | Tuteur | PC_EC | PC_EV | PSC1 | Natation | Manquants | Tuteur_Email | Email_club/* ============================================================
CRCK Nouvelle-Aquitaine — Script Apps Script (v55 = v25 + extensions : referents, dossiers FFCK, poles, page Demarches, contacts CR/CD, acces CD limite, candidats voisins, boite a lettres, statuts du calendrier etendus, signature fixe retiree du script, bandeau d'info mis en forme, regime alimentaire et sexe dans le recapitulatif)
Changements vs v23 :
1. Les courriers affichent desormais les vrais logos du papier a
en-tete officiel : logo FFCK / Comite Nouvelle-Aquitaine en
haut, et en pied de page la decoration CRCK NA (coin gauche +
coin droit avec le logo CRCK) de part et d'autre des
coordonnees de l'organisme, plus les logos Region
Nouvelle-Aquitaine et Ministere Jeunesse et Sports / CNDS juste
au-dessus. Ces images sont hebergees comme fichiers statiques
sur le site (dossier logos/) et chargees par URL (reglages
ORG_LOGO_HEADER_URL / ORG_LOGO_FOOTER_LEFT_URL /
ORG_LOGO_FOOTER_RIGHT_URL / ORG_LOGO_REGION_URL /
ORG_LOGO_JS_URL dans Securite > Coordonnees generales, avec des
valeurs par defaut pointant vers le site Netlify). Si les
fichiers logos/ ne sont pas deployes, le courrier reste lisible
(seules les images manquent, le texte du pied de page demeure).
1bis. v25 : largeur/hauteur HTML explicites sur toutes les images
(corrige un vide affiche par Apple Mail/Outlook) ; pied de page
reorganise en 3 colonnes (deco gauche / coordonnees / deco +
logo CRCK a droite) pour coller a la mise en page du papier a
en-tete ; suppression du lien direct vers le tableur Google
Sheets dans les e-mails recapitulatifs (le tableur n'est pas
protege par departement — le lien donnait acces a toutes les
donnees a quiconque l'obtenait).
Changements vs v22 :
1. Courriers (confirmation, convocation, recapitulatif, codes
d'acces, messages...) reprennent desormais la presentation du
papier a en-tete officiel du CRCK : bloc "Suivi dossier" en
haut (nom/e-mail/telephone), et pied de page complet (adresse,
SIRET, telephone, e-mail, site web). Pour les courriers lies a
un Pole (confirmation, convocation, notification de
changement), le bloc "Suivi dossier" reprend automatiquement le
referent du Pole concerne (nom/e-mail/telephone, reglables dans
Securite > Personnalisation par Pole, qui gagne un champ
Telephone). Pour les autres courriers (recapitulatif, codes
d'acces, messages libres...), un contact par defaut est utilise
(reglable dans Securite > Coordonnees generales), de meme que
l'adresse/SIRET/telephone/e-mail/site affiches en pied de page.
2. Le departement est desormais demande a l'inscription pour
TOUTES les formations (MFPC/CQP-EC/CCP en plus d'AMFPC), pas
seulement AMFPC. Objectif : que le departement apparaisse aussi
pour ces candidats dans les statistiques/impressions, et que la
vue CD (compteurs par departement, y compris mutualisation)
reste coherente pour toutes les formations. Le routage
automatique d'e-mail vers le CD concerne (deja existant pour
AMFPC) s'applique donc aussi aux autres formations.
3. Interrupteurs pour l'espace CD, prets a l'emploi mais
desactives par defaut (en attente d'autorisation) :
- un interrupteur general (reglage CD_SITES_ENABLED, action
set_cd_global_enabled) qui coupe l'acces CD pour TOUS les
departements en un clic ;
- un interrupteur par departement (reglage CD_ACTIVE_<code>,
action set_cd_active) pour activer un departement a la fois,
par exemple en phase de test.
Tant que l'interrupteur general est sur "non", ou qu'un
departement n'est pas lui-meme actif, son code CD ne fonctionne
pas, meme s'il existe deja (findCdRoleByCode). list_cd_codes
renvoie desormais aussi l'etat de ces interrupteurs pour
affichage cote admin.
Changements vs v21 :
1. Validation gestionnaire avant envoi des convocations aux
candidats (reglage CONVOC_VALIDATION, active par defaut) :
l'envoi d'une convocation (action send_convocation) ne part
plus directement aux candidats. Un e-mail recapitulatif est
d'abord envoye aux destinataires du recapitulatif (CRCK et
adresses configurees), avec le detail de la session, la liste
des destinataires, et deux liens : l'un pour valider et
envoyer reellement la convocation (action approve_send_batch),
l'autre pour l'annuler sans rien envoyer (action
reject_send_batch). Le lot en attente est stocke temporairement
dans les proprietes du script (BATCH_<id>) puis supprime une
fois traite. Ce reglage peut etre desactive dans l'onglet
Securite ("Envoyer directement sans validation"). La
convocation automatique J-30 (autoSendConvocations) et le
renvoi suite a changement de date (resendConvocation) ne sont
pas concernes : ils continuent de partir directement, car
personne ne serait la pour valider a temps.
2. Filet de securite sur cette validation : si personne ne clique
sur aucun des deux liens dans les 48h (checkPendingBatches,
declenchee toutes les 6h via createBatchCheckTrigger, a
executer UNE SEULE FOIS depuis l'editeur Apps Script comme
createDailyTrigger), la convocation part automatiquement aux
candidats comme prevu par defaut, et les gestionnaires en sont
informes par e-mail.
Changements vs v20 :
1. Double niveau d'acces : le code CRCK (superviseur, comme avant)
coexiste desormais avec un code par Comite Departemental
(reglage CODE_CD_<departement>). L'action check_admin_code
renvoie maintenant le role (crck ou cd) et, pour un CD, son
departement.
2. "Code oublie" en libre-service (action forgot_code) : genere
un nouveau code aleatoire et l'envoie par e-mail (CRCK : aux
destinataires du recapitulatif ; CD : aux adresses configurees
pour son departement, CRCK toujours en copie). Plus besoin
d'intervention manuelle en cas de blocage.
3. Le CRCK garde une visibilite directe sur les codes CD
(action list_cd_codes, protegee par le code CRCK lui-meme) et
peut en regenerer ou en definir un manuellement (actions
generate_cd_code / set_cd_code), avec envoi automatique par
e-mail a la structure concernee. Un CD peut aussi changer son
propre code (set_cd_code_self).
4. list_registrations et edit_registration verifient desormais
le code transmis (parametre code / d.code) pour determiner le
role de l'appelant : un CD ne recoit les donnees nominatives
(nom, email, club...) que des candidats de son propre
departement ; pour les autres departements, seul un compteur
anonyme (departement, formation, statut) est renvoye. Un CD
ne peut modifier que les candidats de son departement, et ne
peut pas changer la formation/le pole/le departement d'une
inscription (reserve au CRCK).
Changements v20 vs v19 :
1. Code d'acces gestionnaire desormais stocke cote serveur
(reglage ADMIN_CODE, PropertiesService), et non plus
uniquement dans le navigateur. Nouvelles actions :
check_admin_code (verifie un code sans jamais le renvoyer,
utilisee a la connexion) et set_admin_code (change le code,
exige l'ancien code en plus du nouveau). Objectif : permettre
un changement de gestionnaire (depart en retraite, passation)
sans intervention sur le code source — le nouveau code est
valable immediatement sur tous les navigateurs/ordinateurs.
Valeur par defaut si jamais configuree : 'crck2026' (comme
avant), pour ne rien casser pour les installations existantes.
Changements v19 vs v18 :
1. Nouvelle colonne "Statut" (23e colonne du tableau
Inscriptions) : En attente / Absent / Recu / Ajourne. Valeur
par defaut "En attente" a l'inscription. Modifiable depuis
l'espace gestionnaire (tableau recapitulatif, menu deroulant
par ligne, ou via edit_registration). Sert de base aux
nouvelles vues "Statistiques" cote site (repartition des
candidatures AMFPC par departement, et suivi presentes/recus
par formation, annee, pole, departement).
Changements v18 vs v17 :
1. Nouvelle formation "AMFPC" selectionnable dans le formulaire
public, avec un champ Departement obligatoire (les 12
departements de la Nouvelle-Aquitaine). A l'inscription, en
plus du circuit habituel (tableur, confirmation candidat,
notification interne), un e-mail de candidature est envoye
automatiquement au(x) Comite(s) Departemental(aux) concerne(s)
(1 a plusieurs adresses par departement, configurables dans
l'onglet Messages : reglages "CD_<code departement>").
Nouvelle colonne "Departement" dans le tableau Inscriptions ;
list_registrations et edit_registration mis a jour en
consequence. Correction au passage : le bloc "Deja titulaire
de l'AMFPC ?" (jusqu'ici invisible car rattache par erreur a
la valeur "AMFPC" du formulaire, qui n'etait pas encore
selectionnable) s'affiche desormais pour MFPC, formation pour
laquelle cette question a du sens.
Changements v17 vs v16 :
1. Courriers de confirmation et de convocation desormais
personnalises par Pole (Nord/Sud) : encart colore avec
l'interlocuteur du pole (nom modifiable + e-mail, par defaut
dlaurent@ffck.org des deux cotes), adresse par defaut du pole
(utilisee comme lieu de convocation si l'evenement n'en a pas
de renseigne), texte d'introduction propre a chaque pole, et
signature adaptee. Tout ceci est configurable par le
gestionnaire depuis l'onglet Messages (nouveaux reglages
REFERENT_NORD_NOM/EMAIL, REFERENT_SUD_NOM/EMAIL, ADRESSE_NORD,
ADRESSE_SUD, CONFIRM_MSG_NORD, CONFIRM_MSG_SUD). Sans
personnalisation, un texte d'intro par defaut est utilise pour
chaque pole et l'e-mail de contact par defaut est
dlaurent@ffck.org.
Changements v16 vs v15 :
1. Nouvelle action "edit_registration" : permet au gestionnaire
de corriger directement depuis le site (sans passer par le
tableur) la Formation, le Pole, le Nom/Prenom, le Club,
l'e-mail et les pieces manquantes d'une inscription existante.
L'action list_registrations renvoie desormais aussi le
numero de ligne (rowIndex) de chaque inscription, necessaire
pour cibler la bonne ligne a corriger.
Changements v15 vs v14 :
1. Le Pole geographique (Nord/Sud) choisi par le candidat a
l'inscription est desormais visible par les gestionnaires :
une colonne "Pole" (avec badge colore Nord/Sud) a ete ajoutee
au tableau recapitulatif des inscrits (e-mail recapitulatif
et action list_registrations), la ou seule la Formation etait
visible auparavant.
Changements v14 vs v13 :
1. Les prix/dates des formations AMFPC, MFPC (EC/EV, Poles Nord/Sud),
CQP EC et CCP sont desormais sauvegardes cote serveur (Google
Sheets) et non plus seulement dans le navigateur du gestionnaire :
ils s'affichent maintenant correctement pour tous les visiteurs du
site, pas seulement sur l'ordinateur utilise pour les modifier.
Changements v13 vs v12 :
1. Adresse de contact / destinataire par defaut changee de
nouvelleaquitaine@ffck.org vers nouvelleaquitaine@ffck.org
(uniquement l'adresse affichee/destinataire par defaut ; le compte
Google qui envoie reellement les mails n'est pas modifie par ceci).
Changements v12 vs v11 :
1. Copie cachee (bcc) systematique vers le secretariat sur chaque
tentative d'envoi de la confirmation candidat (succes ou echec),
pour diagnostiquer les cas ou le mail candidat n'arrive jamais
(Gmail/Free) sans que le script ne recoive d'erreur.
Changements v11 vs v10 :
1. Retrait du diagnostic temporaire (depot de documents valide et
fonctionnel, autorisation Drive accordee).
2. Ajout d'un champ "E-mail de secours" a l'inscription (colonne
"EmailSecours" dans le tableau, affiche aussi dans l'e-mail
interne de notification).
Changements v9 vs v8 :
1. Depot de documents a l'inscription : le candidat peut joindre
ses pieces justificatives (PDF/photos) directement dans le
formulaire. Elles sont enregistrees automatiquement dans un
dossier Google Drive dedie a ce candidat (cree a la volee),
et le lien vers ce dossier est ajoute dans une nouvelle colonne
"Documents" du tableau Inscriptions, ainsi que dans l'e-mail
interne de notification. Les pieces manquantes (case a cocher
non cochee) continuent d'etre signalees en rouge comme avant,
independamment des documents deposes.
(Reprend integralement les fonctions de la v8 : statuts Annule /
Deplace, destinataires du recapitulatif configurables, boite a
lettres candidats, bandeau, recapitulatif colore, AutoOff,
confirmation candidat, convocations HTML, etc.)
============================================================ */
// -- Jeton de securite ---------------------------------------------------
function getSecretToken(){
return PropertiesService.getScriptProperties().getProperty('SECRET_TOKEN');
}
function isAuthorized(d){
var expected = getSecretToken();
if(!expected) return true;
return d && d.token === expected;
}
// -- Feuilles --------------------------------------------------------------
function getInscSheet(){
var sh=SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
ensureColumn(sh,'Documents');
ensureColumn(sh,'EmailSecours');
return sh;
}
// -- Depot des pieces jointes dans un dossier Google Drive dedie -----------
// Renvoie l'URL du dossier cree (ou '' si aucun fichier fourni / erreur).
function driveRoot_(){
var f=getSetting('DRIVE_FOLDER','');
var m=String(f).match(/[-\w]{25,}/);
if(m) return DriveApp.getFolderById(m[0]);
var rootName='CRCK NA - Dossiers inscriptions';
var it=DriveApp.getFoldersByName(rootName);
return it.hasNext() ? it.next() : DriveApp.createFolder(rootName);
}
/* A executer UNE FOIS depuis l'editeur Apps Script (menu Executer) pour accorder l'acces a Google Drive */
function autoriserDrive(){
var r=DriveApp.getRootFolder();
SpreadsheetApp.getActiveSpreadsheet();
MailApp.getRemainingDailyQuota();
Logger.log('Autorisations accordees : Drive, Sheets, Mail. Dossier racine : '+r.getName());
}
/* A executer UNE FOIS pour autoriser l'appel a l'IA Gemini (acces externe) */
function autoriserGemini(){
var r=UrlFetchApp.fetch('https://generativelanguage.googleapis.com/',{muteHttpExceptions:true});
Logger.log('Acces externe autorise (code '+r.getResponseCode()+'). Vous pouvez enregistrer la cle Gemini dans le site.');
}
/* A executer UNE FOIS pour autoriser la lecture de la boite Gmail du compte du script (import des pieces recues par mail) */
function autoriserGmail(){
var n=GmailApp.getInboxUnreadCount();
Logger.log('Acces Gmail autorise ('+n+' message(s) non lus). Vous pouvez utiliser le bouton Recuperer les pieces recues par mail dans la fiche candidat.');
}
/* Depose des fichiers (envoyes depuis le site) dans le dossier Drive d'un candidat (cree si besoin) */
function dossierAdd_(d){
var sh=getInscSheet(), r=parseInt(d.rowIndex,10);
if(!r||r<2||r>sh.getLastRow()) return {ok:false,error:'candidat introuvable'};
var blobs=[], tot=0;
(d.files||[]).slice(0,20).forEach(function(f){
if(!f||!f.data) return;
var by=Utilities.base64Decode(String(f.data)); tot+=by.length;
if(tot>30*1024*1024) throw new Error('fichiers trop volumineux (30 Mo maximum au total)');
blobs.push(Utilities.newBlob(by,String(f.mimeType||'application/octet-stream'),String(f.name||'piece').replace(/[\\\/:*?"<>|]/g,'_').slice(0,120)));
});
if(!blobs.length) return {ok:false,error:'aucun fichier recu'};
var head=sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0];
var cDoc=head.indexOf('Documents')+1||20;
var row=sh.getRange(r,1,1,sh.getLastColumn()).getValues()[0];
var folder=null, m=String(row[cDoc-1]||'').match(/[-\w]{25,}/);
if(m){ try{ folder=DriveApp.getFolderById(m[0]); }catch(e){ folder=null; } }
if(!folder){
var stamp=Utilities.formatDate(new Date(),Session.getScriptTimeZone()||'Europe/Paris','yyyy-MM-dd_HHmm');
folder=driveRoot_().createFolder((String(row[3]||'')+' '+String(row[4]||'')).trim()+' - '+stamp);
sh.getRange(r,cDoc).setValue(folder.getUrl());
}
var added=[]; blobs.forEach(function(b){ folder.createFile(b); added.push(b.getName()); });
return {ok:true,added:added,folder:folder.getUrl()};
}
/* Cherche dans Gmail les pieces jointes envoyees par un candidat et les range dans son dossier Drive */
function mailImport_(d){
var sh=getInscSheet(), r=parseInt(d.rowIndex,10);
if(!r||r<2||r>sh.getLastRow()) return {ok:false,error:'candidat introuvable'};
var head=sh.getRange(1,1,1,sh.getLastColumn()).getValues()[0];
var cDoc=head.indexOf('Documents')+1||20, cSec=head.indexOf('EmailSecours')+1||21;
var row=sh.getRange(r,1,1,sh.getLastColumn()).getValues()[0];
var nom=String(row[3]||''), prenom=String(row[4]||'');
var addrs=[String(row[9]||''),String(row[cSec-1]||'')].map(function(x){return x.trim().toLowerCase();}).filter(function(x){return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(x);});
if(!addrs.length) return {ok:false,error:"ce candidat n'a pas d'adresse e-mail"};
var q='has:attachment ('+addrs.map(function(a){return 'from:'+a;}).join(' OR ')+')';
var threads;
try{ threads=GmailApp.search(q,0,40); }catch(e){ return {ok:false,error:"Acces Gmail non autorise : lancez une fois la fonction autoriserGmail dans l'editeur Apps Script. ("+String(e.message||e)+")"}; }
var folder=null, docUrl=String(row[cDoc-1]||''), m=docUrl.match(/[-\w]{25,}/);
if(m){ try{ folder=DriveApp.getFolderById(m[0]); }catch(e){ folder=null; } }
var known={};
if(folder){ var it=folder.getFiles(); while(it.hasNext()){ var f0=it.next(); var ds=f0.getDescription(); if(ds) known[ds]=1; } }
var added=[], seen=0;
threads.forEach(function(th){
th.getMessages().forEach(function(msg){
var from=String(msg.getFrom()||'').toLowerCase();
if(!addrs.some(function(a){return from.indexOf(a)>-1;})) return;
msg.getAttachments({includeInlineImages:false,includeAttachments:true}).forEach(function(at){
seen++;
var key='gmail:'+msg.getId()+':'+at.getName();
if(known[key]) return;
if(!folder){
var stamp=Utilities.formatDate(new Date(),Session.getScriptTimeZone()||'Europe/Paris','yyyy-MM-dd_HHmm');
folder=driveRoot_().createFolder((nom+' '+prenom).trim()+' - '+stamp);
sh.getRange(r,cDoc).setValue(folder.getUrl());
}
var nf=folder.createFile(at.copyBlob());
nf.setDescription(key); known[key]=1;
added.push(at.getName());
});
});
});
return {ok:true,added:added,scanned:seen,folder:folder?folder.getUrl():'',addrs:addrs};
}
function geminiAsk_(prompt){
var key=getSetting('GEMINI_KEY','');
if(!key) return {ok:false,error:"Aucune cle Gemini enregistree (Reglages techniques du site)."};
var models=[getSetting('GEMINI_MODEL','')||'gemini-2.5-flash','gemini-flash-latest','gemini-2.5-flash-lite','gemini-flash-lite-latest'], last='', busy=false;
for(var i=0;i<models.length;i++){
for(var essai=0;essai<2;essai++){
try{
var r=UrlFetchApp.fetch('https://generativelanguage.googleapis.com/v1beta/models/'+models[i]+':generateContent',{
method:'post',contentType:'application/json',muteHttpExceptions:true,
headers:{'x-goog-api-key':key},
payload:JSON.stringify({contents:[{role:'user',parts:[{text:prompt}]}],generationConfig:{temperature:0.4,maxOutputTokens:1500}})
});
var code=r.getResponseCode(), txt=r.getContentText(), j={};
try{ j=JSON.parse(txt); }catch(e0){}
if(code===200){
var out=(((j.candidates||[])[0]||{}).content||{}).parts;
var t=(out||[]).map(function(p){return p.text||'';}).join('').trim();
if(t) return {ok:true,text:t};
last='reponse vide'; continue;
}
last='HTTP '+code+' '+((j.error&&j.error.message)||'').slice(0,200);
if(code===503||code===500){ busy=true; if(essai===0){ Utilities.sleep(2500); continue; } break; }
if(code===429) return {ok:false,error:"Quota gratuit Gemini atteint pour le moment, reessayez dans quelques minutes. ("+last+")"};
if(code===400||code===403) return {ok:false,error:"Cle Gemini refusee ou invalide : "+last};
}catch(e){ last=String(e); if(String(e).indexOf('UrlFetchApp')>=0||String(e).indexOf('autorisation')>=0||String(e).toLowerCase().indexOf('permission')>=0) return {ok:false,error:"Autorisation manquante : dans l'editeur Apps Script, executez la fonction autoriserGemini puis redeployez une nouvelle version. ("+e+")"}; }
break; }
}
if(busy) return {ok:false,busy:true,error:"Les serveurs Gemini sont surcharges en ce moment (votre cle est bonne). Reessayez dans quelques minutes."};
return {ok:false,error:'Gemini indisponible : '+last};
}
function aiReply_(d){
var q=String(d.question||'').slice(0,3000), ctx=String(d.context||'').slice(0,14000);
if(!q) return {ok:false,error:'question vide'};
var prompt="Tu es l'assistant du Comite Regional de Canoe-Kayak Nouvelle-Aquitaine (CRCK NA), charge de preparer des brouillons de reponse aux questions de personnes interessees par les formations (MFPC, CQP, CCP...). "+
"Un gestionnaire humain relira ta reponse avant envoi.\n\nREGLES : reponds en francais, ton chaleureux et professionnel, vouvoiement, court (5 a 12 lignes). "+
"Appuie-toi UNIQUEMENT sur les informations ci-dessous. Si l'information n'y figure pas, dis que tu vas verifier et reviendras vers la personne, sans inventer de date, de prix ou de regle. "+
"Commence par 'Bonjour "+String(d.nom||'').replace(/[^\wÀ-ÿ' -]/g,'').slice(0,60)+",' et termine par 'Cordialement,' puis 'CRCK Nouvelle-Aquitaine'. Pas de markdown, pas d'asterisques, texte simple avec des paragraphes.\n\n"+
"=== INFORMATIONS DU SITE (calendrier, demarches, formations) ===\n"+ctx+"\n\n=== MESSAGE RECU ===\n"+q+"\n\n=== TON BROUILLON DE REPONSE ===";
return geminiAsk_(prompt);
}
/* A executer UNE FOIS (menu Executer) : remplace dans tous les reglages enregistres l'ancienne adresse
formation@canoe-nouvelle-aquitaine.fr par nouvelleaquitaine@ffck.org et retire laurent.domi@gmail.com
au profit de dlaurent@ffck.org et laurent.domi@free.fr. Consultez ensuite le Journal d'execution. */
function majAdresses(){
var P=PropertiesService.getScriptProperties(), all=P.getProperties(), log=[];
var OLD='formation@canoe-nouvelle-aquitaine.fr', NEW='nouvelleaquitaine@ffck.org', GM='laurent.domi@gmail.com';
var ADD=['nouvelleaquitaine@ffck.org','dlaurent@ffck.org','laurent.domi@free.fr'];
function fix(v){
var items=String(v).split(',').map(function(x){return x.trim();}).filter(String), out=[], hadGm=false;
items.forEach(function(x){ var l=x.toLowerCase(); if(l===OLD) l=NEW; if(l===GM){ hadGm=true; return; } if(out.indexOf(l)<0) out.push(l); });
if(hadGm) ADD.forEach(function(a){ if(out.indexOf(a)<0) out.push(a); });
return out.join(',');
}
['RECAP_EMAILS','MSG_DEST','REPLY_FROM','FFCK_MAIL'].forEach(function(k){
if(all[k]!==undefined){ var nv=fix(all[k]); if(nv!==all[k]){ P.setProperty(k,nv); log.push(k+' : '+all[k]+' -> '+nv); } }
});
Object.keys(all).forEach(function(k){
if(['RECAP_EMAILS','MSG_DEST','REPLY_FROM','FFCK_MAIL'].indexOf(k)>=0) return;
var v=String(all[k]);
if(v.toLowerCase().indexOf(GM)>=0 || v.indexOf(OLD)>=0){ var nv=v.split(OLD).join(NEW).replace(new RegExp(GM,'ig'),'dlaurent@ffck.org'); P.setProperty(k,nv); log.push(k+' : adresse remplacee'); }
});
try{
var ss=SpreadsheetApp.getActiveSpreadsheet(), shs=ss.getSheets();
shs.forEach(function(sh){
if(sh.getName().indexOf('Inscriptions')===0) return;
var rg=sh.getDataRange(), vals=rg.getValues(), ch=0;
for(var r=0;r<vals.length;r++) for(var c=0;c<vals[r].length;c++){
var x=vals[r][c]; if(typeof x==='string' && (x.indexOf(OLD)>=0 || x.toLowerCase().indexOf(GM)>=0)){ vals[r][c]=x.split(OLD).join(NEW).replace(new RegExp(GM,'ig'),'dlaurent@ffck.org'); ch++; }
}
if(ch){ rg.setValues(vals); log.push('Feuille '+sh.getName()+' : '+ch+' cellule(s) modifiee(s)'); }
});
}catch(e){ log.push('Feuilles : '+e); }
Logger.log(log.length?log.join('\n'):'Rien a modifier : aucune ancienne adresse trouvee dans les reglages.');
}
/* Tous les mails du script passent ici : l'adresse de reponse par defaut est celle du comite (et non le compte Google qui heberge le script) */
function mailStrip_(list){
var ex=mailExcluded_();
return String(list||'').split(/[,;]+/).map(function(s){return s.trim();}).filter(function(s){return s && ex.indexOf(s.toLowerCase())<0;}).join(',');
}
function mailSend_(to,subject,body,opts){
opts=opts||{};
if(!(opts&&opts.keepExcluded)){ to=mailStrip_(to); if(!to) return; if(opts.cc) opts.cc=mailStrip_(opts.cc); if(opts.bcc) opts.bcc=mailStrip_(opts.bcc); }
else { delete opts.keepExcluded; }
if(!opts.replyTo){ var rt=String(getSetting('REPLY_DEFAULT','nouvelleaquitaine@ffck.org, dlaurent@ffck.org, laurent.domi@free.fr')).split(/[,;\s]+/).filter(function(a){return a.indexOf('@')>0;}).join(','); if(rt) opts.replyTo=rt; }
/* copie cachee systematique vers les gestionnaires (sauf si le quota du jour devient juste) */
try{
var cp=String(getSetting('COPY_TO','nouvelleaquitaine@ffck.org,dlaurent@ffck.org,laurent.domi@free.fr'));
if(cp.trim()!=='-' && MailApp.getRemainingDailyQuota()>40){
var have=(String(to)+','+String(opts.cc||'')+','+String(opts.bcc||'')).toLowerCase().split(/[,;\s]+/).filter(String);
var add=cp.split(/[,;\s]+/).filter(function(a){ return a && a.indexOf('@')>0 && have.indexOf(a.toLowerCase())<0; });
if(add.length) opts.bcc=(opts.bcc?String(opts.bcc)+',':'')+add.join(',');
}
}catch(eC){}
MailApp.sendEmail(to,subject,body,opts);
}
/* ---- Clotures des inscriptions par formation et par pole (reglage CLOSURES, JSON) ---- */
function closuresGet_(){ try{ var o=JSON.parse(getSetting('CLOSURES','{}')); return (o&&typeof o==='object')?o:{}; }catch(e){ return {}; } }
function closureKeyFor_(label,map){
var t=String(label||'').toUpperCase(), std='';
if(/RECYCL/.test(t)) std='RECYCLAGE-MFPC'; else if(/\bH1\b/.test(t)) std='H1'; else if(/\bCCP\b/.test(t)) std='CCP'; else if(/\bCQP\b/.test(t)) std='CQP-EC'; else if(/\bAMFPC\b/.test(t)) std='AMFPC'; else if(/MFPC/.test(t)) std='MFPC';
if(std) return std;
var found='';
Object.keys(map||{}).forEach(function(k){ if(!found && k && t.indexOf(String(k).toUpperCase())>-1) found=k; });
return found;
}
function closureIsClosed_(entry){
if(!entry||!entry.date) return false;
var m=/^(\d{4})-(\d{2})-(\d{2})$/.exec(String(entry.date)); if(!m) return false;
return new Date()>=new Date(+m[1],+m[2]-1,+m[3]+1,0,0,0);
}
function closuresClean_(o){
var out={};
Object.keys(o||{}).slice(0,40).forEach(function(k){
var v=o[k]; if(!v||typeof v!=='object') return;
var kk=String(k).slice(0,80), e={};
['','Nord','Sud'].forEach(function(p){
var x=v[p]; if(!x||typeof x!=='object') return;
var d=/^\d{4}-\d{2}-\d{2}$/.test(String(x.date||''))?String(x.date):'', l=String(x.label||'').replace(/[<>]/g,'').slice(0,200);
if(d||l) e[p]={date:d,label:l};
});
if(Object.keys(e).length) out[kk]=e;
});
return out;
}
/* ---- Pieces jointes aux e-mails des stagiaires ---- */
function attFolder_(){ var root=driveRoot_(), it=root.getFoldersByName('Pieces jointes envois'); return it.hasNext()?it.next():root.createFolder('Pieces jointes envois'); }
function attBlobs_(files){
var out=[], total=0;
(files||[]).slice(0,8).forEach(function(f){
if(!f||!f.data) return;
var bytes=Utilities.base64Decode(String(f.data)); total+=bytes.length;
if(total>18*1024*1024) throw new Error('pieces jointes trop volumineuses (18 Mo maximum au total)');
var nm=String(f.name||'piece').replace(/[\\\/:*?"<>|]/g,'_').slice(0,120);
out.push(Utilities.newBlob(bytes,String(f.mimeType||'application/octet-stream'),nm));
});
return out;
}
function attSave_(files){ var blobs=attBlobs_(files), fo=attFolder_(), out=[]; blobs.forEach(function(b){ var fi=fo.createFile(b); out.push({id:fi.getId(),name:fi.getName()}); }); return out; }
function attBlobsFromIds_(ids){
var out=[], total=0;
(ids||[]).forEach(function(id){ try{ var b=DriveApp.getFileById(id).getBlob(); total+=b.getBytes().length; if(total<=20*1024*1024) out.push(b); }catch(e){} });
return out;
}
function convocAttList_(){ try{ var a=JSON.parse(getSetting('CONVOC_ATT','[]')); return Array.isArray(a)?a:[]; }catch(e){ return []; } }
function convocAttBlobs_(extraIds){ return attBlobsFromIds_(convocAttList_().map(function(x){return x.id;}).concat(extraIds||[])); }
function saveUploadedDocuments(d,candidateLabel){
if(!d.files || !d.files.length){
return '';
}
try{
var root=driveRoot_();
var stamp=Utilities.formatDate(new Date(),Session.getScriptTimeZone()||'Europe/Paris','yyyy-MM-dd_HHmm');
var subName=(candidateLabel||'Candidat')+' - '+stamp;
var sub=root.createFolder(subName);
// Tri alphabetique des fichiers avant depot (l'ordre d'affichage dans
// Drive suit deja l'alphabet par defaut, ceci fixe aussi l'ordre de creation).
var files=d.files.slice().sort(function(a,b){
return String(a.name||'').localeCompare(String(b.name||''),'fr',{sensitivity:'base'});
});
files.forEach(function(f){
if(!f || !f.data) return;
try{
var bytes=Utilities.base64Decode(f.data);
var blob=Utilities.newBlob(bytes, f.mimeType||'application/octet-stream', f.name||'document');
sub.createFile(blob);
}catch(fileErr){
// Un fichier illisible ne doit pas bloquer le reste de l'inscription.
}
});
return sub.getUrl();
}catch(err){
d._driveErr=String(err);
return '';
}
}
function getCalSheet(){
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sh=ss.getSheetByName('Calendrier');
if(!sh){ sh=ss.insertSheet('Calendrier'); sh.appendRow(['ID','Annee','Mois','Jour','Titre','Description','Couleur','Lieu','Heure']); }
ensureColumn(sh,'ConvocAuto');
ensureColumn(sh,'AutoOff');
ensureColumn(sh,'Statut');
return sh;
}
// Ajoute une colonne (par nom d'en-tete) si elle n'existe pas encore.
// Ne touche jamais aux colonnes existantes ni a leur ordre : compatible
// avec un tableur deja rempli.
function ensureColumn(sh,name){
var lastCol=Math.max(sh.getLastColumn(),1);
var headers=sh.getRange(1,1,1,lastCol).getValues()[0];
if(headers.indexOf(name)===-1){
sh.getRange(1,lastCol+1).setValue(name);
}
}
function colIndexByHeader(sh,name){
var lastCol=sh.getLastColumn();
var headers=sh.getRange(1,1,1,lastCol).getValues()[0];
return headers.indexOf(name); // 0-based, -1 si absent
}
function getConvocSheet(){
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sh=ss.getSheetByName('Convocations');
if(!sh){ sh=ss.insertSheet('Convocations'); sh.appendRow(['EventID','Titre','Date session','Emails envoyes','Date envoi']); }
return sh;
}
function findRegistrations(formationText){
var sheet=getInscSheet();
var vals=sheet.getDataRange().getValues();
var needle=(formationText||'').toLowerCase();
var out=[];
for(var i=1;i<vals.length;i++){
var row=vals[i];
var formation=String(row[1]||'').toLowerCase();
if(!formation && !row[3] && !row[4]) continue;
if(!needle || (formation && (formation.indexOf(needle)>-1 || needle.indexOf(formation)>-1))){
out.push({nom:row[3],prenom:row[4],email:row[9],tutemail:row[17]||'',pole:row[2]||'N/A'});
}
}
return out;
}
// -- Couleur par type de formation (recap + reference future) --------------
function formationColor(formationText){
var f=(formationText||'').toUpperCase();
if(f.indexOf('RECYCLAGE')>-1) return {bg:'#ECEFF1',fg:'#37474F',label:'Recyclage'};
if(f.indexOf('AMFPC')>-1) return {bg:'#E8F5E9',fg:'#1D8348',label:'AMFPC'};
if(f.indexOf('MFPC')>-1) return {bg:'#E3F2FD',fg:'#1565C0',label:'MFPC'};
if(f.indexOf('CCP')>-1) return {bg:'#F3E5F5',fg:'#6A1B9A',label:'CCP'};
if(f.indexOf('CQP')>-1) return {bg:'#FFF3E0',fg:'#E05A00',label:'CQP'};
return {bg:'#F5F5F5',fg:'#555',label:formationText||'\u2014'};
}
function poleColor(poleText){
var p=(poleText||'').toUpperCase();
if(p.indexOf('NORD')>-1) return {bg:'#E3F2FD',fg:'#1565C0',label:'Nord'};
if(p.indexOf('SUD')>-1) return {bg:'#FFF3E0',fg:'#E05A00',label:'Sud'};
return {bg:'#F5F5F5',fg:'#777',label:poleText||'N/A'};
}
// -- Recapitulatif colore de tous les inscrits (envoye a chaque inscription
// et/ou a la demande via l'action 'send_recap') -------------------------
function buildRecapHtml(){
var sheet=getInscSheet();
var vals=sheet.getDataRange().getValues();
var rows='';
for(var i=1;i<vals.length;i++){
var row=vals[i];
if(!row[3] && !row[4]) continue; // ligne vide
var formation=row[1]||'';
var col=formationColor(formation);
var pole=row[2]||'N/A';
var polC=poleColor(pole);
var manquants=row[16]||'';
var manqBlock = (manquants && manquants.toLowerCase()!=='aucun')
? '<span style="color:#C0392B;font-weight:700">'+esc(manquants)+'</span>'
: '<span style="color:#1D8348">complet</span>';
rows+='<tr style="background:'+col.bg+'">'
+'<td style="padding:6px 8px;font-weight:700;color:'+col.fg+';white-space:nowrap">'+esc(formation)+'</td>'
+'<td style="padding:6px 8px;white-space:nowrap"><span style="background:'+polC.bg+';color:'+polC.fg+';font-weight:700;padding:2px 6px;border-radius:4px">'+esc(polC.label)+'</span></td>'
+'<td style="padding:6px 8px">'+esc(row[3])+' '+esc(row[4])+'</td>'
+'<td style="padding:6px 8px">'+esc(row[8])+'</td>'
+'<td style="padding:6px 8px">'+manqBlock+'</td>'
+'</tr>';
}
if(!rows){
rows='<tr><td style="padding:10px;color:#888" colspan="5">Aucune inscription enregistree pour le moment.</td></tr>';
}
var table='<table style="width:100%;border-collapse:collapse;font-size:13px">'
+'<tr style="background:#003882;color:#fff">'
+'<td style="padding:6px 8px;font-weight:700">Formation</td>'
+'<td style="padding:6px 8px;font-weight:700">Pole</td>'
+'<td style="padding:6px 8px;font-weight:700">Candidat</td>'
+'<td style="padding:6px 8px;font-weight:700">Club</td>'
+'<td style="padding:6px 8px;font-weight:700">Pieces / prerequis</td>'
+'</tr>'
+rows
+'</table>';
return table;
}
// -- Utilitaires de mise en forme ------------------------------------------
function esc(s){ return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
function stripHtml(html){ return String(html).replace(/<[^>]*>/g,' ').replace(/ /g,' ').replace(/\s+/g,' ').trim(); }
var CRCK_ADDR = 'Maison Regionale des Sports — 2 avenue de l\'Universite — 33400 Talence';
var CRCK_MAIL = 'nouvelleaquitaine@ffck.org';
var CRCK_TEL = '05 40 05 01 31';
var DEFAULT_CONFIRM_MSG = 'Votre dossier est desormais entre les mains du secretariat du CRCK Nouvelle-Aquitaine. Vous recevrez par e-mail une convocation avec toutes les informations pratiques (lieu, horaires, materiel a prevoir) environ un mois avant le debut de la session.';
var DEFAULT_CONFIRM_MSG_NORD = 'Votre dossier est desormais entre les mains de l\'equipe du Pole Nord du CRCK Nouvelle-Aquitaine. Vous recevrez par e-mail une convocation avec toutes les informations pratiques (lieu, horaires, materiel a prevoir) environ un mois avant le debut de la session. Attention, il peut y avoir des modifications dans les dates.';
var DEFAULT_CONFIRM_MSG_SUD = 'Votre dossier est desormais entre les mains de l\'equipe du Pole Sud du CRCK Nouvelle-Aquitaine. Vous recevrez par e-mail une convocation avec toutes les informations pratiques (lieu, horaires, materiel a prevoir) environ un mois avant le debut de la session. Attention, il peut y avoir des modifications dans les dates.';
// -- Infos specifiques a un pole geographique (Nord/Sud) : referent,
// adresse par defaut, message d'intro. Modifiables par le gestionnaire
// (onglet Messages) ; email de secours = dlaurent@ffck.org par defaut.
function getPoleInfo(pole){
if(pole==='Nord'){
return {
label:'Nord', bg:'#E3F2FD', fg:'#1565C0',
nom:getSetting('REFERENT_NORD_NOM',''),
email:getSetting('REFERENT_NORD_EMAIL','dlaurent@ffck.org'),
tel:getSetting('REFERENT_NORD_TEL',''),
adresse:getSetting('ADRESSE_NORD',''),
msg:getSetting('CONFIRM_MSG_NORD','') || DEFAULT_CONFIRM_MSG_NORD
};
}
if(pole==='Sud'){
return {
label:'Sud', bg:'#FFF3E0', fg:'#E05A00',
nom:getSetting('REFERENT_SUD_NOM',''),
email:getSetting('REFERENT_SUD_EMAIL','dlaurent@ffck.org'),
tel:getSetting('REFERENT_SUD_TEL',''),
adresse:getSetting('ADRESSE_SUD',''),
msg:getSetting('CONFIRM_MSG_SUD','') || DEFAULT_CONFIRM_MSG_SUD
};
}
return null;
}
// -- Contact "suivi dossier" par defaut (courriers sans pole precis :
// recapitulatif, codes d'acces, statut, messages libres...). Reglable
// dans l'onglet Securite ("Coordonnees generales"). Par defaut, le
// secretariat CRCK tel qu'affiche sur le papier a en-tete officiel.
function getDefaultSuivi(){
var nom=getSetting('ORG_SUIVI_NOM','');
var email=getSetting('ORG_SUIVI_EMAIL','');
var tel=getSetting('ORG_SUIVI_TEL','');
if(!nom && !email && !tel) return null;
return {nom:nom,email:email,tel:tel};
}
// -- Departements de la Nouvelle-Aquitaine (routage des candidatures AMFPC) --
var DEPARTEMENTS = [
{code:'16',label:'Charente'},
{code:'17',label:'Charente-Maritime'},
{code:'19',label:'Correze'},
{code:'23',label:'Creuse'},
{code:'24',label:'Dordogne'},
{code:'33',label:'Gironde'},
{code:'40',label:'Landes'},
{code:'47',label:'Lot-et-Garonne'},
{code:'64',label:'Pyrenees-Atlantiques'},
{code:'79',label:'Deux-Sevres'},
{code:'86',label:'Vienne'},
{code:'87',label:'Haute-Vienne'}
];
function departementLabel(code){
var d=DEPARTEMENTS.filter(function(x){return x.code===String(code);})[0];
return d?d.label:'';
}
// Retourne la liste (array) des e-mails configures pour le CD d'un departement
// (reglage 'CD_<code>', valeurs separees par des virgules).
function getCdEmails(code){
var raw=getSetting('CD_'+code,'');
return raw.split(',').map(function(s){return s.trim();}).filter(function(s){return s;});
}
// -- Codes d'acces (CRCK superviseur + un par Comite Departemental) --------
function generateRandomCode(){
var chars='ABCDEFGHJKMNPQRSTUVWXYZ23456789'; // sans caracteres ambigus (0/O, 1/I/l)
var s='';
for(var i=0;i<6;i++) s+=chars.charAt(Math.floor(Math.random()*chars.length));
return s;
}
// L'espace CD complet (connexion, edition, etc.) est coupe tant que
// CD_SITES_ENABLED n'est pas active (interrupteur general) ET que le
// departement concerne n'est pas lui-meme actif (CD_ACTIVE_<code>). Les deux
// se reglent dans Securite > "Activer les espaces CD" ; par defaut, tout est
// desactive (fonctionnalite prete mais en attente d'autorisation).
function findCdRoleByCode(code){
if(!code) return null;
if(getSetting('CD_SITES_ENABLED','0')!=='1') return null;
for(var i=0;i<DEPARTEMENTS.length;i++){
var dc=DEPARTEMENTS[i].code;
if(getSetting('CD_ACTIVE_'+dc,'0')!=='1') continue;
var stored=getSetting('CODE_CD_'+dc,'');
if(stored && code===stored) return dc;
}
return null;
}
function sendCdCodeEmail(deptCode,code){
var emails=getCdEmails(deptCode);
if(!emails.length) return false;
var body='<p>Bonjour,</p>'
+'<p>Voici le code d\'acces a l\'espace de gestion en ligne du CRCK Nouvelle-Aquitaine pour le departement '
+'<strong>'+esc(deptCode)+' — '+esc(departementLabel(deptCode))+'</strong> :</p>'
+'<p style="font-size:22px;font-weight:700;letter-spacing:2px;color:#003882;background:#E3F2FD;padding:12px 18px;border-radius:8px;display:inline-block">'+esc(code)+'</p>'
+'<p>Rendez-vous sur le site des formations CRCK Nouvelle-Aquitaine, bouton « Espace Gestionnaire », pour l\'utiliser.</p>'
+'<p>Cordialement,<br>CRCK Nouvelle-Aquitaine</p>';
var shell=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Code d\'acces — Departement '+deptCode,body);
var opts={htmlBody:shell};
var recap=getRecapRecipients();
if(recap.length) opts.cc=recap.join(',');
mailSend_(emails.join(','),
'[CRCK NA] Code d\'acces — Departement '+deptCode+' ('+departementLabel(deptCode)+')',
stripHtml(shell), (opts.keepExcluded=true, opts));
return true;
}
function sendCrckCodeEmail(code,rappel){
var recap=getRecapRecipients();
if(!recap.length) return false;
var body='<p>Bonjour,</p>'
+'<p>'+(rappel?'Voici le code actuel d\'acces superviseur (CRCK) a l\'espace de gestion en ligne (rappel demande depuis l\'ecran de connexion) :':'Voici le nouveau code d\'acces superviseur (CRCK) a l\'espace de gestion en ligne :')+'</p>'
+'<p style="font-size:22px;font-weight:700;letter-spacing:2px;color:#003882;background:#E3F2FD;padding:12px 18px;border-radius:8px;display:inline-block">'+esc(code)+'</p>'
+'<p>Cordialement,<br>CRCK Nouvelle-Aquitaine</p>';
var shell=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Code d\'acces superviseur CRCK',body);
mailSend_(recap.join(','), (rappel?'[CRCK NA] Rappel du code d\'acces superviseur':'[CRCK NA] Nouveau code d\'acces superviseur'), stripHtml(shell), {htmlBody:shell});
return true;
}
// -- Reglages libres (stockes cote script, partages par tous les visiteurs) --
function getSetting(name,fallback){
var v=PropertiesService.getScriptProperties().getProperty(name);
return (v===null || v===undefined || v==='') ? (fallback!==undefined?fallback:'') : v;
}
function setSetting(name,value){
PropertiesService.getScriptProperties().setProperty(name,String(value==null?'':value));
}
// -- Destinataires du recapitulatif (liste modifiable, defaut CRCK_MAIL) --
function getRecapRecipients(){
var raw=getSetting('RECAP_EMAILS','');
var ex=mailExcluded_();
var list=raw.split(',').map(function(s){return s.trim();}).filter(function(s){return s && ex.indexOf(s.toLowerCase())<0;});
return list.length ? list : [CRCK_MAIL];
}
// -- Contacts (feuille dediee, remplace la liste figee) ---------------------
function getContactsSheet(){
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sh=ss.getSheetByName('Contacts');
if(!sh){
sh=ss.insertSheet('Contacts');
sh.appendRow(['ID','Nom','Role','Email','Telephone','Lien','LienLabel','Ordre','Type','Dept','Masque']);
// Seed avec les contacts historiques pour ne rien perdre au premier sync.
var seed=[
['c1','Laurent URVOIS','Gestionnaire de formation','lurvois@ffck.org','','','',1],
['c2','Yves NADUZZI','Entraineur regional','ynarduzzi@ffck.org','','','',2],
['c3','Dominique LAURENT','CRCK Nouvelle-Aquitaine','dlaurent@ffck.org','','','',3],
['c4','Secretariat CRCK NA','Maison Regionale des Sports \u2014 Talence','nouvelleaquitaine@ffck.org','05 40 05 01 31','','',4],
['c5','Comites Departementaux','Inscriptions AMFPC','','','https://canoe-nouvelle-aquitaine.fr/','S\'inscrire',5]
];
seed.forEach(function(r){ sh.appendRow(r); });
}
try{ if(sh.getLastColumn()<11){ sh.getRange(1,9,1,3).setValues([['Type','Dept','Masque']]); } }catch(e){}
return sh;
}
function readContacts(){
var sh=getContactsSheet();
var vals=sh.getDataRange().getValues();
var out=[];
for(var i=1;i<vals.length;i++){
var row=vals[i];
if(!row[1]) continue; // nom vide = ligne ignoree
out.push({id:row[0],nom:row[1],role:row[2]||'',email:row[3]||'',tel:row[4]||'',lien:row[5]||'',lienLabel:row[6]||'',ordre:row[7]||(i),type:(String(row[8]||'').toUpperCase()==='CD'?'CD':'CR'),dept:String(row[9]||''),masque:(String(row[10]||'')==='1')});
}
out.sort(function(a,b){ return (a.ordre||0)-(b.ordre||0); });
return out;
}
function writeContacts(list){
var sh=getContactsSheet();
var lastRow=sh.getLastRow();
if(lastRow>1) sh.getRange(2,1,lastRow-1,11).clearContent();
(list||[]).forEach(function(c,i){
var id='c'+(new Date().getTime())+i;
sh.getRange(i+2,1,1,11).setValues([[id,c.nom||'',c.role||'',c.email||'',c.tel||'',c.lien||'',c.lienLabel||'',i+1,(c.type==='CD'?'CD':'CR'),String(c.dept||''),(c.masque?'1':'0')]]);
});
}
// -- Formations supplementaires (cartes libres ajoutees au catalogue) -------
function getFormationsExtraSheet(){
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sh=ss.getSheetByName('FormationsExtra');
if(!sh){
sh=ss.insertSheet('FormationsExtra');
sh.appendRow(['ID','Badge','Titre','Description','Prerequis','Dates','Cout','LinkType','LinkUrl','LinkLabel','Ordre']);
}
return sh;
}
function readFormationsExtra(){
var sh=getFormationsExtraSheet();
var vals=sh.getDataRange().getValues();
var out=[];
for(var i=1;i<vals.length;i++){
var row=vals[i];
if(!row[2]) continue; // titre vide = ligne ignoree
out.push({id:row[0],badge:row[1]||'',titre:row[2],desc:row[3]||'',prereq:row[4]||'',dates:row[5]||'',cout:row[6]||'',linkType:row[7]||'form',linkUrl:row[8]||'',linkLabel:row[9]||'',ordre:row[10]||i});
}
out.sort(function(a,b){ return (a.ordre||0)-(b.ordre||0); });
return out;
}
function writeFormationsExtra(list){
var sh=getFormationsExtraSheet();
var lastRow=sh.getLastRow();
if(lastRow>1) sh.getRange(2,1,lastRow-1,11).clearContent();
(list||[]).forEach(function(f,i){
var id='f'+(new Date().getTime())+i;
sh.getRange(i+2,1,1,11).setValues([[id,f.badge||'',f.titre||'',f.desc||'',f.prereq||'',f.dates||'',f.cout||'',f.linkType||'form',f.linkUrl||'',f.linkLabel||'',i+1]]);
});
}
// suivi (optionnel) : {nom,email,tel} affiche en haut du courrier, comme le
// bloc "Suivi dossier" du papier a en-tete officiel du CRCK. Si omis, on
// utilise le contact par defaut regle dans Securite > Coordonnees generales
// (getDefaultSuivi) ; si rien n'est configure, le bloc est simplement absent.
// URLs par defaut des logos (heberges comme fichiers statiques sur le site,
// dossier logos/ a deployer a cote d'index.html). Reglables individuellement
// dans Securite > Coordonnees generales si les fichiers sont deplaces.
var DEFAULT_LOGO_BASE = 'https://formations-crck-nouvelle-aquitaine.netlify.app/logos/';
function emailShell(headerTitle,headerSubtitle,bodyHtml,suivi){
var s = suivi || getDefaultSuivi();
var suiviBlock = '';
if(s && (s.nom || s.email || s.tel)){
suiviBlock = '<div style="padding:0 24px 12px;font-size:12px;color:#444;line-height:1.6">'
+'<strong style="color:#003882">Suivi dossier :</strong><br>'
+(s.nom?esc(s.nom)+'<br>':'')
+(s.email?'<a href="mailto:'+esc(s.email)+'" style="color:#1565C0">'+esc(s.email)+'</a><br>':'')
+(s.tel?esc(s.tel):'')
+'</div>';
}
var orgAdresse=getSetting('ORG_ADRESSE',CRCK_ADDR.replace(/—/g,'-'));
var orgSiret=getSetting('ORG_SIRET','413 627 001 00022');
var orgTel=getSetting('ORG_TEL',CRCK_TEL);
var orgEmail=getSetting('ORG_EMAIL',CRCK_MAIL);
var orgWeb=getSetting('ORG_WEB','www.canoe-nouvelle-aquitaine.fr');
var logoHeader=getSetting('ORG_LOGO_HEADER_URL',DEFAULT_LOGO_BASE+'logo-header.png');
var logoFooterLeft=getSetting('ORG_LOGO_FOOTER_LEFT_URL',DEFAULT_LOGO_BASE+'logo-footer-left.png');
var logoFooterRight=getSetting('ORG_LOGO_FOOTER_RIGHT_URL',DEFAULT_LOGO_BASE+'logo-footer-right.png');
var logoRegion=getSetting('ORG_LOGO_REGION_URL',DEFAULT_LOGO_BASE+'logo-region-na.png');
var logoJs=getSetting('ORG_LOGO_JS_URL',DEFAULT_LOGO_BASE+'logo-js-cnds.png');
var headerLogoBlock = logoHeader
? '<div style="padding:18px 24px 6px"><img src="'+esc(logoHeader)+'" width="142" height="40" alt="FFCK - Comite Nouvelle-Aquitaine" style="height:40px;width:142px;display:block;border:0"></div>'
: '';
var footerSmallLogosRow = (logoRegion || logoJs)
? '<table role="presentation" style="margin:0 auto 6px" cellpadding="0" cellspacing="0"><tr>'
+(logoRegion?'<td style="padding:0 8px"><img src="'+esc(logoRegion)+'" width="30" height="33" alt="Region Nouvelle-Aquitaine" style="height:33px;width:30px;border:0"></td>':'')
+(logoJs?'<td style="padding:0 8px"><img src="'+esc(logoJs)+'" width="38" height="27" alt="Ministere charge des Sports / CNDS" style="height:27px;width:38px;border:0"></td>':'')
+'</tr></table>'
: '';
var orgTextBlock = '<div style="font-size:11px;color:#888;line-height:1.6">'
+footerSmallLogosRow
+'<strong style="color:#003882">Comite Regional de Canoe-Kayak Nouvelle-Aquitaine</strong><br>'
+esc(orgAdresse)+'<br>'
+'N° SIRET : '+esc(orgSiret)+' – Code APE : 9312Z<br>'
+'Tel : '+esc(orgTel)+' – <a href="mailto:'+esc(orgEmail)+'" style="color:#888">'+esc(orgEmail)+'</a> - '+esc(orgWeb)
+'</div>';
var footerRow = '<table role="presentation" width="100%" style="margin-top:16px;border-collapse:collapse" cellpadding="0" cellspacing="0"><tr>'
+(logoFooterLeft?'<td width="55" valign="bottom" style="padding:0"><img src="'+esc(logoFooterLeft)+'" width="55" height="65" alt="" style="display:block;border:0"></td>':'')
+'<td valign="bottom" align="center" style="padding:0 10px;text-align:center">'+orgTextBlock+'</td>'
+(logoFooterRight?'<td width="210" valign="bottom" align="right" style="padding:0"><img src="'+esc(logoFooterRight)+'" width="210" height="134" alt="CRCK Nouvelle-Aquitaine" style="display:block;border:0"></td>':'')
+'</tr></table>';
return '<div style="font-family:Arial,Helvetica,sans-serif;max-width:600px;margin:auto;color:#1a1a2e;background:#fff">'
+headerLogoBlock
+suiviBlock
+'<div style="background:#003882;color:#fff;padding:20px 24px;border-radius:8px 8px 0 0">'
+'<h2 style="margin:0;font-size:19px">'+esc(headerTitle)+'</h2>'
+'<p style="margin:4px 0 0;font-size:13px;opacity:.85">'+esc(headerSubtitle)+'</p>'
+'</div>'
+'<div style="border:1px solid #e0e0e0;border-top:none;padding:22px 24px;border-radius:0 0 8px 8px;font-size:14px;line-height:1.55">'
+bodyHtml
+'</div>'
+footerRow
+'</div>';
}
// -- Lettre de confirmation (accuse de reception) au candidat --------------
/* ---------- v36 : fiche candidat (lien personnel), types FFCK, modele de convocation ---------- */
var DEFAULT_SITE_URL = 'https://crck-na-formations.laurent-domi.workers.dev/';
function siteUrl_(){ return getSetting('SITE_URL',DEFAULT_SITE_URL); }
function ficheSecret_(){
var s=getSetting('FICHE_SECRET','');
if(!s){ s=Utilities.getUuid()+Utilities.getUuid(); setSetting('FICHE_SECRET',s); }
return s;
}
function ficheSig_(row,nom,prenom){
var raw=Utilities.computeHmacSha256Signature(String(row)+'|'+String(nom||'').toLowerCase()+'|'+String(prenom||'').toLowerCase(),ficheSecret_());
var hex=''; for(var i=0;i<raw.length;i++){ var b=(raw[i]<0?raw[i]+256:raw[i]).toString(16); hex+=(b.length<2?'0':'')+b; }
return hex.slice(0,20);
}
function ficheLink_(row,nom,prenom){
var u=siteUrl_(); if(u.charAt(u.length-1)!=='/' && u.indexOf('?')<0) u+='/';
return u+'?fiche='+row+'.'+ficheSig_(row,nom,prenom);
}
function ficheCheck_(r,sig){
var sh=getInscSheet(), row=parseInt(r,10);
if(!(row>1) || row>sh.getLastRow()) return null;
var v=sh.getRange(row,1,1,sh.getLastColumn()).getValues()[0];
if(!v[3] && !v[4]) return null;
if(String(sig||'')!==ficheSig_(row,v[3],v[4])) return null;
return {sh:sh,row:row,v:v};
}
function ficheGet_(r,sig){
var c=ficheCheck_(r,sig); if(!c) return {ok:false,error:'lien invalide ou expire'};
var ri=colIndexByHeader(c.sh,'Regime'), ai=colIndexByHeader(c.sh,'Allergies');
var f=String(c.v[1]||'');
return {ok:true,nom:c.v[3],prenom:c.v[4],formation:f,pole:c.v[2]||'',
regime:(ri>-1?String(c.v[ri]||''):''),allergies:(ai>-1?String(c.v[ai]||''):''),
needPole:/(^|[^A])MFPC|CQP|CCP|H1|RECYCLAGE/i.test(f)};
}
function ficheSet_(d){
var c=ficheCheck_(d.r,d.s); if(!c) return {ok:false,error:'lien invalide ou expire'};
var reg=String(d.regime||'').trim(), all=String(d.allergies||'').trim(), pole=String(d.pole||'').trim();
if(!reg || !all) return {ok:false,error:'regime alimentaire et allergies : reponse obligatoire (Non ou precision)'};
var f=String(c.v[1]||''), need=/(^|[^A])MFPC|CQP|CCP|H1|RECYCLAGE/i.test(f);
if(pole && pole!=='Nord' && pole!=='Sud') return {ok:false,error:'pole invalide'};
if(need && !pole && !c.v[2]) return {ok:false,error:'pole obligatoire (Nord ou Sud)'};
ensureColumn(c.sh,'Regime'); ensureColumn(c.sh,'Allergies');
c.sh.getRange(c.row,colIndexByHeader(c.sh,'Regime')+1).setValue(reg.slice(0,300));
c.sh.getRange(c.row,colIndexByHeader(c.sh,'Allergies')+1).setValue(all.slice(0,300));
var poleOld=String(c.v[2]||'');
if(pole && pole!==poleOld) c.sh.getRange(c.row,3).setValue(pole);
try{
mailSend_(getRecapRecipients().join(','),'[CRCK NA] Dossier complete par le candidat - '+c.v[3]+' '+c.v[4],
c.v[3]+' '+c.v[4]+' ('+f+') a complete son dossier :\n- Regime alimentaire : '+reg+'\n- Allergies : '+all+(pole?'\n- Pole : '+pole+(pole!==poleOld?' (ancienne valeur : '+(poleOld||'non renseigne')+')':''):''));
}catch(e){}
return {ok:true};
}
function sendFicheLinks_(force){
var sh=getInscSheet(); ensureColumn(sh,'Regime'); ensureColumn(sh,'Allergies'); ensureColumn(sh,'RelanceFiche');
var ri=colIndexByHeader(sh,'Regime'), ai=colIndexByHeader(sh,'Allergies'), li=colIndexByHeader(sh,'RelanceFiche');
var vals=sh.getDataRange().getValues(), n=0;
for(var i=1;i<vals.length;i++){
var v=vals[i];
if(!v[3] && !v[4]) continue;
var email=String(v[9]||'').trim(); if(!email) continue;
var need=/(^|[^A])MFPC|CQP|CCP|H1|RECYCLAGE/i.test(String(v[1]||'')) && !v[2];
var miss=!String(v[ri]||'').trim() || !String(v[ai]||'').trim() || need;
if(!miss) continue;
if(!force && li>-1 && v[li]) continue;
var link=ficheLink_(i+1,v[3],v[4]);
var body='<p>Bonjour '+esc(v[4])+' '+esc(v[3])+',</p>'
+'<p>Pour preparer au mieux la formation <strong>'+esc(v[1])+'</strong>, merci de completer votre dossier en repondant a quelques questions (regime alimentaire, allergies'+(need?', pole Nord ou Sud':'')+'). Cela prend moins d\'une minute :</p>'
+'<p style="margin:18px 0"><a href="'+esc(link)+'" style="background:#003882;color:#fff;padding:11px 20px;border-radius:7px;text-decoration:none;font-weight:bold">Completer mon dossier</a></p>'
+'<p style="font-size:13px;color:#555">Si le bouton ne fonctionne pas, copiez ce lien dans votre navigateur :<br>'+esc(link)+'</p>'
+'<p>Cordialement,<br>Le secretariat du CRCK Nouvelle-Aquitaine</p>';
var shell=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Completer votre dossier d\'inscription',body);
try{
mailSend_(email,'[CRCK NA] Merci de completer votre dossier - '+v[1],stripHtml(shell),{htmlBody:shell,name:'CRCK Nouvelle-Aquitaine'});
sh.getRange(i+1,li+1).setValue(new Date()); n++;
}catch(e){}
}
return n;
}
/* types de formation transmis a la FFCK (reglage FFCK_TYPES, defaut : MFPC seulement) */
function ffckTypeKey_(f){
f=String(f||'').toUpperCase();
if(f.indexOf('RECYCLAGE')>-1) return 'RECYCLAGE';
if(f.indexOf('AMFPC')>-1) return 'AMFPC';
if(f.indexOf('MFPC')>-1) return 'MFPC';
if(f.indexOf('CQP')>-1) return 'CQP';
if(f.indexOf('CCP')>-1) return 'CCP';
if(/(^|[^A-Z])H1/.test(f)) return 'H1';
return 'AUTRE';
}
function ffckTypes_(){ return getSetting('FFCK_TYPES','MFPC').split(',').map(function(x){return x.trim().toUpperCase();}).filter(Boolean); }
function ffckWants_(formation){ return ffckTypes_().indexOf(ffckTypeKey_(formation))>-1; }
/* modele editable de la convocation */
var DEFAULT_CONVOC_SUBJECT='[CRCK NA] Convocation - {formation} - {date}';
var DEFAULT_CONVOC_INTRO='Vous etes convoque(e) a la session suivante :';
var DEFAULT_CONVOC_OUTRO='Merci de confirmer votre presence aupres du secretariat.';
/* texte de convocation : texte simple OU HTML mis en forme (nettoye), avec marqueurs {prenom}... */
function convocFmt_(txt,vars){
txt=String(txt||'');
if(bannerLooksHtml_(txt)){
var h=sanitizeBannerHtml_(txt).html;
return h.replace(/\{(prenom|nom|formation|date|heure|lieu)\}/g,function(m,k){ return esc(vars&&vars[k]!==undefined?String(vars[k]):''); });
}
return esc(convocTpl_(txt,vars)).replace(/\n/g,'<br>');
}
function convocPlain_(txt){ return String(txt||'').replace(/<[^>]*>/g,'').replace(/ /g,' ').trim(); }
function convocTpl_(txt,vars){
return String(txt||'').replace(/\{(prenom|nom|formation|date|heure|lieu)\}/g,function(m,k){ return vars&&vars[k]!==undefined?String(vars[k]):''; });
}
function convocSubject_(title,dateLabel,tpl){
var t=(tpl&&tpl.subject)||getSetting('CONVOC_SUBJECT',DEFAULT_CONVOC_SUBJECT);
return convocTpl_(t,{formation:title,date:dateLabel});
}
function buildConfirmationHtml(d, missArr, confirmMsg){
var rows='';
rows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;width:130px;vertical-align:top">Formation</td><td>'+esc(d.formation)+'</td></tr>';
if(d.pole && d.pole!=='N/A') rows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Pole</td><td>'+esc(d.pole)+'</td></tr>';
rows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Club</td><td>'+esc(d.club)+'</td></tr>';
var missBlock='';
if(missArr && missArr.length){
missBlock='<div style="background:#FFF3E0;border-left:4px solid #E05A00;padding:12px 16px;margin:16px 0;border-radius:0 6px 6px 0">'
+'<strong>Pieces / prerequis en attente :</strong>'
+'<ul style="margin:8px 0 0;padding-left:20px">'
+missArr.map(function(m){return '<li>'+esc(m)+'</li>';}).join('')
+'</ul>'
+'<p style="margin:10px 0 0;font-size:13px">Cela n\'empeche pas la prise en compte de votre inscription — merci de nous transmettre ces elements des que possible, par retour d\'e-mail.</p>'
+'</div>';
} else {
missBlock='<div style="background:#E8F5E9;border-left:4px solid #1D8348;padding:12px 16px;margin:16px 0;border-radius:0 6px 6px 0">'
+'Votre dossier est complet, aucune piece manquante identifiee a ce stade.'
+'</div>';
}
// Infos propres au pole (Nord/Sud) : referent, adresse par defaut, message
// d'intro specifique. Null si le pole n'est pas connu (dossiers anciens).
var pInfo = getPoleInfo(d.pole);
var poleBlock='';
if(pInfo){
poleBlock='<div style="background:'+pInfo.bg+';border-left:4px solid '+pInfo.fg+';padding:12px 16px;margin:16px 0;border-radius:0 6px 6px 0">'
+'<strong>Interlocuteur Pole '+esc(pInfo.label)+'</strong><br>'
+(pInfo.nom?esc(pInfo.nom)+' — ':'')+'<a href="mailto:'+esc(pInfo.email)+'">'+esc(pInfo.email)+'</a>'
+(pInfo.adresse?('<br>'+esc(pInfo.adresse)):'')
+'</div>';
}
// Paragraphe editable par le gestionnaire (texte libre stocke cote script :
// CONFIRM_MSG_NORD / CONFIRM_MSG_SUD si le pole est connu, sinon
// CONFIRM_MSG general). Retombe sur un texte par defaut si vide.
var msgText = pInfo ? pInfo.msg : (confirmMsg && String(confirmMsg).trim() ? confirmMsg : DEFAULT_CONFIRM_MSG);
var msgHtml = '<p>'+esc(msgText).replace(/\n/g,'<br>')+'</p>';
var ficheBlock = d.ficheLink ? '<p style="font-size:13px;background:#F3F6FA;border-radius:6px;padding:10px 14px">Un oubli ou un changement (regime alimentaire, allergies, pole Nord/Sud) ? <a href="'+esc(d.ficheLink)+'">Completer ou modifier mon dossier</a>.</p>' : '';
var contactEmail = pInfo ? pInfo.email : CRCK_MAIL;
var closing = (pInfo && pInfo.nom)
? ('<p>Cordialement,<br>'+esc(pInfo.nom)+' — Pole '+esc(pInfo.label)+', CRCK Nouvelle-Aquitaine</p>')
: '<p>Cordialement,<br>Le secretariat du CRCK Nouvelle-Aquitaine</p>';
var body='<p>Bonjour '+esc(d.prenom)+' '+esc(d.nom)+',</p>'
+'<p>Nous vous confirmons la bonne reception de votre dossier d\'inscription :</p>'
+'<table style="width:100%;border-collapse:collapse;margin:14px 0">'+rows+'</table>'
+missBlock
+poleBlock
+msgHtml
+ficheBlock
+'<p>Pour toute question, vous pouvez nous contacter a <a href="mailto:'+contactEmail+'">'+contactEmail+'</a>'+(!pInfo?(' ou au '+CRCK_TEL):'')+'.</p>'
+closing;
return emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Confirmation de reception de dossier',body,
pInfo ? {nom:pInfo.nom,email:pInfo.email,tel:pInfo.tel} : null);
}
// -- Convocation (utilisee par l'envoi manuel ET automatique) --------------
// pole (optionnel, 'Nord'/'Sud') : personnalise le referent, l'adresse par
// defaut (si aucun lieu n'est renseigne sur l'evenement) et la signature.
function buildConvocationHtml(title,dateLabel,heure,lieu,desc,prenom,nom,pole,tpl){
var pInfo = getPoleInfo(pole);
var lieuFinal = lieu || (pInfo && pInfo.adresse) || '';
var rows='';
rows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;width:130px;vertical-align:top">Formation</td><td>'+esc(title)+'</td></tr>';
rows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Date</td><td>'+esc(dateLabel)+'</td></tr>';
if(heure) rows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Heure</td><td>'+esc(heure)+'</td></tr>';
if(lieuFinal) rows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Lieu</td><td>'+esc(lieuFinal)+'</td></tr>';
var descBlock = desc ? '<div style="background:#F3F6FA;border-left:4px solid #003882;padding:14px 16px;margin:16px 0;border-radius:0 6px 6px 0;white-space:pre-line">'+esc(desc)+'</div>' : '';
var poleBlock='';
if(pInfo && (pInfo.nom || pInfo.email)){
poleBlock='<div style="background:'+pInfo.bg+';border-left:4px solid '+pInfo.fg+';padding:12px 16px;margin:16px 0;border-radius:0 6px 6px 0">'
+'<strong>Interlocuteur Pole '+esc(pInfo.label)+'</strong><br>'
+(pInfo.nom?esc(pInfo.nom)+' — ':'')+'<a href="mailto:'+esc(pInfo.email)+'">'+esc(pInfo.email)+'</a>'
+'</div>';
}
var contactEmail = pInfo ? pInfo.email : CRCK_MAIL;
var closing = (pInfo && pInfo.nom)
? ('<p>Cordialement,<br>'+esc(pInfo.nom)+' — Pole '+esc(pInfo.label)+', CRCK Nouvelle-Aquitaine</p>')
: '<p>Cordialement,<br>CRCK Nouvelle-Aquitaine</p>';
var tv={prenom:prenom,nom:nom,formation:title,date:dateLabel,heure:heure,lieu:lieuFinal};
var introRaw=(tpl&&tpl.intro!==undefined)?tpl.intro:getSetting('CONVOC_INTRO',DEFAULT_CONVOC_INTRO);
var outroRaw=(tpl&&tpl.outro!==undefined)?tpl.outro:getSetting('CONVOC_OUTRO',DEFAULT_CONVOC_OUTRO);
var body='<p>Bonjour '+esc(prenom)+' '+esc(nom)+',</p>'
+'<div style="margin:0 0 12px">'+convocFmt_(introRaw,tv)+'</div>'
+'<table style="width:100%;border-collapse:collapse;margin:14px 0">'+rows+'</table>'
+descBlock
+poleBlock
+((convocPlain_(outroRaw) && convocPlain_(outroRaw)!=='-')?'<div style="margin:12px 0">'+convocFmt_(outroRaw,tv)+'</div>':'')
+'<p>Pour toute question : <a href="mailto:'+contactEmail+'">'+contactEmail+'</a>'+(!pInfo?(' · '+CRCK_TEL):'')+'</p>'
+closing;
return emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Convocation',body,
pInfo ? {nom:pInfo.nom,email:pInfo.email,tel:pInfo.tel} : null);
}
// Envoi reel des convocations aux candidats (appele soit directement, soit
// apres validation du gestionnaire via approve_send_batch). Retourne le
// nombre d'e-mails envoyes.
function doSendConvocationEmails(title,dateLabel,heure,lieu,desc,recipients,eventId,extraIds){
var subject=convocSubject_(title,dateLabel);
var sentEmails=[], atts=convocAttBlobs_(extraIds);
(recipients||[]).forEach(function(r){
if(!r.email) return;
var htmlBody=buildConvocationHtml(title,dateLabel,heure,lieu,desc,r.prenom,r.nom,r.pole);
var opts={htmlBody:htmlBody};
if(r.tutemail) opts.cc=r.tutemail;
if(atts.length) opts.attachments=atts;
mailSend_(r.email,subject,stripHtml(htmlBody),opts);
sentEmails.push(r.email+(r.tutemail?(' (cc: '+r.tutemail+')'):''));
});
if(eventId){
var csh=getConvocSheet();
csh.appendRow([eventId,title,dateLabel,sentEmails.join('; '),new Date()]);
var csh2=getCalSheet();
var cv2=csh2.getDataRange().getValues();
var caIdx2=colIndexByHeader(csh2,'ConvocAuto');
for(var k=1;k<cv2.length;k++){
if(String(cv2[k][0])===String(eventId)){ csh2.getRange(k+1,caIdx2+1).setValue(new Date()); break; }
}
}
return sentEmails.length;
}
// -- Requetes entrantes du site ---------------------------------------------
function doPostCore(e){
try{
var d=JSON.parse(e.postData.contents);
var protectedTypes = ['calendar_add','calendar_delete','calendar_edit','send_convocation','calendar_toggle_auto','contacts_save','settings_save','formations_extra_save','broadcast_banner','send_recap','send_candidate_message','send_status_notice','edit_registration','set_admin_code','list_cd_codes','generate_cd_code','set_cd_code','set_cd_code_self','set_cd_global_enabled','set_cd_active'];
if(protectedTypes.indexOf(d.type)>-1 && !isAuthorized(d)){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'unauthorized'})).setMimeType(ContentService.MimeType.JSON);
}
// -- Code d'acces : verification (CRCK ou CD, renvoie le role) ----------
if(d.type==='check_admin_code'){
var code0=String(d.code||'');
if(code0 && code0===getSetting('ADMIN_CODE','crck2026')){
return ContentService.createTextOutput(JSON.stringify({ok:true,role:'crck'})).setMimeType(ContentService.MimeType.JSON);
}
var cdDept0=findCdRoleByCode(code0);
if(cdDept0){
return ContentService.createTextOutput(JSON.stringify({ok:true,role:'cd',departement:cdDept0})).setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput(JSON.stringify({ok:false})).setMimeType(ContentService.MimeType.JSON);
}
// -- CRCK : change son propre code --
if(d.type==='set_admin_code'){
var curCode=getSetting('ADMIN_CODE','crck2026');
if(String(d.oldCode||'')!==curCode){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Code actuel incorrect'})).setMimeType(ContentService.MimeType.JSON);
}
var newCodeStr=String(d.newCode||'').trim();
if(newCodeStr.length<4){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Le nouveau code doit faire au moins 4 caracteres'})).setMimeType(ContentService.MimeType.JSON);
}
setSetting('ADMIN_CODE', newCodeStr);
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
// -- CRCK : voit tous les codes CD (visibilite en cas de blocage) --
if(d.type==='list_cd_codes'){
if(String(d.crckCode||'')!==getSetting('ADMIN_CODE','crck2026')){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'unauthorized'})).setMimeType(ContentService.MimeType.JSON);
}
var codesMap={};
var activeMap={};
DEPARTEMENTS.forEach(function(dp){
codesMap[dp.code]=getSetting('CODE_CD_'+dp.code,'');
activeMap[dp.code]=getSetting('CD_ACTIVE_'+dp.code,'0')==='1';
});
return ContentService.createTextOutput(JSON.stringify({ok:true,codes:codesMap,active:activeMap,globalEnabled:getSetting('CD_SITES_ENABLED','0')==='1'})).setMimeType(ContentService.MimeType.JSON);
}
// -- CRCK : interrupteur general (active/desactive TOUS les espaces CD) --
if(d.type==='set_cd_global_enabled'){
if(String(d.crckCode||'')!==getSetting('ADMIN_CODE','crck2026')){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'unauthorized'})).setMimeType(ContentService.MimeType.JSON);
}
setSetting('CD_SITES_ENABLED', d.enabled?'1':'0');
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
// -- CRCK : active/desactive un departement CD en particulier --
if(d.type==='set_cd_active'){
if(String(d.crckCode||'')!==getSetting('ADMIN_CODE','crck2026')){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'unauthorized'})).setMimeType(ContentService.MimeType.JSON);
}
var deptA=String(d.departement||'');
if(!deptA){ return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Departement manquant'})).setMimeType(ContentService.MimeType.JSON); }
setSetting('CD_ACTIVE_'+deptA, d.active?'1':'0');
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
// -- CRCK : regenere un code CD aleatoire et l'envoie par e-mail --
if(d.type==='generate_cd_code'){
if(String(d.crckCode||'')!==getSetting('ADMIN_CODE','crck2026')){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'unauthorized'})).setMimeType(ContentService.MimeType.JSON);
}
var deptG=String(d.departement||'');
if(!deptG){ return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Departement manquant'})).setMimeType(ContentService.MimeType.JSON); }
var newCodeG=generateRandomCode();
setSetting('CODE_CD_'+deptG,newCodeG);
var emailedG=sendCdCodeEmail(deptG,newCodeG);
return ContentService.createTextOutput(JSON.stringify({ok:true,code:newCodeG,emailed:emailedG})).setMimeType(ContentService.MimeType.JSON);
}
// -- CRCK : definit manuellement un code CD --
if(d.type==='set_cd_code'){
if(String(d.crckCode||'')!==getSetting('ADMIN_CODE','crck2026')){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'unauthorized'})).setMimeType(ContentService.MimeType.JSON);
}
var deptS=String(d.departement||'');
var codeS=String(d.code||'').trim();
if(!deptS || codeS.length<4){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Code trop court (4 caracteres minimum)'})).setMimeType(ContentService.MimeType.JSON);
}
setSetting('CODE_CD_'+deptS,codeS);
var emailedS=sendCdCodeEmail(deptS,codeS);
return ContentService.createTextOutput(JSON.stringify({ok:true,emailed:emailedS})).setMimeType(ContentService.MimeType.JSON);
}
// -- CD : change son propre code depuis son espace --
if(d.type==='set_cd_code_self'){
var deptSelf=String(d.departement||'');
var curSelf=getSetting('CODE_CD_'+deptSelf,'');
if(!deptSelf || !curSelf || String(d.oldCode||'')!==curSelf){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Code actuel incorrect'})).setMimeType(ContentService.MimeType.JSON);
}
var newSelf=String(d.newCode||'').trim();
if(newSelf.length<4){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Le nouveau code doit faire au moins 4 caracteres'})).setMimeType(ContentService.MimeType.JSON);
}
setSetting('CODE_CD_'+deptSelf,newSelf);
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
// -- Code oublie (CRCK ou CD) : regenere et envoie par e-mail, sans auth --
// -- Rappel du code actuel par e-mail (sans l'ecraser, 1 fois / 10 min, envoye uniquement aux adresses du comite) --
if(d.type==='send_code_reminder'){
var lastR=Number(getSetting('CODE_REMINDER_AT','0'))||0;
if(Date.now()-lastR<10*60*1000){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Un rappel vient d\'etre envoye. Patientez quelques minutes et verifiez vos e-mails.'})).setMimeType(ContentService.MimeType.JSON);
}
setSetting('CODE_REMINDER_AT',String(Date.now()));
var sentR=sendCrckCodeEmail(getSetting('ADMIN_CODE','crck2026'),true);
return ContentService.createTextOutput(JSON.stringify(sentR?{ok:true}:{ok:false,error:'Aucune adresse de destinataire configuree'})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='forgot_code'){
var deptF=String(d.departement||'').trim();
if(deptF){
if(DEPARTEMENTS.filter(function(x){return x.code===deptF;}).length===0){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Departement inconnu'})).setMimeType(ContentService.MimeType.JSON);
}
if(!getCdEmails(deptF).length){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:"Aucune adresse e-mail configuree pour ce departement : contactez le CRCK"})).setMimeType(ContentService.MimeType.JSON);
}
var codeF=generateRandomCode();
setSetting('CODE_CD_'+deptF,codeF);
sendCdCodeEmail(deptF,codeF);
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
var codeFC=generateRandomCode();
setSetting('ADMIN_CODE',codeFC);
var sentFC=sendCrckCodeEmail(codeFC);
if(!sentFC){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:"Aucune adresse de destinataire configuree"})).setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='edit_registration'){
var shE=getInscSheet();
var rowE=parseInt(d.rowIndex,10);
var lastRowE=shE.getLastRow();
if(rowE && rowE>1 && rowE<=lastRowE){
var callerCodeE=String(d.code||'');
var callerRoleE='none', callerDeptE='';
if(callerCodeE && callerCodeE===getSetting('ADMIN_CODE','crck2026')){
callerRoleE='crck';
} else {
var cdDeptE=findCdRoleByCode(callerCodeE);
if(cdDeptE){ callerRoleE='cd'; callerDeptE=cdDeptE; }
}
if(callerRoleE==='none' || callerRoleE==='cd'){ /* v28 : les CD consultent seulement */
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'unauthorized'})).setMimeType(ContentService.MimeType.JSON);
}
var rowDeptE=shE.getRange(rowE,22).getValue()||'';
if(callerRoleE==='cd' && rowDeptE!==callerDeptE){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'unauthorized'})).setMimeType(ContentService.MimeType.JSON);
}
if(d.formation!==undefined && callerRoleE==='crck') shE.getRange(rowE,2).setValue(d.formation);
if(d.pole!==undefined && callerRoleE==='crck') shE.getRange(rowE,3).setValue(d.pole||'N/A');
if(d.nom!==undefined) shE.getRange(rowE,4).setValue(d.nom);
if(d.prenom!==undefined) shE.getRange(rowE,5).setValue(d.prenom);
if(d.club!==undefined) shE.getRange(rowE,9).setValue(d.club);
if(d.email!==undefined) shE.getRange(rowE,10).setValue(d.email);
if(d.manquants!==undefined) shE.getRange(rowE,17).setValue(d.manquants);
if(d.departement!==undefined && callerRoleE==='crck') shE.getRange(rowE,22).setValue(d.departement);
if(d.statut!==undefined) shE.getRange(rowE,23).setValue(d.statut||'En attente');
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'ligne introuvable'})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='contacts_save'){
writeContacts(d.contacts||[]);
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='formations_extra_save'){
writeFormationsExtra(d.formations||[]);
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='settings_save'){
if(d.confirmMsg!==undefined) setSetting('CONFIRM_MSG',d.confirmMsg);
if(d.bannerMsg!==undefined) setSetting('BANNER_MSG',bannerStore_(d.bannerMsg));
if(d.bannerActive!==undefined) setSetting('BANNER_ACTIVE',d.bannerActive?'1':'');
if(d.convocValidation!==undefined) setSetting('CONVOC_VALIDATION',d.convocValidation?'1':'');
if(d.recapEmails!==undefined) setSetting('RECAP_EMAILS',d.recapEmails);
if(d.amfpcS!==undefined) setSetting('AMFPC_S',d.amfpcS);
if(d.amfpcC!==undefined) setSetting('AMFPC_C',d.amfpcC);
if(d.ecn!==undefined) setSetting('EC_N',d.ecn);
if(d.ecs!==undefined) setSetting('EC_S',d.ecs);
if(d.ecc!==undefined) setSetting('EC_C',d.ecc);
if(d.evn!==undefined) setSetting('EV_N',d.evn);
if(d.evs!==undefined) setSetting('EV_S',d.evs);
if(d.evc!==undefined) setSetting('EV_C',d.evc);
if(d.cqpd!==undefined) setSetting('CQP_D',d.cqpd);
if(d.cqpc!==undefined) setSetting('CQP_C',d.cqpc);
if(d.ccpevd!==undefined) setSetting('CCP_EVD',d.ccpevd);
if(d.ccpmerd!==undefined) setSetting('CCP_MERD',d.ccpmerd);
if(d.ccpc!==undefined) setSetting('CCP_C',d.ccpc);
if(d.referentNordNom!==undefined) setSetting('REFERENT_NORD_NOM',d.referentNordNom);
if(d.referentNordEmail!==undefined) setSetting('REFERENT_NORD_EMAIL',d.referentNordEmail);
if(d.referentNordTel!==undefined) setSetting('REFERENT_NORD_TEL',d.referentNordTel);
if(d.referentSudNom!==undefined) setSetting('REFERENT_SUD_NOM',d.referentSudNom);
if(d.referentSudEmail!==undefined) setSetting('REFERENT_SUD_EMAIL',d.referentSudEmail);
if(d.referentSudTel!==undefined) setSetting('REFERENT_SUD_TEL',d.referentSudTel);
if(d.adresseNord!==undefined) setSetting('ADRESSE_NORD',d.adresseNord);
if(d.adresseSud!==undefined) setSetting('ADRESSE_SUD',d.adresseSud);
if(d.confirmMsgNord!==undefined) setSetting('CONFIRM_MSG_NORD',d.confirmMsgNord);
if(d.confirmMsgSud!==undefined) setSetting('CONFIRM_MSG_SUD',d.confirmMsgSud);
if(d.cdEmails && typeof d.cdEmails==='object'){
DEPARTEMENTS.forEach(function(dep){
if(d.cdEmails[dep.code]!==undefined) setSetting('CD_'+dep.code,d.cdEmails[dep.code]);
});
}
if(d.orgAdresse!==undefined) setSetting('ORG_ADRESSE',d.orgAdresse);
if(d.orgSiret!==undefined) setSetting('ORG_SIRET',d.orgSiret);
if(d.orgTel!==undefined) setSetting('ORG_TEL',d.orgTel);
if(d.orgEmail!==undefined) setSetting('ORG_EMAIL',d.orgEmail);
if(d.orgWeb!==undefined) setSetting('ORG_WEB',d.orgWeb);
if(d.orgSuiviNom!==undefined) setSetting('ORG_SUIVI_NOM',d.orgSuiviNom);
if(d.orgSuiviEmail!==undefined) setSetting('ORG_SUIVI_EMAIL',d.orgSuiviEmail);
if(d.orgSuiviTel!==undefined) setSetting('ORG_SUIVI_TEL',d.orgSuiviTel);
if(d.orgLogoHeaderUrl!==undefined) setSetting('ORG_LOGO_HEADER_URL',d.orgLogoHeaderUrl);
if(d.orgLogoFooterLeftUrl!==undefined) setSetting('ORG_LOGO_FOOTER_LEFT_URL',d.orgLogoFooterLeftUrl);
if(d.orgLogoFooterRightUrl!==undefined) setSetting('ORG_LOGO_FOOTER_RIGHT_URL',d.orgLogoFooterRightUrl);
if(d.orgLogoRegionUrl!==undefined) setSetting('ORG_LOGO_REGION_URL',d.orgLogoRegionUrl);
if(d.orgLogoJsUrl!==undefined) setSetting('ORG_LOGO_JS_URL',d.orgLogoJsUrl);
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='broadcast_banner'){
var msgTxt=(d.msg!==undefined && String(d.msg).trim()) ? d.msg : getSetting('BANNER_MSG','');
if(!msgTxt || !String(msgTxt).trim()){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Message vide'})).setMimeType(ContentService.MimeType.JSON);
}
var bnSan=bannerLooksHtml_(msgTxt)?sanitizeBannerHtml_(msgTxt):null;
if(bnSan && !stripHtml(bnSan.html).trim()) return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Message vide'})).setMimeType(ContentService.MimeType.JSON);
var sheetI=getInscSheet();
var valsI=sheetI.getDataRange().getValues();
var seen={};
var sentCount=0;
for(var bi=1;bi<valsI.length;bi++){
var rowI=valsI[bi];
var emailI=rowI[9];
if(!emailI || seen[emailI]) continue;
seen[emailI]=true;
var htmlB='<p>Bonjour '+esc(rowI[4])+' '+esc(rowI[3])+',</p>'
+'<div style="background:'+(bnSan&&bnSan.bg?bnSan.bg:'#FFF8E1')+';border-left:4px solid #E05A00;padding:14px 16px;margin:14px 0;border-radius:0 6px 6px 0'+(bnSan?'':';white-space:pre-line')+'">'
+(bnSan?bnSan.html:esc(msgTxt))
+'</div>'
+'<p>Cordialement,<br>CRCK Nouvelle-Aquitaine</p>';
var shellB=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Information importante',htmlB);
mailSend_(emailI,'[CRCK NA] Information importante',stripHtml(shellB),{htmlBody:shellB});
sentCount++;
}
return ContentService.createTextOutput(JSON.stringify({ok:true,count:sentCount})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='send_recap'){
var recapHtmlNow=buildRecapHtml();
var shellR=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Recapitulatif des inscriptions',recapHtmlNow);
mailSend_(getRecapRecipients().join(','),'[CRCK NA] Recapitulatif des inscriptions',stripHtml(shellR),{htmlBody:shellR});
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='send_candidate_message'){
if(!d.email){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'E-mail candidat manquant'})).setMimeType(ContentService.MimeType.JSON);
}
var msgInner=(d.html && String(d.html).trim()) ? sanitizeBannerHtml_(d.html).html : '<div style="white-space:pre-line">'+esc(d.message||'')+'</div>';
var msgBody='<p>Bonjour '+esc(d.prenom)+' '+esc(d.nom)+',</p>'
+'<div>'+msgInner+'</div>'
+'<p>Cordialement,<br>CRCK Nouvelle-Aquitaine</p>';
var subjectMsg=d.subject && String(d.subject).trim() ? d.subject : ('[CRCK NA] '+(d.formation||'Votre inscription'));
var shellMsg=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine',d.formation||'Message du secretariat',msgBody);
var ccArr=(d.cc||[]).filter(function(x){return x && String(x).trim();});
var mOpts={htmlBody:shellMsg};
if(ccArr.length) mOpts.cc=ccArr.join(',');
try{ var attM=attBlobs_(d.attachments); if(attM.length) mOpts.attachments=attM; }catch(eM){ return ContentService.createTextOutput(JSON.stringify({ok:false,error:String(eM.message||eM)})).setMimeType(ContentService.MimeType.JSON); }
var toList=[d.email]; if(d.to && d.to.length){ var tl2=msgParseDest_(d.to.join(',')); if(tl2) toList=toList.concat(tl2.filter(function(x){return x!==String(d.email).toLowerCase();})); }
mailSend_(toList.join(','),subjectMsg,stripHtml(shellMsg),mOpts);
sentLog_('Candidat',toList.join(', '),ccArr.join(', '),subjectMsg,msgInner,d.attachments);
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='calendar_add'){
var sh=getCalSheet();
var id=String(new Date().getTime())+Math.floor(Math.random()*1000);
sh.appendRow([id,d.year,d.month,d.day||'',d.title,d.desc||'',d.color||'',d.lieu||'',d.heure||'']);
var lastRow=sh.getLastRow();
if(d.autoOff){
var autoIdx=colIndexByHeader(sh,'AutoOff');
if(autoIdx>-1) sh.getRange(lastRow,autoIdx+1).setValue(true);
}
if(d.statut){
var statutIdxAdd=colIndexByHeader(sh,'Statut');
if(statutIdxAdd>-1) sh.getRange(lastRow,statutIdxAdd+1).setValue(d.statut);
}
return ContentService.createTextOutput(JSON.stringify({ok:true,id:id})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='calendar_toggle_auto'){
var sh=getCalSheet();
var vals=sh.getDataRange().getValues();
var autoIdx=colIndexByHeader(sh,'AutoOff');
var found=false;
for(var i=1;i<vals.length;i++){
if(String(vals[i][0])===String(d.id)){
sh.getRange(i+1,autoIdx+1).setValue(!!d.off);
found=true;
break;
}
}
return ContentService.createTextOutput(JSON.stringify({ok:found})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='calendar_delete'){
var sh=getCalSheet();
var vals=sh.getDataRange().getValues();
for(var i=1;i<vals.length;i++){
if(String(vals[i][0])===String(d.id)){ sh.deleteRow(i+1); break; }
}
return ContentService.createTextOutput(JSON.stringify({ok:true})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='calendar_edit'){
var sh=getCalSheet();
var vals=sh.getDataRange().getValues();
var found=false;
for(var i=1;i<vals.length;i++){
if(String(vals[i][0])===String(d.id)){
sh.getRange(i+1,2).setValue(d.year);
sh.getRange(i+1,3).setValue(d.month);
sh.getRange(i+1,4).setValue(d.day||'');
sh.getRange(i+1,5).setValue(d.title);
if(d.desc!==undefined) sh.getRange(i+1,6).setValue(d.desc);
if(d.color!==undefined) sh.getRange(i+1,7).setValue(d.color);
if(d.lieu!==undefined) sh.getRange(i+1,8).setValue(d.lieu);
if(d.heure!==undefined) sh.getRange(i+1,9).setValue(d.heure);
if(d.statut!==undefined){
var statutIdxEdit=colIndexByHeader(sh,'Statut');
if(statutIdxEdit>-1) sh.getRange(i+1,statutIdxEdit+1).setValue(d.statut);
}
// Si la date change, on remet ConvocAuto a vide pour permettre un
// nouvel envoi automatique a la bonne date.
var caIdx=colIndexByHeader(sh,'ConvocAuto');
if((d.year!==undefined||d.month!==undefined||d.day!==undefined) && caIdx>-1){
sh.getRange(i+1,caIdx+1).setValue('');
}
found=true;
break;
}
}
if(found){
var csh=getConvocSheet();
var cvals=csh.getDataRange().getValues();
for(var j=1;j<cvals.length;j++){
if(String(cvals[j][0])===String(d.id)){
var approveUrl=ScriptApp.getService().getUrl()+'?action=approve_resend&id='+encodeURIComponent(d.id);
mailSend_(CRCK_MAIL,
'[CRCK NA] Date modifiee - validation requise',
'La date de la session "'+d.title+'" a ete modifiee.\n\n'
+'Des convocations avaient deja ete envoyees a : '+cvals[j][3]+'\n\n'
+'Nouvelle date : '+d.dateLabel+'\n\n'
+'Pour renvoyer automatiquement une convocation a jour a ces memes personnes, cliquez sur ce lien :\n'
+approveUrl+'\n\n'
+'(La convocation automatique J-30 reprendra aussi normalement son cours sur cette nouvelle date, sauf si vous l\'avez desactivee pour cet evenement.)');
break;
}
}
}
return ContentService.createTextOutput(JSON.stringify({ok:found})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='send_convocation'){
var recipients=d.recipients||[];
var extraIdsV=[], extraNamesV=[];
if(d.attachments&&d.attachments.length){ try{ var savedV=attSave_(d.attachments); extraIdsV=savedV.map(function(x){return x.id;}); extraNamesV=savedV.map(function(x){return x.name;}); }catch(eAt){ return ContentService.createTextOutput(JSON.stringify({ok:false,error:String(eAt.message||eAt)})).setMimeType(ContentService.MimeType.JSON); } }
var validationOn = getSetting('CONVOC_VALIDATION','1')==='1';
if(validationOn && !d.forceSend){
var batchId=Utilities.getUuid();
var batchData={
kind:'convocation', eventId:d.eventId||'', title:d.title, dateLabel:d.dateLabel,
heure:d.heure||'', lieu:d.lieu||'', desc:d.desc||'', recipients:recipients, extraIds:extraIdsV,
createdAt:new Date().toISOString()
};
PropertiesService.getScriptProperties().setProperty('BATCH_'+batchId, JSON.stringify(batchData));
var namesList = recipients.map(function(r){return '- '+(r.prenom||'')+' '+(r.nom||'')+' ('+(r.email||'sans e-mail')+')';}).join('\n');
var approveUrl = ScriptApp.getService().getUrl()+'?action=approve_send_batch&id='+encodeURIComponent(batchId);
var rejectUrl = ScriptApp.getService().getUrl()+'?action=reject_send_batch&id='+encodeURIComponent(batchId);
var bodyV = 'Une convocation est prete a partir et attend votre validation avant envoi aux candidats.\n\n'
+'Session : '+d.title+'\n'
+'Date : '+d.dateLabel+(d.heure?(' a '+d.heure):'')+(d.lieu?(' - '+d.lieu):'')+'\n\n'
+(extraNamesV.length?('Pieces jointes de cet envoi : '+extraNamesV.join(', ')+'\n'):'')+(convocAttList_().length?('Pieces jointes permanentes : '+convocAttList_().map(function(x){return x.name;}).join(', ')+'\n'):'')+'\n'
+'Destinataires ('+recipients.length+') :\n'+namesList+'\n\n'
+'Verifiez ces informations (date, heure, lieu, liste des personnes), puis cliquez sur ce lien pour envoyer reellement la convocation :\n'
+approveUrl+'\n\n'
+'Si une information est incorrecte : corrigez d\'abord la session dans le calendrier (onglet Calendrier), puis relancez l\'envoi depuis le site.\n'
+'Pour annuler cet envoi sans rien transmettre aux candidats :\n'+rejectUrl+'\n\n'
+'Rappel : si aucun des deux liens n\'est clique dans les '+BATCH_TIMEOUT_HOURS+'h, la convocation partira automatiquement aux candidats (filet de securite, pour ne pas manquer le delai).';
mailSend_(getRecapRecipients().join(','), '[CRCK NA] Validation requise avant envoi - '+d.title, bodyV);
return ContentService.createTextOutput(JSON.stringify({ok:true,pending:true,count:recipients.length})).setMimeType(ContentService.MimeType.JSON);
}
var sentCountV=doSendConvocationEmails(d.title,d.dateLabel,d.heure,d.lieu,d.desc,recipients,d.eventId,extraIdsV);
return ContentService.createTextOutput(JSON.stringify({ok:true,count:sentCountV})).setMimeType(ContentService.MimeType.JSON);
}
if(d.type==='send_status_notice'){
var recipientsS=d.recipients||[];
var STAT_N={
annule:{t:'Session annulée',c:'#C0392B',v:'est <strong style="color:#C0392B">annulée</strong>'},
deplace:{t:'Session déplacée',c:'#E05A00',v:'a été <strong style="color:#E05A00">déplacée</strong>'},
remplace:{t:'Session remplacée',c:'#6A1B9A',v:'a été <strong style="color:#6A1B9A">remplacée</strong>'},
reporte:{t:'Session reportée',c:'#0288D1',v:'a été <strong style="color:#0288D1">reportée</strong>'}
};
var stN=STAT_N[d.statut]||STAT_N.deplace;
var isAnnule = d.statut==='annule';
var subjectS = '[CRCK NA] '+stN.t+' - '+d.title;
var introS = 'La session "'+esc(d.title)+'" prévue le '+esc(d.dateLabel)+' '+stN.v+'.'
+((!isAnnule && d.newDateLabel)?('<br>Nouvelle date : <strong>'+esc(d.newDateLabel)+'</strong>'):'');
var extraS=[];
if(d.lieu) extraS.push('Lieu : '+esc(d.lieu));
if(d.heure) extraS.push('Heure : '+esc(d.heure));
var sentEmailsS=[];
recipientsS.forEach(function(r){
if(!r.email) return;
var bodyS='<p>Bonjour '+esc(r.prenom)+' '+esc(r.nom)+',</p>'
+'<p>'+introS+'</p>'
+(extraS.length?'<p>'+extraS.join('<br>')+'</p>':'')
+(d.message && String(d.message).trim() ? '<div style="white-space:pre-line;margin-top:10px">'+esc(d.message)+'</div>' : '')
+'<p>Cordialement,<br>CRCK Nouvelle-Aquitaine</p>';
var pInfoS=getPoleInfo(r.pole);
var shellS=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine',stN.t,bodyS,
pInfoS ? {nom:pInfoS.nom,email:pInfoS.email,tel:pInfoS.tel} : null);
var optsS={htmlBody:shellS};
if(r.tutemail) optsS.cc=r.tutemail;
mailSend_(r.email,subjectS,stripHtml(shellS),optsS);
sentEmailsS.push(r.email);
});
return ContentService.createTextOutput(JSON.stringify({ok:true,count:sentEmailsS.length})).setMimeType(ContentService.MimeType.JSON);
}
// -- Inscription publique (formulaire du site, non protegee) --
try{
var mapC=closuresGet_(), kC=closureKeyFor_(d.formation,mapC), poleC=String(d.pole||'');
if(/^(MFPC|CQP-EC|CCP|RECYCLAGE-MFPC|H1)$/.test(kC) && poleC!=='Nord' && poleC!=='Sud'){
return ContentService.createTextOutput(JSON.stringify({ok:false,error:'Le pole (Nord ou Sud) est obligatoire pour cette formation.'})).setMimeType(ContentService.MimeType.JSON);
}
var eC=(kC&&mapC[kC])?(mapC[kC][poleC]||mapC[kC]['']):null;
if(closureIsClosed_(eC)){
try{ mailSend_(getRecapRecipients().join(','),'[CRCK NA] Inscription BLOQUEE (clôture dépassée) - '+(d.nom||'')+' '+(d.prenom||''),
'Inscription bloquee car les inscriptions sont closes depuis le '+eC.date+'.\n\nFormation : '+(d.formation||'')+'\nPole : '+poleC+'\nCandidat : '+(d.nom||'')+' '+(d.prenom||'')+'\nE-mail : '+(d.email||'')+'\nTel : '+(d.tel||'')+'\n\nAucune ligne n\'a ete ajoutee au tableur. A vous d\'examiner la demande si besoin.'); }catch(eM){}
return ContentService.createTextOutput(JSON.stringify({ok:false,closed:true,error:'Les inscriptions sont closes pour cette session.'})).setMimeType(ContentService.MimeType.JSON);
}
}catch(eCl){}
var sheet=getInscSheet();
var docsUrl=saveUploadedDocuments(d,(d.nom||'')+' '+(d.prenom||''));
var rowVals_=function(form){ return [
new Date(),form||'',d.pole||'N/A',
d.nom,d.prenom,d.ddn,d.sexe,
d.licence,d.club,d.email,d.tel,
d.tuteur,d.pc_ec,d.pc_ev,d.psc1,d.natation,
d.manquants||'aucun',d.tutemail||'',d.clubmail||'',docsUrl,d.secours||'',
d.departement||'','En attente'
]; };
sheet.appendRow(rowVals_(d.formation));
var mainRowX=sheet.getLastRow();
/* Inscription simultanee : les autres formations cochees (CQP, CCP) recoivent une ligne avec les memes informations et les memes pieces */
var extraOk=[], extraSkipped=[];
try{
(Array.isArray(d.extraFormations)?d.extraFormations:[]).slice(0,3).forEach(function(lab){
lab=String(lab||'').slice(0,160); if(!lab) return;
var kX=closureKeyFor_(lab,mapC), eX=(kX&&mapC[kX])?(mapC[kX][poleC]||mapC[kX]['']):null;
if(closureIsClosed_(eX)){ extraSkipped.push(lab+' (inscriptions closes depuis le '+eX.date+')'); return; }
sheet.appendRow(rowVals_(lab)); extraOk.push(lab);
});
}catch(eX2){}
if(extraOk.length||extraSkipped.length){
d.formationPrincipale=d.formation;
d.formation=[d.formation].concat(extraOk).join(' + ');
d._extraSkipped=extraSkipped;
}
if(d._driveErr){
try{
ensureColumn(sheet,'ErreurDocs');
sheet.getRange(sheet.getLastRow(),colIndexByHeader(sheet,'ErreurDocs')+1).setValue(String(d._driveErr).slice(0,300));
mailSend_(getRecapRecipients().join(','),'[CRCK NA] ATTENTION : pieces jointes non enregistrees - '+d.nom+' '+d.prenom,
'Le candidat '+d.nom+' '+d.prenom+' a joint '+((d.files||[]).length)+' fichier(s) mais ils n\'ont PAS pu etre deposes dans Google Drive.\n\nErreur : '+d._driveErr+'\n\nSolution la plus frequente : dans l\'editeur Apps Script, choisir la fonction autoriserDrive puis cliquer sur Executer et accepter les autorisations ; puis redeployer (Gerer les deploiements > crayon > Nouvelle version). Les pieces sont a demander au candidat.');
}catch(eD){}
}
var detailRows='';
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;width:130px;vertical-align:top">Formation</td><td>'+esc(d.formation)+'</td></tr>';
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Nom</td><td>'+esc(d.nom)+' '+esc(d.prenom)+'</td></tr>';
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Club</td><td>'+esc(d.club)+(d.clubmail?' ('+esc(d.clubmail)+')':'')+'</td></tr>';
if(d.departement) detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Departement</td><td>'+esc(d.departement)+' — '+esc(departementLabel(d.departement))+'</td></tr>';
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">E-mail</td><td>'+esc(d.email)+'</td></tr>';
if(d.secours) detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">E-mail de secours</td><td>'+esc(d.secours)+'</td></tr>';
if(d.regime && String(d.regime).toLowerCase()!=='non') detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Régime alimentaire</td><td style="color:#C0392B;font-weight:bold">'+esc(d.regime)+'</td></tr>';
if(d.allergies && String(d.allergies).toLowerCase()!=='non') detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Allergies</td><td style="color:#C0392B;font-weight:bold">'+esc(d.allergies)+'</td></tr>';
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Pieces jointes</td><td>'+((d.files||[]).length?((d.files||[]).length+' fichier(s) '+(d._driveErr?'<span style="color:#C0392B;font-weight:bold">NON ENREGISTRES : '+esc(d._driveErr)+'</span>':'enregistres dans Drive')):'aucune')+'</td></tr>';
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Telephone</td><td>'+esc(d.tel)+'</td></tr>';
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Tuteur</td><td>'+esc(d.tuteur)+(d.tutemail?' ('+esc(d.tutemail)+')':'')+'</td></tr>';
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">PC EC / PC EV</td><td>'+esc(d.pc_ec)+' / '+esc(d.pc_ev)+'</td></tr>';
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">PSC1 / Natation</td><td>'+esc(d.psc1)+' / '+esc(d.natation)+'</td></tr>';
if(d.manquants && d.manquants.toLowerCase()!=='aucun'){
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top;color:#C0392B">Manquants</td><td style="color:#C0392B;font-weight:700">'+esc(d.manquants)+'</td></tr>';
}
if(docsUrl){
detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top">Documents joints</td><td><a href="'+docsUrl+'">Ouvrir le dossier Drive</a></td></tr>';
}
if(d._extraSkipped&&d._extraSkipped.length) detailRows+='<tr><td style="padding:5px 10px 5px 0;font-weight:bold;vertical-align:top;color:#C0392B">Non enregistre</td><td style="color:#C0392B;font-weight:700">'+esc(d._extraSkipped.join(' ; '))+'</td></tr>';
var detailTable='<table style="width:100%;border-collapse:collapse;margin-bottom:18px">'+detailRows+'</table>';
var recapTable=buildRecapHtml();
var internalBody=detailTable
+'<h3 style="margin:18px 0 8px;font-size:15px;color:#003882">Recapitulatif de tous les inscrits</h3>'
+recapTable;
var internalShell=emailShell('CRCK Nouvelle-Aquitaine','Nouvelle inscription : '+d.formation,internalBody);
// Note : le candidat n'est plus mis en copie de cet e-mail interne, car il
// contient desormais le recapitulatif de TOUS les inscrits (donnees d'autres
// candidats). Le candidat recoit sa propre confirmation separement ci-dessous.
mailSend_(getRecapRecipients().join(','),
'[CRCK NA] Inscription '+d.formation+' - '+d.nom+' '+d.prenom,
stripHtml(internalShell), {htmlBody:internalShell});
// -- Routage vers le Comite Departemental --------------------------------
// Si le candidat a indique son departement (AMFPC, ou toute autre
// formation depuis que le champ est demande a tous), on transmet son
// dossier au(x) destinataire(s) configure(s) pour ce departement
// (reglages CD_<code>, onglet Messages de l'espace gestionnaire).
// Plusieurs adresses possibles par departement, separees par des virgules.
if(d.departement){
var cdEmails=getCdEmails(d.departement);
if(cdEmails.length){
var cdBody=detailTable
+'<p style="margin-top:14px">Ce candidat a choisi le departement <strong>'+esc(d.departement)+' — '+esc(departementLabel(d.departement))+'</strong> lors de son inscription en ligne au CRCK Nouvelle-Aquitaine.</p>';
var cdShell=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Nouvelle candidature '+(d.formation||'')+' - Departement '+d.departement,cdBody);
try{
mailSend_(cdEmails.join(','),
'[CRCK NA] Candidature '+(d.formation||'')+' - '+departementLabel(d.departement)+' ('+d.nom+' '+d.prenom+')',
stripHtml(cdShell), {htmlBody:cdShell, bcc:getRecapRecipients().join(','), keepExcluded:true});
}catch(cdErr){}
}
}
if(d.email && getSetting('CONFIRM_OFF','')!=='1'){
var missArr=(d.manquants||'').split(',').map(function(s){return s.trim();}).filter(function(s){return s && s.toLowerCase()!=='aucun';});
var customMsg=getSetting('CONFIRM_MSG',DEFAULT_CONFIRM_MSG);
try{ var shF=getInscSheet(), rF=(mainRowX>0?mainRowX:shF.getLastRow()); if(String(shF.getRange(rF,4).getValue())===String(d.nom||'')) d.ficheLink=ficheLink_(rF,d.nom,d.prenom); }catch(eF){}
var htmlConf=buildConfirmationHtml(d,missArr,customMsg);
var ccList=[];
if(d.tutemail) ccList.push(d.tutemail);
if(d.clubmail) ccList.push(d.clubmail);
var confOpts={htmlBody:htmlConf};
if(ccList.length) confOpts.cc=ccList.join(',');
// Copie cachee systematique vers le secretariat, sur CHAQUE tentative
// (succes ou echec), pour verifier si Google envoie bien le mail meme
// quand il n'arrive pas chez le candidat (Gmail/Free peuvent le filtrer
// sans que le script recoive d'erreur).
confOpts.bcc=getRecapRecipients().join(',');
try{
mailSend_(d.email,
'Confirmation de reception - Inscription '+d.formation+' - CRCK Nouvelle-Aquitaine',
stripHtml(htmlConf), confOpts);
}catch(mailErr){
// Si l'envoi de la confirmation candidat echoue (quota Gmail depasse,
// adresse invalide, etc.), on prevoit quand meme le secretariat par
// un canal qu'on sait fonctionnel (deja teste plus haut dans ce
// meme appel), avec le detail de l'erreur.
try{
mailSend_(getRecapRecipients().join(','),
'[CRCK NA] ECHEC envoi confirmation candidat',
'Echec de l\'envoi du mail de confirmation a '+d.email+' pour l\'inscription '+d.formation+' ('+d.nom+' '+d.prenom+').\n\n'
+'Erreur : '+mailErr);
}catch(mailErr2){}
}
}
return ContentService.createTextOutput('OK');
}catch(err){
mailSend_(CRCK_MAIL,'[CRCK NA] Erreur',err.toString());
return ContentService.createTextOutput('ERROR');
}
}
// -- Convocations automatiques (J-30) --------------------------------------
// A lancer une fois par jour via un declencheur horaire (voir
// createDailyTrigger, a executer UNE SEULE FOIS depuis l'editeur Apps
// Script). Ignore desormais les evenements marques AutoOff (bouton
// cloche sur le site).
function autoSendConvocations(){
var sh=getCalSheet();
var vals=sh.getDataRange().getValues();
var caIdx=colIndexByHeader(sh,'ConvocAuto');
var autoOffIdx=colIndexByHeader(sh,'AutoOff');
var today=new Date(); today.setHours(0,0,0,0);
for(var i=1;i<vals.length;i++){
var row=vals[i];
var id=row[0];
if(!id) continue;
if(row[caIdx]) continue; // deja convoque
if(autoOffIdx>-1 && row[autoOffIdx]) continue; // convocation automatique desactivee pour cet evenement
var y=parseInt(row[1],10), m=parseInt(row[2],10), day=row[3];
if(isNaN(y)||isNaN(m)||!day){ continue; }
var evtDate=new Date(y,m,parseInt(day,10)); evtDate.setHours(0,0,0,0);
var diffDays=Math.round((evtDate-today)/86400000);
if(diffDays<=30 && diffDays>=0){
var title=row[4], desc=row[5], lieu=row[7], heure=row[8];
var dateLabel=day+'/'+(m+1)+'/'+y;
var recipients=findRegistrations(title);
if(recipients && recipients.length){
var subject=convocSubject_(title,dateLabel);
var sentEmails=[], attsJ=convocAttBlobs_([]);
recipients.forEach(function(r){
if(!r.email) return;
var htmlBody=buildConvocationHtml(title,dateLabel,heure,lieu,desc,r.prenom,r.nom,r.pole);
var opts={htmlBody:htmlBody};
if(r.tutemail) opts.cc=r.tutemail;
if(attsJ.length) opts.attachments=attsJ;
mailSend_(r.email,subject,stripHtml(htmlBody),opts);
sentEmails.push(r.email+(r.tutemail?(' (cc: '+r.tutemail+')'):''));
});
var csh=getConvocSheet();
csh.appendRow([id,title,dateLabel,sentEmails.join('; '),new Date()]);
sh.getRange(i+1,caIdx+1).setValue(new Date());
} else {
sh.getRange(i+1,caIdx+1).setValue(new Date());
mailSend_(CRCK_MAIL,'[CRCK NA] Convocation auto : aucun inscrit trouve',
'La session "'+title+'" du '+dateLabel+' arrive a J-30 (ou moins) mais aucun inscrit '
+'correspondant n\'a ete trouve dans le tableau des inscriptions.\n\n'
+'Verifiez que le titre de l\'evenement dans le calendrier contient bien un mot-cle '
+'present dans le champ "Formation" des inscriptions concernees (ex. "MFPC", "CQP EC", "CCP EV").');
}
}
}
}
function createDailyTrigger(){
ScriptApp.getProjectTriggers().forEach(function(t){
if(t.getHandlerFunction()==='autoSendConvocations') ScriptApp.deleteTrigger(t);
});
ScriptApp.newTrigger('autoSendConvocations').timeBased().everyDays(1).atHour(7).create();
}
// -- Filet de securite : envoi automatique si personne n'a valide ni annule --
// un lot de convocations en attente (voir send_convocation / BATCH_<id>) dans
// un delai de 48h. Verifie toutes les proprietes du script prefixees BATCH_.
var BATCH_TIMEOUT_HOURS = 48;
function checkPendingBatches(){
var props=PropertiesService.getScriptProperties();
var all=props.getProperties();
var now=new Date();
Object.keys(all).forEach(function(key){
if(key.indexOf('BATCH_')!==0) return;
var batch;
try{ batch=JSON.parse(all[key]); }catch(err){ return; }
if(!batch || !batch.createdAt) return;
var created=new Date(batch.createdAt);
var hoursElapsed=(now-created)/3600000;
if(hoursElapsed>=BATCH_TIMEOUT_HOURS){
var cntAuto=0;
if(batch.kind==='convocation'){
cntAuto=doSendConvocationEmails(batch.title,batch.dateLabel,batch.heure,batch.lieu,batch.desc,batch.recipients,batch.eventId,batch.extraIds);
}
props.deleteProperty(key);
mailSend_(getRecapRecipients().join(','),
'[CRCK NA] Convocation envoyee automatiquement (delai de validation ecoule) - '+batch.title,
'Personne n\'a valide ni annule cette convocation dans les '+BATCH_TIMEOUT_HOURS+'h suivant la demande de validation.\n\n'
+'Elle a donc ete envoyee automatiquement a '+cntAuto+' personne(s) pour "'+batch.title+'" ('+batch.dateLabel+'), comme prevu par defaut (filet de securite).\n\n'
+'Si ce n\'etait pas souhaite : desactivez la validation prealable dans l\'onglet Securite, ou validez/annulez plus rapidement la prochaine fois.');
}
});
}
function createBatchCheckTrigger(){
ScriptApp.getProjectTriggers().forEach(function(t){
if(t.getHandlerFunction()==='checkPendingBatches') ScriptApp.deleteTrigger(t);
});
ScriptApp.newTrigger('checkPendingBatches').timeBased().everyHours(6).create();
}
// -- Renvoi de convocation apres changement de date (lien dans l'e-mail d'alerte) --
function resendConvocation(id){
var csh=getConvocSheet();
var cvals=csh.getDataRange().getValues();
for(var i=1;i<cvals.length;i++){
if(String(cvals[i][0])===String(id)){
var emails=String(cvals[i][3]||'').split(';');
var title=cvals[i][1];
var newDateLabel=cvals[i][2];
var newHeure='', newLieu='', newDesc='';
var csh2=getCalSheet();
var vals2=csh2.getDataRange().getValues();
for(var k=1;k<vals2.length;k++){
if(String(vals2[k][0])===String(id)){
newDateLabel=(vals2[k][3]?vals2[k][3]+'/':'')+(parseInt(vals2[k][2],10)+1)+'/'+vals2[k][1];
newLieu=vals2[k][7]||'';
newHeure=vals2[k][8]||'';
newDesc=vals2[k][5]||'';
break;
}
}
emails.forEach(function(em){
var addr=em.split('(')[0].trim();
if(addr){
var htmlBody=buildConvocationHtml(title,newDateLabel,newHeure,newLieu,newDesc||'La date de cette session a change.','','');
mailSend_(addr,'[CRCK NA] Convocation mise a jour - '+title,stripHtml(htmlBody));
}
});
csh.getRange(i+1,3).setValue(newDateLabel);
csh.getRange(i+1,5).setValue(new Date());
return {ok:true};
}
}
return {ok:false,error:'Aucune convocation trouvee pour cet evenement.'};
}
function doGetCore(e){
var action=e && e.parameter && e.parameter.action;
if(action==='contacts'){
return ContentService.createTextOutput(JSON.stringify(readContacts())).setMimeType(ContentService.MimeType.JSON);
}
if(action==='formations_extra'){
return ContentService.createTextOutput(JSON.stringify(readFormationsExtra())).setMimeType(ContentService.MimeType.JSON);
}
if(action==='settings'){
return ContentService.createTextOutput(JSON.stringify({
confirmMsg:getSetting('CONFIRM_MSG',''),
bannerMsg:getSetting('BANNER_MSG',''),
bannerActive:getSetting('BANNER_ACTIVE','')==='1',
convocValidation:getSetting('CONVOC_VALIDATION','1')==='1',
recapEmails:getSetting('RECAP_EMAILS',''),
amfpcS:getSetting('AMFPC_S',''),
amfpcC:getSetting('AMFPC_C',''),
ecn:getSetting('EC_N',''),
ecs:getSetting('EC_S',''),
ecc:getSetting('EC_C',''),
evn:getSetting('EV_N',''),
evs:getSetting('EV_S',''),
evc:getSetting('EV_C',''),
cqpd:getSetting('CQP_D',''),
cqpc:getSetting('CQP_C',''),
ccpevd:getSetting('CCP_EVD',''),
ccpmerd:getSetting('CCP_MERD',''),
ccpc:getSetting('CCP_C',''),
referentNordNom:getSetting('REFERENT_NORD_NOM',''),
referentNordEmail:getSetting('REFERENT_NORD_EMAIL','dlaurent@ffck.org'),
referentNordTel:getSetting('REFERENT_NORD_TEL',''),
referentSudNom:getSetting('REFERENT_SUD_NOM',''),
referentSudEmail:getSetting('REFERENT_SUD_EMAIL','dlaurent@ffck.org'),
referentSudTel:getSetting('REFERENT_SUD_TEL',''),
adresseNord:getSetting('ADRESSE_NORD',''),
adresseSud:getSetting('ADRESSE_SUD',''),
confirmMsgNord:getSetting('CONFIRM_MSG_NORD',''),
confirmMsgSud:getSetting('CONFIRM_MSG_SUD',''),
orgAdresse:getSetting('ORG_ADRESSE','Maison Regionale des Sports - 2 avenue de l\'Universite - 33400 Talence'),
orgSiret:getSetting('ORG_SIRET','413 627 001 00022'),
orgTel:getSetting('ORG_TEL',CRCK_TEL),
orgEmail:getSetting('ORG_EMAIL',CRCK_MAIL),
orgWeb:getSetting('ORG_WEB','www.canoe-nouvelle-aquitaine.fr'),
orgSuiviNom:getSetting('ORG_SUIVI_NOM',''),
orgSuiviEmail:getSetting('ORG_SUIVI_EMAIL',''),
orgSuiviTel:getSetting('ORG_SUIVI_TEL',''),
orgLogoHeaderUrl:getSetting('ORG_LOGO_HEADER_URL',DEFAULT_LOGO_BASE+'logo-header.png'),
orgLogoFooterLeftUrl:getSetting('ORG_LOGO_FOOTER_LEFT_URL',DEFAULT_LOGO_BASE+'logo-footer-left.png'),
orgLogoFooterRightUrl:getSetting('ORG_LOGO_FOOTER_RIGHT_URL',DEFAULT_LOGO_BASE+'logo-footer-right.png'),
orgLogoRegionUrl:getSetting('ORG_LOGO_REGION_URL',DEFAULT_LOGO_BASE+'logo-region-na.png'),
orgLogoJsUrl:getSetting('ORG_LOGO_JS_URL',DEFAULT_LOGO_BASE+'logo-js-cnds.png'),
cdSitesEnabled:getSetting('CD_SITES_ENABLED','0')==='1',
cdEmails:(function(){
var out={};
DEPARTEMENTS.forEach(function(dep){ out[dep.code]=getSetting('CD_'+dep.code,''); });
return out;
})()
})).setMimeType(ContentService.MimeType.JSON);
}
if(action==='banner'){
return ContentService.createTextOutput(JSON.stringify({
msg:getSetting('BANNER_MSG',''),
active:getSetting('BANNER_ACTIVE','')==='1'
})).setMimeType(ContentService.MimeType.JSON);
}
if(action==='calendar'){
var sh=getCalSheet();
var vals=sh.getDataRange().getValues();
var autoOffIdx=colIndexByHeader(sh,'AutoOff');
var statutIdxGet=colIndexByHeader(sh,'Statut');
var rows=[];
for(var i=1;i<vals.length;i++){
if(!vals[i][0])continue;
rows.push({
id:vals[i][0],year:vals[i][1],month:vals[i][2],day:vals[i][3],title:vals[i][4],
desc:vals[i][5],color:vals[i][6],lieu:vals[i][7],heure:vals[i][8],
autoOff: autoOffIdx>-1 ? !!vals[i][autoOffIdx] : false,
statut: statutIdxGet>-1 ? (vals[i][statutIdxGet]||'') : ''
});
}
return ContentService.createTextOutput(JSON.stringify(rows)).setMimeType(ContentService.MimeType.JSON);
}
if(action==='match_registrations'){
var rows2=findRegistrations(e.parameter.formation||'');
return ContentService.createTextOutput(JSON.stringify(rows2)).setMimeType(ContentService.MimeType.JSON);
}
if(action==='list_registrations'){
var callerCodeL=String(e.parameter.code||'');
var callerRoleL='none', callerDeptL='';
if(callerCodeL && callerCodeL===getSetting('ADMIN_CODE','crck2026')){
callerRoleL='crck';
} else {
var cdDeptL=findCdRoleByCode(callerCodeL);
if(cdDeptL){ callerRoleL='cd'; callerDeptL=cdDeptL; }
}
var sheetL=getInscSheet();
var valsL=sheetL.getDataRange().getValues();
var regIdxL=colIndexByHeader(sheetL,'Regime');
var allIdxL=colIndexByHeader(sheetL,'Allergies');
var outL=[];
for(var li=1;li<valsL.length;li++){
var rowL=valsL[li];
if(!rowL[3] && !rowL[4]) continue; // ligne vide
var deptRowL=rowL[21]||'';
if(callerRoleL==='crck'){
outL.push({
rowIndex:li+1,
nom:rowL[3],prenom:rowL[4],formation:rowL[1]||'',pole:rowL[2]||'N/A',club:rowL[8]||'',
email:rowL[9]||'',tutemail:rowL[17]||'',clubmail:rowL[18]||'',
manquants:rowL[16]||'',date:rowL[0],documents:rowL[19]||'',secours:rowL[20]||'',
departement:deptRowL,statut:rowL[22]||'En attente',
sexe:rowL[6]||'',ddn:(rowL[5] instanceof Date?Utilities.formatDate(rowL[5],'Europe/Paris','dd/MM/yyyy'):String(rowL[5]||'')),licence:rowL[7]||'',tel:rowL[10]||'',tuteur:rowL[11]||'',pc_ec:rowL[12]||'',pc_ev:rowL[13]||'',psc1:rowL[14]||'',natation:rowL[15]||'',regime:(regIdxL>-1?String(rowL[regIdxL]||''):''),allergies:(allIdxL>-1?String(rowL[allIdxL]||''):'')
});
} else if(callerRoleL==='cd'){
if(deptRowL===callerDeptL){
outL.push({
rowIndex:li+1,
nom:rowL[3],prenom:rowL[4],formation:rowL[1]||'',pole:rowL[2]||'N/A',club:rowL[8]||'',
email:rowL[9]||'',manquants:rowL[16]||'',date:rowL[0],
departement:deptRowL,statut:rowL[22]||'En attente'
});
} else if((rowL[1]||'').toUpperCase().indexOf('AMFPC')>-1 && deptRowL){
// Comptage seul pour les autres departements, sans donnees nominatives.
outL.push({ departement:deptRowL, formation:rowL[1]||'', statut:rowL[22]||'En attente', anon:true });
}
}
// callerRoleL==='none' : aucune donnee renvoyee.
}
return ContentService.createTextOutput(JSON.stringify(outL)).setMimeType(ContentService.MimeType.JSON);
}
if(action==='approve_resend'){
var result=resendConvocation(e.parameter.id||'');
return HtmlService.createHtmlOutput('<h2>'+(result.ok?'Convocations renvoyees avec succes.':'Erreur : '+result.error)+'</h2>');
}
if(action==='approve_send_batch'){
var idA=e.parameter.id||'';
var rawA=PropertiesService.getScriptProperties().getProperty('BATCH_'+idA);
if(!rawA){
return HtmlService.createHtmlOutput('<h2>Cet envoi est introuvable (deja valide, annule, ou lien perime).</h2>');
}
var batchA=JSON.parse(rawA);
var cntA=0;
if(batchA.kind==='convocation'){
cntA=doSendConvocationEmails(batchA.title,batchA.dateLabel,batchA.heure,batchA.lieu,batchA.desc,batchA.recipients,batchA.eventId,batchA.extraIds);
}
PropertiesService.getScriptProperties().deleteProperty('BATCH_'+idA);
return HtmlService.createHtmlOutput('<h2>Convocation envoyee a '+cntA+' personne(s) pour "'+batchA.title+'".</h2>');
}
if(action==='reject_send_batch'){
var idR=e.parameter.id||'';
var hadR=!!PropertiesService.getScriptProperties().getProperty('BATCH_'+idR);
PropertiesService.getScriptProperties().deleteProperty('BATCH_'+idR);
return HtmlService.createHtmlOutput('<h2>'+(hadR?'Envoi annule. Aucun e-mail n\'a ete envoye aux candidats.':'Cet envoi avait deja ete traite ou n\'existe plus.')+'</h2>');
}
return ContentService.createTextOutput('CRCK NA - Actif');
}
/* ============================================================
CRCK Nouvelle-Aquitaine - EXTENSIONS v26 (a coller dans un NOUVEAU fichier du projet Apps Script)
S'ajoute au script v25 sans le remplacer. Voir le mode d'emploi donne avec ce fichier.
Contenu :
1. Envoi automatique du recapitulatif des inscrits aux personnes referentes (interrupteur).
2. Transmission du dossier de chaque candidat a la FFCK (interrupteur).
3. Poles Nord / Sud du calendrier modifiables (mots-cles).
============================================================ */
var REF_ADMIN_MAIL = 'dlaurent@ffck.org';
function extJson_(o){
return ContentService.createTextOutput(JSON.stringify(o)).setMimeType(ContentService.MimeType.JSON);
}
function extCodeOk_(d){
return String(d.code||'') === getSetting('ADMIN_CODE','crck2026');
}
/* ---------- 0. Page Demarches modifiable (stockee dans l'onglet PageDemarches) ---------- */
function getDemarchesHtml_(){
try{
var sh=SpreadsheetApp.getActiveSpreadsheet().getSheetByName('PageDemarches');
if(!sh) return '';
return String(sh.getRange(1,1).getValue()||'');
}catch(e){ return ''; }
}
function setDemarchesHtml_(html){
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sh=ss.getSheetByName('PageDemarches');
if(!sh) sh=ss.insertSheet('PageDemarches');
sh.getRange(1,1).setValue(html.length>45000 ? html.substring(0,45000) : html);
}
/* ---------- 0bis. Candidats voisins (compteurs anonymes) + carte CD ---------- */
function formKey_(s){
var u=String(s||'').toUpperCase();
if(u.indexOf('RECYCLAGE')>-1) return 'RECYCLAGE';
if(u.indexOf('AMFPC')>-1) return 'AMFPC';
if(u.indexOf('CQP')>-1) return 'CQP';
if(u.indexOf('CCP')>-1) return 'CCP';
if(/\bH1\b/.test(u)) return 'H1';
if(u.indexOf('MFPC')>-1) return 'MFPC';
return u.trim();
}
/* Compte les autres candidats deja inscrits a la meme formation : aucune donnee nominative. */
function neighborCounts_(formation,pole,dept){
var out={ok:true,region:0,pole:0,dept:0};
try{
var key=formKey_(formation); if(!key) return out;
var vals=getInscSheet().getDataRange().getValues();
for(var i=1;i<vals.length;i++){
var r=vals[i];
if(!r[3] && !r[4]) continue;
if(formKey_(r[1])!==key) continue;
if(String(r[22]||'').toLowerCase().indexOf('annul')>-1) continue;
out.region++;
if(pole && String(r[2])===String(pole)) out.pole++;
if(dept && String(r[21])===String(dept)) out.dept++;
}
}catch(err){ out.ok=false; }
return out;
}
/* Un cadre de CD ne peut modifier QUE sa propre carte de contact (type CD + son departement). */
function saveCdContact_(dept,c){
var sh=getContactsSheet();
var vals=sh.getDataRange().getValues();
var row=-1;
for(var i=1;i<vals.length;i++){
if(String(vals[i][8]||'').toUpperCase()==='CD' && String(vals[i][9]||'')===String(dept)){ row=i+1; break; }
}
var nom=String(c.nom||'').trim();
if(!nom) return {ok:false,error:'Nom obligatoire'};
if(row<0){
row=sh.getLastRow()+1;
sh.getRange(row,1,1,11).setValues([['cd'+dept+'_'+new Date().getTime(),nom,String(c.role||''),String(c.email||''),String(c.tel||''),String(c.lien||''),String(c.lienLabel||''),row,'CD',String(dept),'0']]);
} else {
sh.getRange(row,2,1,6).setValues([[nom,String(c.role||''),String(c.email||''),String(c.tel||''),String(c.lien||''),String(c.lienLabel||'')]]);
}
return {ok:true};
}
/* ---------- 1. Referents ---------- */
/* Adresses jamais destinataires des courriers generaux (recapitulatifs, copies...) : reglage MAIL_EXCLUDE (liste separee par des virgules).
Par defaut : un cadre departemental, qui ne recoit que ce qui concerne son comite departemental. */
function mailExcluded_(){
var raw=getSetting('MAIL_EXCLUDE','krzyczkowski.franck@gmail.com');
return String(raw).toLowerCase().split(/[,;\s]+/).filter(function(x){return x.indexOf('@')>0;});
}
function getReferentEmails(){
/* Seuls les contacts du comite regional (type CR) recoivent le recapitulatif : jamais les cartes des comites departementaux */
var seen={}, out=[], ex=mailExcluded_(), cdMails={};
var all=readContacts();
all.forEach(function(c){ if(c.type==='CD'){ var m=String(c.email||'').trim().toLowerCase(); if(m) cdMails[m]=1; } });
all.forEach(function(c){
if(c.type==='CD') return;
var e=String(c.email||'').trim().toLowerCase();
if(e && e.indexOf('@')>0 && !seen[e] && e!==REF_ADMIN_MAIL && !cdMails[e] && ex.indexOf(e)<0){ seen[e]=1; out.push(e); }
});
return out;
}
function autoSendReferentRecap(force){
var on=getSetting('REF_AUTO','')==='1';
var html=buildRecapHtml();
var hash=Utilities.base64Encode(Utilities.computeDigest(Utilities.DigestAlgorithm.MD5,html));
var shell=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Recapitulatif des inscriptions',html);
var sent=0;
if(force===true || hash!==getSetting('REF_HASH_ADM','')){
var subjA='[CRCK NA] Recapitulatif des inscrits'+(on?'':' (envoi aux referents BLOQUE : copie administrateur)');
mailSend_(REF_ADMIN_MAIL,subjA,stripHtml(shell),{htmlBody:shell});
setSetting('REF_HASH_ADM',hash); sent++;
}
if(on && (force===true || hash!==getSetting('REF_HASH_REF',''))){
var refs=getReferentEmails();
if(refs.length){
mailSend_(refs.join(','),'[CRCK NA] Recapitulatif des inscrits',stripHtml(shell),{htmlBody:shell});
sent+=refs.length;
}
setSetting('REF_HASH_REF',hash);
}
return sent;
}
/* A executer UNE fois dans l'editeur pour lancer l'envoi quotidien (8 h). */
function createReferentTrigger(){
ScriptApp.getProjectTriggers().forEach(function(t){ if(t.getHandlerFunction()==='autoSendReferentRecap') ScriptApp.deleteTrigger(t); });
ScriptApp.newTrigger('autoSendReferentRecap').timeBased().everyDays(1).atHour(8).create();
}
/* ---------- 2. Dossier candidat -> FFCK ---------- */
function getFfckMail(){ return getSetting('FFCK_MAIL','serviceformation@ffck.org'); }
function sendDossierFfckRow(sh,r){
ensureColumn(sh,'DossierFFCK');
var mk=colIndexByHeader(sh,'DossierFFCK');
var v=sh.getRange(r,1,1,sh.getLastColumn()).getValues()[0];
if(!v[3] && !v[4]) return false;
if(!ffckWants_(v[1])) return false; /* seuls les types de formation coches sont transmis */
if(mk>-1 && v[mk]) return false;
var docs=String(v[19]||'');
var dest=getFfckMail();
var attachments=[], note='';
if(docs){
try{
var m=docs.match(/[-\w]{20,}/);
if(m){
var folder=DriveApp.getFolderById(m[0]);
try{ folder.addViewer(dest); }catch(e1){ note='(acces Drive non partage : adresse sans compte Google ; pieces en attache)'; }
var it=folder.getFiles(), total=0, blobs=[];
while(it.hasNext()){ var f=it.next(); total+=f.getSize(); blobs.push(f.getBlob()); }
if(total<20*1024*1024){ attachments=blobs; } else { note+=' (pieces trop volumineuses pour l\'e-mail : voir le lien)'; }
}
}catch(e2){ note='(dossier Drive inaccessible : '+e2+')'; }
}
function row(l,x){ return '<tr><td style="padding:4px 12px 4px 0;font-weight:bold;vertical-align:top">'+l+'</td><td>'+esc(x)+'</td></tr>'; }
var body='<table style="font-size:14px">'+row('Formation',v[1])+row('Pole',v[2])+row('Candidat',v[3]+' '+v[4])+row('Date de naissance',v[5])+row('Licence',v[7])+row('Club',v[8])+row('E-mail',v[9])+row('Telephone',v[10])+row('Pieces manquantes',v[16]||'aucune')+'</table>'
+(docs?'<p>Dossier du candidat (pieces jointes) : <a href="'+esc(docs)+'">'+esc(docs)+'</a> '+esc(note)+'</p>':'<p>Aucune piece jointe deposee par le candidat.</p>')
+'<p>Message envoye automatiquement par le site de formation du CRCK Nouvelle-Aquitaine.</p>';
var shell=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Dossier de candidature a transmettre a la FFCK',body);
var opts={htmlBody:shell,cc:REF_ADMIN_MAIL};
if(attachments.length) opts.attachments=attachments;
mailSend_(dest,'[CRCK NA] Dossier candidat - '+v[1]+' - '+v[3]+' '+v[4],stripHtml(shell),opts);
if(mk>-1) sh.getRange(r,mk+1).setValue(new Date());
return true;
}
function sendPendingDossiersFfck(){
var sh=getInscSheet(); ensureColumn(sh,'DossierFFCK');
var n=0;
for(var r=2;r<=sh.getLastRow();r++){ try{ if(sendDossierFfckRow(sh,r)) n++; }catch(e){} }
return n;
}
/* ---------- Boite a lettres : questions des visiteurs (feuille "Messages") ---------- */
function getMsgSheet_(){
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sh=ss.getSheetByName('Messages');
if(!sh){
sh=ss.insertSheet('Messages');
sh.appendRow(['ID','Date','Nom','Email','Telephone','Formation','Message','Statut','Note','DateTraitement']);
}
return sh;
}
/* Inscription d'un candidat par le gestionnaire (sans passer par le formulaire public ni par les clotures) */
function adminAddCandidate_(d){
var nom=String(d.nom||'').trim().slice(0,80), prenom=String(d.prenom||'').trim().slice(0,80), form=String(d.formation||'').trim().slice(0,160), email=String(d.email||'').trim().slice(0,150);
if(!nom||!prenom) return {ok:false,error:'nom et prenom obligatoires'};
if(!form) return {ok:false,error:'formation obligatoire'};
if(!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return {ok:false,error:'adresse e-mail invalide'};
function s(x,n){ return String(x==null?'':x).trim().slice(0,n||150); }
var sh=getInscSheet();
sh.appendRow([new Date(),form,s(d.pole,20)||'N/A',nom,prenom,s(d.ddn,20),s(d.sexe,10),s(d.licence,30),s(d.club,120),email,s(d.tel,30),s(d.tuteur,120),'','','','',s(d.manquants,300)||'aucun',s(d.tutemail,150),s(d.clubmail,150),'',s(d.secours,150),s(d.departement,10),'En attente']);
var row=sh.getLastRow(), mailed=false;
if(d.notify){
try{
var body='Bonjour '+prenom+',\n\nVotre inscription a la formation « '+form+' » a bien ete enregistree par le Comite Regional de Canoe-Kayak Nouvelle-Aquitaine. Nous reviendrons vers vous avec les informations pratiques.\n\nPour toute question, repondez simplement a ce message.\n\nCordialement,\nCRCK Nouvelle-Aquitaine';
mailSend_(email,'[CRCK NA] Votre inscription - '+form,body,{name:'CRCK Nouvelle-Aquitaine'});
mailed=true;
try{ sentLog_('Inscription manuelle',email,'','[CRCK NA] Votre inscription - '+form,body.replace(/\n/g,'<br>'),[]); }catch(eL){}
}catch(eM){}
}
return {ok:true,rowIndex:row,mailed:mailed};
}
/* ---------- Historique des messages envoyes + corbeille (feuille "Envois") ---------- */
function getSentSheet_(){
var ss=SpreadsheetApp.getActiveSpreadsheet(), sh=ss.getSheetByName('Envois');
if(!sh){ sh=ss.insertSheet('Envois'); sh.appendRow(['ID','Date','Type','A','Cc','Objet','Message','Pieces','Corbeille','DateCorbeille']); }
return sh;
}
function sentLog_(type,to,cc,subject,html,atts){
try{
var names=(atts||[]).map(function(f){ return String((f&&f.name)||''); }).filter(Boolean).join(' | ');
getSentSheet_().appendRow([String(new Date().getTime())+Math.floor(Math.random()*1000),new Date(),type,String(to||''),String(cc||''),String(subject||'').slice(0,250),String(html||'').slice(0,45000),names,'','']);
}catch(e){}
}
function sentList_(){
var sh=getSentSheet_(), v=sh.getDataRange().getValues(), out=[];
for(var i=v.length-1;i>=1 && out.length<600;i--){
var r=v[i]; if(!r[0]) continue;
out.push({id:String(r[0]),date:(r[1] instanceof Date)?r[1].toISOString():String(r[1]||''),type:String(r[2]||''),to:String(r[3]||''),cc:String(r[4]||''),subject:String(r[5]||''),html:String(r[6]||''),att:String(r[7]||''),trash:String(r[8]||'')==='1',trashDate:(r[9] instanceof Date)?r[9].toISOString():''});
}
return {ok:true,items:out};
}
function sentAction_(d){
var sh=getSentSheet_(), v=sh.getDataRange().getValues(), act=String(d.action||'');
if(act==='empty'){
var n=0; for(var i=v.length-1;i>=1;i--){ if(String(v[i][8]||'')==='1'){ sh.deleteRow(i+1); n++; } }
return {ok:true,deleted:n};
}
var idx=-1; for(var j=1;j<v.length;j++){ if(String(v[j][0])===String(d.id)){ idx=j; break; } }
if(idx<0) return {ok:false,error:'message introuvable (actualisez la liste)'};
if(act==='trash'){ sh.getRange(idx+1,9).setValue('1'); sh.getRange(idx+1,10).setValue(new Date()); return {ok:true}; }
if(act==='restore'){ sh.getRange(idx+1,9).setValue(''); sh.getRange(idx+1,10).setValue(''); return {ok:true}; }
if(act==='delete'){ if(String(v[idx][8]||'')!=='1') return {ok:false,error:"mettez d'abord le message a la corbeille"}; sh.deleteRow(idx+1); return {ok:true}; }
return {ok:false,error:'action inconnue'};
}
function msgDest_(){ return getSetting('MSG_DEST','nouvelleaquitaine@ffck.org'); }
function msgDestFirst_(){ return String(msgDest_()).split(',')[0].trim(); }
function msgParseDest_(txt){
var parts=String(txt||'').split(/[,;\s]+/), out=[];
for(var i=0;i<parts.length;i++){
var a=parts[i].trim(); if(!a) continue;
if(!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(a)) return null;
if(out.indexOf(a.toLowerCase())<0) out.push(a.toLowerCase());
}
return out.length&&out.length<=10?out:null;
}
function msgSend_(d){
if(String(d.website||'')!=='') return {ok:true}; /* piege anti-robot : champ cache rempli = robot */
var nom=String(d.nom||'').trim().slice(0,100);
var email=String(d.email||'').trim().slice(0,150);
var tel=String(d.tel||'').trim().slice(0,40);
var form=String(d.formation||'').trim().slice(0,200);
var msg=String(d.message||'').trim().slice(0,3000);
if(!nom || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email) || msg.length<5) return {ok:false,error:'champs obligatoires manquants'};
/* limite : 5 messages par heure et par adresse e-mail */
try{
var cache=CacheService.getScriptCache(), key='msg_'+email.toLowerCase();
var n=Number(cache.get(key)||0);
if(n>=5) return {ok:false,error:'trop de messages, reessayez plus tard'};
cache.put(key,String(n+1),3600);
}catch(e){}
var sh=getMsgSheet_();
var id='m'+new Date().getTime();
sh.appendRow([id,new Date(),nom,email,tel,form,msg,'A traiter','','']);
var dest=msgDest_();
var rows='<table style="font-size:14px">'
+'<tr><td style="padding:4px 12px 4px 0;font-weight:bold">Nom</td><td>'+esc(nom)+'</td></tr>'
+'<tr><td style="padding:4px 12px 4px 0;font-weight:bold">E-mail</td><td>'+esc(email)+'</td></tr>'
+(tel?'<tr><td style="padding:4px 12px 4px 0;font-weight:bold">Telephone</td><td>'+esc(tel)+'</td></tr>':'')
+(form?'<tr><td style="padding:4px 12px 4px 0;font-weight:bold">Formation</td><td>'+esc(form)+'</td></tr>':'')
+'</table><p style="white-space:pre-wrap;margin-top:12px">'+esc(msg)+'</p>'
+'<p style="font-size:12px;color:#666">Vous pouvez repondre directement a ce mail (la reponse ira au visiteur) ou depuis l\'espace gestionnaire, onglet Boite a lettres.</p>';
try{
var shell=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Nouvelle question recue par le site',rows);
mailSend_(dest,'[CRCK NA] Question de '+nom+(form?' - '+form:''),stripHtml(shell),{htmlBody:shell,replyTo:email,name:'Site formations CRCK NA'});
}catch(e1){}
try{
var ack='<p>Bonjour '+esc(nom)+',</p><p>Nous avons bien recu votre message et nous vous repondrons dans les meilleurs delais.</p>'
+'<p style="white-space:pre-wrap;color:#555;border-left:3px solid #ccc;padding-left:10px">'+esc(msg)+'</p>';
var shell2=emailShell('Comite Regional de Canoe-Kayak Nouvelle-Aquitaine','Accuse de reception de votre question',ack);
mailSend_(email,'Votre question - CRCK Nouvelle-Aquitaine',stripHtml(shell2),{htmlBody:shell2,replyTo:msgDestFirst_(),name:'CRCK Nouvelle-Aquitaine'});
}catch(e2){}
return {ok:true};
}
function msgList_(){
var sh=getMsgSheet_(), v=sh.getDataRange().getValues(), out=[];
for(var i=v.length-1;i>=1;i--){
var r=v[i]; if(!r[0]) continue;
out.push({row:i+1,id:String(r[0]),date:(r[1] instanceof Date)?r[1].toISOString():String(r[1]||''),nom:r[2],email:r[3],tel:String(r[4]||''),formation:r[5],message:r[6],statut:r[7]||'A traiter',note:r[8]||'',traite:(r[9] instanceof Date)?r[9].toISOString():''});
}
return {ok:true,messages:out,dest:msgDest_()};
}
function msgReply_(d){
try{
var to=msgParseDest_(d.to);
if(!to || !to.length) return {ok:false,error:'adresse du destinataire invalide'};
var cc=[]; if(String(d.cc||'').trim()){ cc=msgParseDest_(d.cc); if(!cc) return {ok:false,error:'adresse(s) en copie invalide(s)'}; }
var rt=''; if(String(d.replyTo||'').trim()){ var r=msgParseDest_(d.replyTo); if(!r||r.length<1||r.length>6) return {ok:false,error:'adresse(s) de reponse invalide(s) (6 adresses maximum, separees par des virgules)'}; rt=r.join(','); }
var subj=String(d.subject||'').trim().slice(0,200) || 'Re: Votre question - CRCK Nouvelle-Aquitaine';
var body=String(d.body||'').trim();
if(!body) return {ok:false,error:'message vide'};
var html='<div style="font-size:14px;line-height:1.55;color:#222">'+((d.html && String(d.html).trim())?sanitizeBannerHtml_(d.html).html:esc(body).replace(/\n/g,'<br>'))+'</div>';
if(!body && d.html) body=stripHtml(html);
var opts={htmlBody:html,name:'CRCK Nouvelle-Aquitaine'};
if(cc.length) opts.cc=cc.join(',');
if(rt) opts.replyTo=rt;
try{ var attR=attBlobs_(d.attachments); if(attR.length) opts.attachments=attR; }catch(eA){ return {ok:false,error:String(eA)}; }
mailSend_(to.join(','),subj,body,opts);
sentLog_('Reponse visiteur',to.join(', '),cc.join(', '),subj,html,d.attachments);
try{ setSetting('REPLY_FROM',String(d.replyFromList||getSetting('REPLY_FROM','nouvelleaquitaine@ffck.org, dlaurent@ffck.org, laurent.domi@free.fr'))); }catch(e0){}
if(d.id){ try{ msgUpdate_({id:d.id,row:d.row,email:to[0],statut:'Traite'}); }catch(e1){} }
return {ok:true};
}catch(err){ return {ok:false,error:'erreur script : '+err}; }
}
function msgUpdate_(d){
try{
if(d.dest!==undefined){
var list=msgParseDest_(d.dest);
if(!list) return {ok:false,error:'adresse(s) invalide(s) - separez-les par une virgule (10 maximum)'};
setSetting('MSG_DEST',list.join(','));
}
if(d.id){
var sh=getMsgSheet_(), v=sh.getDataRange().getValues(), idx=-1;
for(var i=1;i<v.length;i++){ if(String(v[i][0]).trim()===String(d.id).trim()){ idx=i; break; } }
if(idx<0 && d.row && v[d.row-1] && String(v[d.row-1][3]).toLowerCase()===String(d.email||'').toLowerCase()) idx=d.row-1;
if(idx<0) return {ok:false,error:'message introuvable (actualisez la liste)'};
if(d.delete){ sh.deleteRow(idx+1); return {ok:true}; }
if(d.statut!==undefined){
var done=(String(d.statut)==='Traite');
sh.getRange(idx+1,8).setValue(done?'Traite':'A traiter');
sh.getRange(idx+1,10).setValue(done?new Date():'');
}
if(d.note!==undefined) sh.getRange(idx+1,9).setValue(String(d.note).slice(0,1000));
}
return {ok:true,dest:msgDest_()};
}catch(err){ return {ok:false,error:'erreur script : '+err}; }
}
/* ---------- Bandeau d'info mis en forme (HTML filtre) ---------- */
function bannerSafeColor_(c){ c=String(c||'').trim(); return /^#[0-9a-fA-F]{3,8}$/.test(c)||/^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/.test(c)||/^[a-zA-Z]{3,20}$/.test(c)?c:''; }
function bannerSafeStyle_(st){
var out=[], parts=String(st||'').split(';');
for(var i=0;i<parts.length;i++){
var kv=parts[i].split(':'); if(kv.length<2) continue;
var k=kv.shift().trim().toLowerCase(), v=kv.join(':').trim();
if(k==='color'||k==='background-color'){ var c=bannerSafeColor_(v); if(c) out.push(k+':'+c); }
else if(k==='font-size'){ if(/^(x-small|small|medium|large|x-large|xx-large|xxx-large|\d{1,2}(\.\d)?(px|pt|em|rem|%))$/i.test(v)) out.push(k+':'+v); }
else if(k==='font-family'){ if(/^[a-zA-Z0-9 ,'"\-]{1,80}$/.test(v)) out.push(k+':'+v.replace(/"/g,"'")); }
else if(k==='font-weight'){ if(/^(bold|normal|[1-9]00)$/i.test(v)) out.push(k+':'+v); }
else if(k==='font-style'){ if(/^(italic|normal)$/i.test(v)) out.push(k+':'+v); }
else if(k==='text-decoration'||k==='text-decoration-line'){ if(/^(underline|none|line-through)$/i.test(v)) out.push(k+':'+v); }
else if(k==='text-align'){ if(/^(left|center|right)$/i.test(v)) out.push(k+':'+v); }
}
return out.join(';');
}
function sanitizeBannerHtml_(raw){
var src=String(raw||''), bg='';
var m=src.match(/^\s*<!--bg:([^>]*?)-->/);
if(m){ bg=bannerSafeColor_(m[1]); src=src.replace(m[0],''); }
var ALLOWED={b:1,strong:1,i:1,em:1,u:1,span:1,br:1,div:1,p:1,font:1};
var parts=src.split(/(<\/?[a-zA-Z][^>]*>|<!--[\s\S]*?-->)/), out='';
for(var i=0;i<parts.length;i++){
var p=parts[i];
if(!p) continue;
var t=p.match(/^<(\/?)([a-zA-Z][a-zA-Z0-9]*)([^>]*)>$/);
if(t){
var name=t[2].toLowerCase();
if(!ALLOWED[name]) continue;
if(t[1]){ if(name!=='br') out+='</'+(name==='font'?'span':name)+'>'; continue; }
if(name==='br'){ out+='<br>'; continue; }
var style='', sm=t[3].match(/style\s*=\s*"([^"]*)"/i)||t[3].match(/style\s*=\s*'([^']*)'/i);
if(sm) style=bannerSafeStyle_(sm[1]);
if(name==='font'){
var cm=t[3].match(/color\s*=\s*["']([^"']*)["']/i); if(cm){ var cc=bannerSafeColor_(cm[1]); if(cc) style='color:'+cc+(style?';'+style:''); }
var fm=t[3].match(/face\s*=\s*["']([^"']*)["']/i); if(fm&&/^[a-zA-Z0-9 ,\-]{1,60}$/.test(fm[1])) style='font-family:'+fm[1]+(style?';'+style:'');
out+='<span'+(style?' style="'+style+'"':'')+'>'; continue;
}
out+='<'+name+(style?' style="'+style+'"':'')+'>';
} else if(/^<!--/.test(p)){ continue; }
else { out+=p.replace(/</g,'<'); }
}
return {html:out,bg:bg};
}
function bannerLooksHtml_(s){ return /<[a-zA-Z!\/]/.test(String(s||'')); }
function bannerStore_(raw){
if(!bannerLooksHtml_(raw)) return String(raw||'');
var r=sanitizeBannerHtml_(raw);
return (r.bg?'<!--bg:'+r.bg+'-->':'')+r.html;
}
/* ---------- Points d'entree (enveloppent doPost / doGet du script v25) ---------- */
/* ---------- Enquetes de satisfaction (v55) : reponses dans l'onglet "Enquete M1", "Enquete M2"... ---------- */
function surveyMails_(){
return String(getSetting('SURVEY_MAIL','dlaurent@ffck.org, laurent.domi@free.fr')).split(/[,;\s]+/).filter(function(a){ return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(a); });
}
function surveyKey_(s){ s=String(s||'M1').replace(/[^A-Za-z0-9]/g,'').slice(0,6); return s||'M1'; }
function surveySheet_(key){
var ss=SpreadsheetApp.getActiveSpreadsheet(), nm='Enquete '+key, sh=ss.getSheetByName(nm);
if(!sh){ sh=ss.insertSheet(nm); sh.getRange(1,1).setValue('Date'); sh.setFrozenRows(1); }
return sh;
}
function surveyClean_(v,n){
v=String(v==null?'':v).replace(/\r/g,'').slice(0,n||2000);
if(/^[=+\-@]/.test(v)) v="'"+v;
return v;
}
function surveySubmit_(d){
if(d.website) return {ok:true};
if(Number(d.elapsed)>=0 && Number(d.elapsed)<8) return {ok:false,error:'envoi trop rapide, reessayez'};
var key=surveyKey_(d.survey), ans=d.answers;
if(!Array.isArray(ans)||!ans.length) return {ok:false,error:'aucune reponse'};
if(ans.length>90) ans=ans.slice(0,90);
var lock=null; try{ lock=LockService.getScriptLock(); lock.waitLock(20000); }catch(eL){ lock=null; }
var contact='', rows=[];
try{
var sh=surveySheet_(key);
var header=sh.getRange(1,1,1,Math.max(sh.getLastColumn(),1)).getValues()[0].map(String);
var vals={};
ans.forEach(function(a){
var label=surveyClean_(a&&a.k,100).replace(/^'/,''); if(!label) return;
var idx=header.indexOf(label);
if(idx<0){ header.push(label); sh.getRange(1,header.length).setValue(label); idx=header.length-1; }
var v=a.v, t=String(a.t||'');
if(t==='r'){ var n=Number(v); v=(n>=1&&n<=5)?Math.round(n):''; }
else v=surveyClean_(v, t==='t'?2000:150);
vals[idx]=v;
rows.push({k:label,v:v,t:t});
if(label.indexOf('E-mail')===0 && v) contact=String(v);
});
var row=new Array(header.length); for(var i=0;i<row.length;i++) row[i]='';
row[0]=new Date(); Object.keys(vals).forEach(function(i){ row[+i]=vals[i]; });
sh.appendRow(row);
} finally { if(lock){ try{ lock.releaseLock(); }catch(eR){} } }
try{
if(getSetting('SURVEY_NOTIFY','1')!=='0'){
var to=surveyMails_();
if(to.length){
var body='Nouvelle reponse a l\'enquete de satisfaction ('+String(d.title||key).slice(0,80)+')\n\n';
rows.forEach(function(r){ if(r.v==='') return; body+=r.k+' : '+(r.t==='r'?r.v+'/5':r.v)+'\n'; });
body+='\n(Toutes les reponses sont dans l\'onglet "Enquete '+key+'" du tableur ; moyennes et commentaires dans l\'Espace Gestionnaire > Enquetes.)';
var o={name:'Enquetes CRCK NA'}; if(/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(contact)) o.replyTo=contact;
MailApp.sendEmail(to.join(','),'Enquete '+key+' : nouvelle reponse',body,o);
}
}
}catch(eM){}
return {ok:true};
}
function surveyResults_(key){
key=surveyKey_(key);
var ss=SpreadsheetApp.getActiveSpreadsheet(), list=[];
ss.getSheets().forEach(function(s){ var m=s.getName().match(/^Enquete (M\w+)$/); if(m) list.push(m[1]); });
var sh=ss.getSheetByName('Enquete '+key);
if(!sh||sh.getLastRow()<2) return {ok:true,survey:key,surveys:list,n:0,items:[]};
var data=sh.getDataRange().getValues(), head=data[0], n=data.length-1, items=[];
var dmin=null,dmax=null; for(var r=1;r<data.length;r++){ var dt=data[r][0]; if(dt instanceof Date){ if(!dmin||dt<dmin) dmin=dt; if(!dmax||dt>dmax) dmax=dt; } }
for(var c=1;c<head.length;c++){
var col=[]; for(var r2=1;r2<data.length;r2++){ var x=data[r2][c]; if(x!==''&&x!=null) col.push(x); }
if(!col.length){ items.push({k:String(head[c]),type:'t',texts:[]}); continue; }
var allNum=col.every(function(x){ return typeof x==='number'&&x>=1&&x<=5; });
if(allNum){
var dist=[0,0,0,0,0], sum=0; col.forEach(function(x){ dist[x-1]++; sum+=x; });
items.push({k:String(head[c]),type:'r',n:col.length,avg:Math.round(100*sum/col.length)/100,dist:dist});
continue;
}
var cnt={}, maxLen=0; col.forEach(function(x){ x=String(x); cnt[x]=(cnt[x]||0)+1; if(x.length>maxLen) maxLen=x.length; });
if(maxLen<=40 && Object.keys(cnt).length<=6 && !/Commentaire|Les plus|Les moins|Ce qui manque|revoir dans|remarques|Attentes : commentaire|Nom|E-mail/.test(String(head[c]))){
items.push({k:String(head[c]),type:'c',n:col.length,counts:cnt});
} else {
items.push({k:String(head[c]),type:'t',texts:col.map(function(x){ return String(x).replace(/^'/,'').slice(0,2000); }).slice(-200)});
}
}
return {ok:true,survey:key,surveys:list,n:n,from:dmin?dmin.toISOString():'',to:dmax?dmax.toISOString():'',items:items};
}
function surveyAdmin_(d){
var op=String(d.op||'');
if(op==='results') return surveyResults_(d.survey);
if(op==='get') return {ok:true,emails:surveyMails_(),notify:getSetting('SURVEY_NOTIFY','1')!=='0'};
if(op==='set'){
var em=(Array.isArray(d.emails)?d.emails:String(d.emails||'').split(/[,;\s]+/)).map(function(a){ return String(a||'').trim().toLowerCase(); }).filter(function(a){ return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(a); });
var seen={}; em=em.filter(function(a){ if(seen[a]) return false; seen[a]=1; return true; }).slice(0,10);
if(!em.length) return {ok:false,error:'au moins une adresse valide est necessaire'};
setSetting('SURVEY_MAIL',em.join(', ')); setSetting('SURVEY_NOTIFY',d.notify===false?'0':'1');
return {ok:true,emails:em,notify:d.notify!==false};
}
return {ok:false,error:'operation inconnue'};
}
function doPost(e){
var d=null;
try{ d=JSON.parse(e.postData.contents); }catch(err){ return doPostCore(e); }
var t=d.type;
if(t==='contacts_save'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
}
if(t==='message_send'){
return extJson_(msgSend_(d));
}
if(t==='fiche_set'){
return extJson_(ficheSet_(d));
}
if(t==='send_fiche_links'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
return extJson_({ok:true,count:sendFicheLinks_(!!d.force)});
}
if(t==='convoc_preview'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
var tplP={subject:d.subject,intro:d.intro,outro:d.outro};
var dlP=d.dateLabel||'';
return extJson_({ok:true,subject:convocSubject_(d.title||'Formation',dlP,tplP),
html:buildConvocationHtml(d.title||'Formation',dlP,d.heure||'',d.lieu||'',d.desc||'',d.prenom||'Prenom',d.nom||'NOM',d.pole||'',tplP)});
}
if(t==='convoc_att_add'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{
var cur=convocAttList_(); var add=attSave_(d.files||[]);
if(cur.length+add.length>6) return extJson_({ok:false,error:'6 pieces permanentes maximum : retirez-en une avant d\'en ajouter'});
cur=cur.concat(add); setSetting('CONVOC_ATT',JSON.stringify(cur));
return extJson_({ok:true,list:cur});
}catch(eAd){ return extJson_({ok:false,error:String(eAd.message||eAd)}); }
}
if(t==='convoc_att_remove'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
var cur2=convocAttList_().filter(function(x){return x.id!==String(d.id||'');});
try{ DriveApp.getFileById(String(d.id||'')).setTrashed(true); }catch(eRm){}
setSetting('CONVOC_ATT',JSON.stringify(cur2));
return extJson_({ok:true,list:cur2});
}
if(t==='ai_reply'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
return extJson_(aiReply_(d));
}
if(t==='ai_test'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
var rt=geminiAsk_('Reponds uniquement par le mot : OK');
return extJson_(rt.ok?{ok:true}:rt);
}
if(t==='dossier_files'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{
var mF=String(d.url||'').match(/[-\w]{25,}/);
if(!mF) return extJson_({ok:true,files:[],folder:''});
var fo=DriveApp.getFolderById(mF[0]), itF=fo.getFiles(), outF=[];
while(itF.hasNext()){ var fi=itF.next(); outF.push({name:fi.getName(),url:fi.getUrl(),mime:fi.getMimeType(),size:fi.getSize()}); }
outF.sort(function(a,b){return String(a.name).localeCompare(String(b.name),'fr');});
return extJson_({ok:true,files:outF,folder:fo.getUrl()});
}catch(eF){ return extJson_({ok:false,error:String(eF)}); }
}
if(t==='dossier_add'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{ return extJson_(dossierAdd_(d)); }catch(eA){ return extJson_({ok:false,error:String(eA.message||eA)}); }
}
if(t==='mail_import'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{ return extJson_(mailImport_(d)); }catch(eM){ return extJson_({ok:false,error:String(eM.message||eM)}); }
}
if(t==='drive_test'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{
var rootT=driveRoot_(), subT=rootT.createFolder('TEST - a supprimer');
subT.createFile(Utilities.newBlob('test depot CRCK','text/plain','test.txt'));
subT.setTrashed(true);
return extJson_({ok:true,name:rootT.getName(),url:rootT.getUrl()});
}catch(eT){ return extJson_({ok:false,error:String(eT)}); }
}
if(t==='convoc_test'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
var toT=msgParseDest_(d.to);
if(!toT || toT.length!==1) return extJson_({ok:false,error:'adresse de test invalide'});
var tplT={subject:d.subject,intro:d.intro,outro:d.outro};
var htmlT=buildConvocationHtml(d.title||'Formation',d.dateLabel||'',d.heure||'',d.lieu||'',d.desc||'',d.prenom||'Prenom',d.nom||'NOM',d.pole||'',tplT);
var optsT={htmlBody:htmlT,name:'CRCK Nouvelle-Aquitaine'};
try{ var attT=attBlobs_(d.attachments).concat(convocAttBlobs_([])); if(attT.length) optsT.attachments=attT; }catch(eT){ return extJson_({ok:false,error:String(eT.message||eT)}); }
mailSend_(toT[0],'[TEST] '+convocSubject_(d.title||'Formation',d.dateLabel||'',tplT),stripHtml(htmlT),optsT);
return extJson_({ok:true});
}
if(t==='messages_reply'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
return extJson_(msgReply_(d));
}
if(t==='survey_submit'){
try{ return extJson_(surveySubmit_(d)); }catch(eSS){ return extJson_({ok:false,error:'erreur enregistrement : '+String(eSS.message||eSS)}); }
}
if(t==='survey_admin'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{ return extJson_(surveyAdmin_(d)); }catch(eSA){ return extJson_({ok:false,error:String(eSA.message||eSA)}); }
}
if(t==='admin_add_candidate'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{ return extJson_(adminAddCandidate_(d)); }catch(eAC){ return extJson_({ok:false,error:String(eAC.message||eAC)}); }
}
if(t==='sent_list'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{ return extJson_(sentList_()); }catch(eS){ return extJson_({ok:false,error:String(eS.message||eS)}); }
}
if(t==='sent_action'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{ return extJson_(sentAction_(d)); }catch(eS2){ return extJson_({ok:false,error:String(eS2.message||eS2)}); }
}
if(t==='messages_update'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
return extJson_(msgUpdate_(d));
}
if(t==='cd_contact_save'){
var cdDept=findCdRoleByCode(String(d.code||''));
if(!cdDept) return extJson_({ok:false,error:'unauthorized'});
return extJson_(saveCdContact_(cdDept,d.contact||{}));
}
if(t==='settings_save'){
if(d.cdEmails!==undefined && !extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
if(d.demarchesHtml!==undefined){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
setDemarchesHtml_(String(d.demarchesHtml));
}
var ext=(d.refAuto!==undefined || d.ffckAuto!==undefined || d.ffckMail!==undefined || d.confirmAuto!==undefined || d.ffckTypes!==undefined || d.driveFolder!==undefined || d.geminiKey!==undefined || d.closures!==undefined || d.convocSubject!==undefined || d.convocIntro!==undefined || d.convocOutro!==undefined);
if(ext){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
if(d.refAuto!==undefined) setSetting('REF_AUTO',d.refAuto?'1':'');
if(d.confirmAuto!==undefined) setSetting('CONFIRM_OFF',d.confirmAuto?'':'1');
if(d.closures!==undefined) setSetting('CLOSURES',JSON.stringify(closuresClean_(d.closures)));
if(d.geminiKey!==undefined) setSetting('GEMINI_KEY',String(d.geminiKey).replace(/[^A-Za-z0-9_.\-]/g,'').slice(0,160));
if(d.driveFolder!==undefined) setSetting('DRIVE_FOLDER',String(d.driveFolder).trim().slice(0,300));
if(d.ffckTypes!==undefined) setSetting('FFCK_TYPES',String(d.ffckTypes).replace(/[^A-Za-z0-9,]/g,''));
if(d.convocSubject!==undefined) setSetting('CONVOC_SUBJECT',String(d.convocSubject).slice(0,200));
if(d.convocIntro!==undefined) setSetting('CONVOC_INTRO',String(d.convocIntro).slice(0,6000));
if(d.convocOutro!==undefined) setSetting('CONVOC_OUTRO',String(d.convocOutro).slice(0,6000));
if(d.ffckAuto!==undefined) setSetting('FFCK_AUTO',d.ffckAuto?'1':'');
if(d.ffckMail!==undefined && String(d.ffckMail).indexOf('@')>0) setSetting('FFCK_MAIL',String(d.ffckMail).trim());
}
if(d.poleNord!==undefined || d.poleSud!==undefined){
if(!isAuthorized(d)) return extJson_({ok:false,error:'unauthorized'});
if(d.poleNord!==undefined) setSetting('POLE_NORD',d.poleNord);
if(d.poleSud!==undefined) setSetting('POLE_SUD',d.poleSud);
}
return doPostCore(e);
}
if(t==='send_referents_now'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
var nb=autoSendReferentRecap(true);
return extJson_({ok:true,count:nb,active:getSetting('REF_AUTO','')==='1'});
}
if(t==='send_dossiers_ffck'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
return extJson_({ok:true,count:sendPendingDossiersFfck()});
}
if(t==='set_sante'){
if(!extCodeOk_(d)) return extJson_({ok:false,error:'code gestionnaire requis'});
try{
var shS=getInscSheet(), rS=parseInt(d.rowIndex,10);
if(!(rS>1) || rS>shS.getLastRow()) return extJson_({ok:false,error:'ligne introuvable'});
if(String(shS.getRange(rS,4).getValue())!==String(d.nom||'') || String(shS.getRange(rS,5).getValue())!==String(d.prenom||'')) return extJson_({ok:false,error:'la ligne a change, rechargez la liste'});
var vals={Regime:d.regime,Allergies:d.allergies};
for(var nmS in vals){
if(vals[nmS]===undefined) continue;
ensureColumn(shS,nmS);
var cS=colIndexByHeader(shS,nmS);
if(cS>-1) shS.getRange(rS,cS+1).setValue(String(vals[nmS]).slice(0,300));
}
return extJson_({ok:true});
}catch(errS){ return extJson_({ok:false,error:String(errS)}); }
}
var res=doPostCore(e);
/* regime alimentaire et allergies declares par le candidat : colonnes Regime / Allergies (creees au besoin) */
if(!t && d.nom && d.prenom && d.formation && (d.regime || d.allergies)){
try{
var shR=getInscSheet(), lastR=shR.getLastRow();
var tsR=shR.getRange(lastR,1).getValue();
if(tsR instanceof Date && (new Date()-tsR)<180000){
['Regime','Allergies'].forEach(function(nm){
var v=(nm==='Regime'?d.regime:d.allergies);
if(!v) return;
ensureColumn(shR,nm);
var ciR=colIndexByHeader(shR,nm);
if(ciR>-1) shR.getRange(lastR,ciR+1).setValue(String(v).slice(0,300));
});
}
}catch(errR){}
}
/* nouvelle inscription du formulaire public : transmission a la FFCK si l'interrupteur est actif */
if(!t && d.nom && d.prenom && d.formation && getSetting('FFCK_AUTO','')==='1'){
try{
var sh=getInscSheet(), last=sh.getLastRow();
var ts=sh.getRange(last,1).getValue();
if(ts instanceof Date && (new Date()-ts)<180000) sendDossierFfckRow(sh,last);
}catch(errF){}
}
return res;
}
function doGet(e){
var action=e && e.parameter && e.parameter.action;
if(action==='messages_list'){
if(String(e.parameter.code||'')!==getSetting('ADMIN_CODE','crck2026')) return extJson_({ok:false,error:'code gestionnaire requis'});
return extJson_(msgList_());
}
if(action==='fiche_get'){
return extJson_(ficheGet_(e.parameter.r,e.parameter.s));
}
if(action==='neighbors'){
return extJson_(neighborCounts_(e.parameter.formation||'',e.parameter.pole||'',e.parameter.dept||''));
}
var res=doGetCore(e);
if(action==='settings'){
try{
var o=JSON.parse(res.getContent());
o.refAuto=getSetting('REF_AUTO','')==='1';
o.confirmAuto=getSetting('CONFIRM_OFF','')!=='1';
o.ffckTypes=getSetting('FFCK_TYPES','MFPC');
o.driveFolder=getSetting('DRIVE_FOLDER','');
o.geminiSet=!!getSetting('GEMINI_KEY','');
o.closures=closuresGet_();
o.convocAtt=convocAttList_();
o.convocSubject=getSetting('CONVOC_SUBJECT','');
o.convocIntro=getSetting('CONVOC_INTRO','');
o.convocOutro=getSetting('CONVOC_OUTRO','');
o.replyFrom=getSetting('REPLY_FROM','nouvelleaquitaine@ffck.org, dlaurent@ffck.org, laurent.domi@free.fr');
o.ffckAuto=getSetting('FFCK_AUTO','')==='1';
o.ffckMail=getFfckMail();
o.poleNord=getSetting('POLE_NORD','');
o.poleSud=getSetting('POLE_SUD','');
o.demarchesHtml=getDemarchesHtml_();
return extJson_(o);
}catch(err){}
}
return res;
}
createDailyTrigger dans le menu deroulant (a cote du bouton Executer), puis cliquez ExecuterautoriserDrive dans le menu deroulant, cliquez Executer et acceptez les autorisations si une fenetre s'ouvre (si rien ne s'ouvre, c'est deja autorise). Puis, sur le site : Reglages techniques → Tester le depot dans Drive.autoriserGemini (acceptez les autorisations), puis redeployez une Nouvelle version.majAdresses (elle remplace l'ancienne adresse du secretariat par nouvelleaquitaine@ffck.org et retire l'adresse gmail), ou modifiez-les dans Envoi & Tableur et Boite a lettres.Les pièces déposées par les candidats sont rangées dans un sous-dossier par candidat. Par défaut, dans un dossier « CRCK NA - Dossiers inscriptions » à la racine du Drive du compte qui héberge le script. Pour utiliser un dossier précis (ex. « pj »), collez ici son lien Drive.
Permet, dans « Questions des visiteurs », de préremplir un brouillon de réponse. Vous le relisez, vous le corrigez, vous l'envoyez : rien ne part tout seul. Créez une clé gratuite sur aistudio.google.com/apikey (connexion avec un compte Google, bouton « Create API key »), puis collez-la ici. Elle reste dans le script Google, jamais visible des visiteurs. Seuls le texte de la question et les infos du calendrier/démarches sont envoyés à Google.
Un bandeau affiche en haut du site pour tous les visiteurs, et que vous pouvez aussi envoyer par e-mail a tous les inscrits en un clic.
Adresses (separees par des virgules) qui recoivent le recapitulatif colore de toutes les inscriptions, a chaque nouvelle inscription et a la demande. Laissez vide pour garder l'adresse par defaut (nouvelleaquitaine@ffck.org).
Chaque jour, si la liste des inscrits a changé, le récapitulatif est envoyé aux contacts de la rubrique Contacts ayant une adresse e-mail. Quand l'envoi est bloqué, les référents ne reçoivent rien : seul l'administrateur (dlaurent@ffck.org) continue de le recevoir.
Le pôle d'une session est déduit automatiquement du titre, de la description et du lieu de l'événement : si l'un de ces mots y figure, la session est classée Nord ou Sud. Séparez les mots par des virgules.
Quand cette option est activée, chaque nouvelle inscription des formations cochées ci-dessous est envoyée par e-mail au service formation de la FFCK, avec la fiche du candidat, le lien de son dossier et ses pièces jointes (copie à l'administrateur). Bloquée, rien n'est envoyé.
Chaque CD reçoit un code personnel. Avec ce code, ses cadres voient seulement les inscrits de leur département et peuvent modifier leur propre carte de contact : rien d'autre. Le code est envoyé par e-mail à l'adresse indiquée.
Chargement...
C'est le mot de passe qui ouvre cet Espace Gestionnaire, pour tous les appareils. Choisissez un code d'au moins 8 caractères, que vous ne réutilisez pas ailleurs, et communiquez-le seulement aux gestionnaires du comité. Si vous l'oubliez, le lien « code oublié » de l'écran de connexion en envoie un nouveau à l'adresse du comité.
Accès en consultation : vous voyez les inscrits de votre département. Vous pouvez modifier uniquement votre carte de contact (ci-dessous).
Chargement...
Les questions posées depuis le site arrivent ici et par e-mail aux adresses ci-dessous. Cliquez sur « Répondre » : la réponse s'écrit juste sous la question (gras, police, couleur…). Vous choisissez les destinataires, les adresses en copie et l'adresse de réponse ; le message part directement depuis le site.
Une enquête par module. Envoyez le lien aux stagiaires (message, convocation, WhatsApp…). Eux ne voient jamais les résultats : seules les adresses ci-dessous reçoivent chaque réponse par e-mail, et tout est conservé dans le tableur (onglet « Enquete M1 »).
Cochez les stagiaires : chacun reçoit son propre message (« Bonjour Prénom Nom »), sans voir les adresses des autres.
Ce texte remplace le paragraphe d'explication dans le courrier envoye automatiquement a chaque candidat apres son inscription. Le reste (recapitulatif de la formation, pieces manquantes, signature) reste automatique et n'est pas modifiable ici.
Modifiez ici le texte de la convocation et vérifiez l'aperçu. Ce modèle sert à tous les envois (automatique J-30, manuel, mise à jour après changement de date). Vous pouvez utiliser {prenom} {nom} {formation} {date} {heure} {lieu}. Le tableau des informations (formation, date, heure, lieu), la description de l'événement et l'interlocuteur du pôle restent ajoutés automatiquement.
Envoyez un message libre par e-mail a un inscrit precis. Vous pouvez retirer ou ajouter des destinataires et des adresses en copie, et mettre le texte en forme (gras, police, taille, couleur).
Chaque message envoyé depuis la boîte à lettres candidats ou en réponse à un visiteur est conservé ici. Mettez-le à la corbeille pour le retirer de la liste ; il peut être restauré tant que vous ne l’avez pas supprimé définitivement.