1<!DOCTYPE html> 2<html lang="fr"> 3<head> 4<meta charset="UTF-8"> 5<!-- This site is hosted on Netlify. Anyone can build and deploy a site 6 like this one for free: https://netlify.new/?utm_campaign=loops&utm_source=ai-legible&utm_medium=owned&utm_content=comment&utm_id=c546c6af-5f84-4186-88af-d86070818407 7 Netlify hosting facts for this site: static/SSR served via Netlify Edge. --> 8<meta name="viewport" content="width=device-width, initial-scale=1.0"> 9<title>Endolight Formation â Espace Formateur</title> 10<link href="https://fonts.googleapis.com/css2?family=Raleway:wght@300;400;500;600;700&family=Open+Sans:wght@300;400;500&display=swap" rel="stylesheet">
11<script type='text/javascript' src='https://api.netlify.com/hiqqRFFQ6sMDieSyFuKmpyLwP6F4m6wi03Zq1T11jMsmFzr_ssGKdPe_nvD6Wh6yenRWzTGlNBeTLvxgcDRtk8foLZyw1jDozkrcPkT5QnUc5XZQUUnRUEZPYlbu9cSu4T-JSaE1_FR1TxvRCUkJe1xCg-MJ_GO5g_Cyz2HvskR-ps6D8vfnzLG8Ce5OywnmpYC6wK9oye7vXU3XSKkw1b-X9lVrsG8a4NYI77YFIKMILBAC1Fe_qBqFjNC5iOMcarMGTY97FLnEjmwx-w9P7QPxEaKTYcTZnneP9xnaXQAUXDZAx2oKtLZsgnRLp-Mkv8mz5HD8b_1bFzUR9Urzeln_DNmNns6iqMQ4yBiAdO_JvEIO6p3Ne9EeJYOBO2YiXcX1ey9F6hoqqMOfj4MN2DFaRCpsP_ke_O44qAGLaI-Xh6P7Ouh6Msw0PiubNrDmFHkGXy5wJizFEE70PQEFG8ZMUZFHt3OOCXw58pFpXNsSaZQOsfuPai26BQC-XbdqP4G_T0Fi08proL3f1Zqr'></script>
11<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
11 12<style> 13.login-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(145deg,#1a3560,#2ea8d5);z-index:9999;display:flex;align-items:center;justify-content:center;} 14.login-box{background:#fff;border-radius:16px;padding:40px 36px;width:90%;max-width:380px;box-shadow:0 20px 60px rgba(26,53,96,.35);text-align:center;} 15.login-box .logo-name{font-family:"Raleway",sans-serif;font-weight:700;font-size:1.5rem;letter-spacing:.08em;margin-bottom:4px;} 16.login-box .logo-name .endo{color:#1a3560;}.login-box .logo-name .light{color:#2ea8d5;} 17.login-box .logo-sub{font-family:"Raleway",sans-serif;font-size:.62rem;letter-spacing:.28em;color:#7a8fa6;text-transform:uppercase;margin-bottom:28px;} 18.login-box p{font-size:.82rem;color:#7a8fa6;margin-bottom:16px;} 19.login-box input{width:100%;border:1px solid #d0dde8;border-radius:8px;padding:12px 16px;font-family:"Open Sans",sans-serif;font-size:.9rem;color:#1a2b3c;outline:none;text-align:center;letter-spacing:.1em;margin-bottom:12px;transition:border .2s,box-shadow .2s;} 20.login-box input:focus{border-color:#2ea8d5;box-shadow:0 0 0 3px rgba(46,168,213,.12);} 21.login-box .login-btn{width:100%;background:#1a3560;color:#fff;border:none;border-radius:8px;padding:12px;font-family:"Raleway",sans-serif;font-size:.85rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;cursor:pointer;transition:background .2s;} 22.login-box .login-btn:hover{background:#142a50;} 23.login-box .login-error{color:#e05555;font-size:.78rem;margin-top:8px;display:none;} 24:root { 25 --navy:#1a3560; --blue:#2ea8d5; --blue-lt:#a8d8ee; --blue-xs:#e4f4fb; 26 --bg:#f2f5f8; --white:#fff; --text:#1a2b3c; --muted:#7a8fa6; 27 --border:#d0dde8; --green:#2ecc8a; --red:#e05555; --shadow:0 6px 32px rgba(26,53,96,.10); 28} 29*{box-sizing:border-box;margin:0;padding:0;} 30body{font-family:'Open Sans',sans-serif;background:linear-gradient(145deg,#eaf2f8,#f8fafc 60%,#e8f0f8);min-height:100vh;} 31header{background:var(--navy);padding:0 32px;display:flex;align-items:center;gap:18px;height:70px;box-shadow:0 3px 20px rgba(26,53,96,.25);} 32.logo-name{font-family:'Raleway',sans-serif;font-weight:700;font-size:1.25rem;letter-spacing:.08em;} 33.logo-name .endo{color:#fff;}.logo-name .light{color:var(--blue);} 34.logo-sub{font-family:'Raleway',sans-serif;font-size:.6rem;letter-spacing:.28em;color:var(--blue-lt);margin-top:3px;text-transform:uppercase;} 35.badge{margin-left:auto;background:rgba(46,168,213,.15);border:1px solid rgba(46,168,213,.4);color:var(--blue);font-family:'Raleway',sans-serif;font-size:.72rem;font-weight:600;letter-spacing:.06em;padding:6px 14px;border-radius:4px;text-transform:uppercase;} 36.accent-bar{width:100%;height:3px;background:linear-gradient(90deg,var(--navy),var(--blue) 60%,var(--blue-lt));} 37.container{max-width:980px;margin:0 auto;padding:28px 20px 60px;} 38 39/* Tabs */ 40.tabs{display:flex;gap:4px;margin-bottom:24px;background:var(--white);border:1px solid var(--border);border-radius:10px;padding:5px;} 41.tab{flex:1;padding:10px;font-family:'Raleway',sans-serif;font-size:.78rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;border:none;background:transparent;color:var(--muted);border-radius:7px;cursor:pointer;transition:all .2s;} 42.tab.active{background:var(--navy);color:#fff;} 43.tab-content{display:none;}.tab-content.active{display:block;} 44 45.card{background:var(--white);border:1px solid var(--border);border-radius:10px;padding:24px 26px;box-shadow:var(--shadow);margin-bottom:20px;} 46.card-title{font-family:'Raleway',sans-serif;font-size:.85rem;font-weight:600;color:var(--navy);text-transform:uppercase;letter-spacing:.06em;margin-bottom:16px;padding-bottom:12px;border-bottom:2px solid var(--blue-xs);} 47.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:13px;}
48.form-group{display:flex;flex-direction:column;gap:5px;} 49.form-group.full{grid-column:1/-1;} 50label{font-size:.72rem;font-weight:600;color:var(--navy);letter-spacing:.03em;text-transform:uppercase;} 51input,select,textarea{border:1px solid var(--border);border-radius:6px;padding:10px 13px;font-family:'Open Sans',sans-serif;font-size:.84rem;color:var(--text);outline:none;background:#fafcfe;transition:border .2s,box-shadow .2s;} 52input:focus,select:focus,textarea:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(46,168,213,.12);} 53textarea{min-height:65px;resize:vertical;} 54select{cursor:pointer;} 55 56.docs-grid{display:flex;flex-wrap:wrap;gap:9px;margin-top:4px;} 57.doc-check{display:flex;align-items:center;gap:7px;background:var(--bg);border:1px solid var(--border);border-radius:20px;padding:7px 14px;cursor:pointer;font-size:.79rem;color:var(--navy);font-weight:500;transition:all .18s;user-select:none;} 58.doc-check.active{background:var(--navy);color:#fff;border-color:var(--navy);} 59 60.btn{border:none;border-radius:6px;font-family:'Raleway',sans-serif;font-size:.8rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase;cursor:pointer;transition:all .2s;padding:10px 22px;} 61.btn-blue{background:var(--blue);color:#fff;}.btn-blue:hover{background:#2494bc;} 62.btn-navy{background:var(--navy);color:#fff;}.btn-navy:hover{background:#142a50;} 63.btn-outline{background:transparent;color:var(--navy);border:1px solid var(--border);}.btn-outline:hover{border-color:var(--blue);color:var(--blue);} 64.btn-sm{padding:7px 14px;font-size:.72rem;} 65.btn-danger{background:transparent;color:var(--red);border:1px solid var(--red);}.btn-danger:hover{background:var(--red);color:#fff;} 66 67/* Formations list */ 68.formation-card{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:16px 18px;margin-bottom:10px;display:flex;align-items:center;gap:14px;cursor:pointer;transition:all .2s;} 69.formation-card:hover,.formation-card.selected{background:var(--blue-xs);border-color:var(--blue);} 70.formation-card.selected{border-width:2px;} 71.f-badge{padding:4px 11px;border-radius:20px;font-size:.7rem;font-weight:600;font-family:'Raleway',sans-serif;text-transform:uppercase;letter-spacing:.05em;} 72.f-badge.presentiel{background:#e8f5e9;color:#2e7d32;} 73.f-badge.distanciel{background:#e3f2fd;color:#1565c0;} 74.f-title{font-family:'Raleway',sans-serif;font-weight:600;font-size:.92rem;color:var(--navy);} 75.f-meta{font-size:.75rem;color:var(--muted);margin-top:2px;} 76.f-actions{margin-left:auto;display:flex;gap:7px;} 77 78/* Apprenants */ 79.apprenant-row{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px 16px;margin-bottom:8px;display:flex;align-items:center;gap:12px;} 80.apprenant-row:hover{box-shadow:0 2px 10px rgba(26,53,96,.08);} 81.ap-name{font-family:'Raleway',sans-serif;font-weight:600;font-size:.88rem;color:var(--navy);} 82.ap-meta{font-size:.73rem;color:var(--muted);margin-top:1px;} 83.ap-actions{margin-left:auto;display:flex;gap:7px;align-items:center;} 84 85.link-box{background:var(--blue-xs);border:1px solid var(--blue-lt);border-radius:7px;padding:10px 13px;display:flex;align-items:center;gap:9px;margin-top:8px;} 86.link-box input{flex:1;background:transparent;border:none;font-size:.75rem;color:var(--navy);outline:none;} 87.copy-btn{background:var(--blue);color:#fff;border:none;border-radius:5px;padding:5px 11px;font-size:.7rem;font-family:'Raleway',sans-serif;font-weight:600;cursor:pointer;white-space:nowrap;text-transform:uppercase;transition:background .2s;} 88.copy-btn:hover{background:#2494bc;}.copy-btn.copied{background:var(--green);} 89 90.doc-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:5px;} 91.doc-tag{background:var(--white);border:1px solid var(--blue-lt);border-radius:11px;padding:2px 9px;font-size:.68rem;color:var(--navy);font-weight:500;} 92 93.empty-state{text-align:center;padding:40px 20px;color:var(--muted);} 94.empty-state .icon{font-size:2.2rem;margin-bottom:10px;} 95.empty-state p{font-size:.83rem;} 96 97/* Stats bar */ 98.stats-bar{display:flex;gap:12px;margin-bottom:22px;flex-wrap:wrap;} 99.stat-card{background:var(--white);border:1px solid var(--border);border-radius:9px;padding:14px 18px;flex:1;min-width:130px;box-shadow:var(--shadow);} 100.stat-card .val{font-family:'Raleway',sans-serif;font-size:1.6rem;font-weight:700;color:var(--navy);} 101.stat-card .lbl{font-size:.72rem;color:var(--muted);margin-top:2px;text-transform:uppercase;letter-spacing:.04em;} 102 103/* Status dot */ 104.status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;} 105.dot-green{background:#3dd68c;}.dot-blue{background:var(--blue);}.dot-gray{background:#ccc;} 106 107/* Loader */ 108.loader{display:inline-block;width:16px;height:16px;border:2px solid var(--blue-lt);border-top-color:var(--blue);border-radius:50%;animation:spin .7s linear infinite;vertical-align:middle;margin-right:6px;} 109@keyframes spin{to{transform:rotate(360deg);}} 110 111.modal-docs-formation{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(26,53,96,.45);z-index:900;display:flex;align-items:center;justify-content:center;} 112.modal-formation{background:#fff;border-radius:12px;padding:28px;width:90%;max-width:560px;max-height:85vh;overflow-y:auto;box-shadow:0 20px 60px rgba(26,53,96,.25);} 113.modal-formation h3{font-family:"Raleway",sans-serif;font-size:1rem;font-weight:600;color:var(--navy);margin-bottom:4px;} 114.modal-formation p.sub{font-size:.78rem;color:var(--muted);margin-bottom:18px;} 115.upload-zone{border:2px dashed var(--blue-lt);border-radius:8px;padding:16px;margin-bottom:12px;transition:border .2s;} 116.upload-zone:hover{border-color:var(--blue);} 117.upload-zone label{font-size:.72rem;font-weight:600;color:var(--navy);text-transform:uppercase;letter-spacing:.04em;display:block;margin-bottom:6px;} 118.upload-zone .file-info{font-size:.78rem;color:var(--muted);margin-top:4px;} 119.upload-zone .file-info.ok{color:var(--green);} 120.upload-input{display:none;} 121.upload-btn{background:var(--blue-xs);border:1px solid var(--blue-lt);color:var(--navy);border-radius:6px;padding:7px 14px;font-size:.78rem;font-family:"Raleway",sans-serif;font-weight:600;cursor:pointer;transition:all .2s;} 122.upload-btn:hover{background:var(--blue);color:#fff;border-color:var(--blue);} 123.doc-existant{display:flex;align-items:center;gap:8px;background:var(--blue-xs);border:1px solid var(--blue-lt);border-radius:6px;padding:7px 12px;margin-top:4px;font-size:.78rem;color:var(--navy);} 124.doc-existant .del-doc{margin-left:auto;background:transparent;border:none;color:var(--red);cursor:pointer;font-size:.9rem;} 125.modal-edit{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(26,53,96,.45);z-index:900;display:flex;align-items:center;justify-content:center;} 126.modal-edit-inner{background:#fff;border-radius:12px;padding:28px;width:90%;max-width:520px;max-height:88vh;overflow-y:auto;box-shadow:0 20px 60px rgba(26,53,96,.25);} 127.modal-edit-inner h3{font-family:"Raleway",sans-serif;font-size:1rem;font-weight:600;color:var(--navy);margin-bottom:4px;} 128.modal-edit-inner p.sub{font-size:.78rem;color:var(--muted);margin-bottom:18px;} 129.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(26,53,96,.45);z-index:900;display:flex;align-items:center;justify-content:center;} 130.modal{background:#fff;border-radius:12px;padding:28px;width:90%;max-width:460px;box-shadow:0 20px 60px rgba(26,53,96,.25);} 131.modal h3{font-family:'Raleway',sans-serif;font-size:1rem;font-weight:600;color:var(--navy);margin-bottom:6px;} 132.modal p{font-size:.8rem;color:var(--muted);margin-bottom:16px;} 133.modal-docs{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px;} 134.modal-actions{display:flex;gap:9px;justify-content:flex-end;} 135.notif{position:fixed;bottom:22px;right:22px;background:var(--navy);color:#fff;border-radius:8px;padding:12px 20px;font-size:.82rem;font-family:'Raleway',sans-serif;font-weight:500;box-shadow:var(--shadow);transform:translateY(80px);opacity:0;transition:all .3s;z-index:999;} 136.notif.show{transform:translateY(0);opacity:1;} 137.divider{height:1px;background:var(--border);margin:16px 0;} 138 139@media(max-width:650px){.form-grid{grid-template-columns:1fr;}.stats-bar{flex-direction:column;}.container{padding:16px 12px 40px;}} 140</style> 141</head> 142<body> 143 144<!-- ÃCRAN LOGIN --> 145<div class="login-overlay" id="login-overlay"> 146 <div class="login-box"> 147 <div class="logo-name"><span class="endo">ENDO</span><span class="light">LIGHT</span></div> 148 <div class="logo-sub">Formation</div> 149 <p>Espace formateur â accès restreint</p> 150 <input type="password" id="login-input" placeholder="â¢â¢â¢â¢â¢â¢â¢â¢" onkeydown="if(event.key==='Enter') verifierMdp()" /> 151 <button class="login-btn" onclick="verifierMdp()">Accéder</button> 152 <div class="login-error" id="login-error">Mot de passe incorrect</div> 153 </div> 154</div> 155 156<header> 157 <img src="data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAl
157kZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAQnBCcDASIAAhEBAxEB/8QAHQABAAICAwEBAAAAAAAAAAAAAAEHBQYECAkDAv/EAE4QAQABAwIBBQkMCAQFAwUAAAABAgMEBQYRBxIhMUEIExY3UWFxgZEUIjJSU3R1k6Gxs9EjM0JVYnKywRVDY4KSosLh8CRUg0VzhMPS/8QAGwEBAQADAQEBAAAAAAAAAAAAAAEEBQYCAwf/xAA1EQEAAgEBBAcGBgIDAQAAAAAAAQIDBAURITESFTJBUXHREyIzYZGhBhZSgbHhFPAjQsFD/9oADAMBAAIRAxEAPwDruA6pxwAAAIACgAAAAACEgAACEggAAAKAAAAAAAAAAAAAAAAACAAoAAAIACgAAIBIAAAgAAAAAKAAAAAAAAACAAoAAAAAIACgAAAAAAAAAgAKAAAAACAAoAAAAAAAAAAAAAAAAAAAIACgAAAAAAAAAgAKACAAoAIACgIBIACEgAAAAgISKAAACAAoAIACgAAAAAgAKAAAAAAAAAAAAAAACAAoAAAIACgAAAAAAAAAgAAAKAAACAAAAoAAAAAAAAAIACgAgAKAgEgCAAAIFSAAAAAAAIACgAgAKhIAACAAoAAAAAAAAAIACgAgAAAKAAAAAAAAAgEgAAAAAAAAACEgACAAoAIACgAgAKAAAAAAAAAAAAACAAoAIACgAAAgAKAAAAISAAAAAhIAACAAAAoAAAIACgAAAgAKAAAAACAAoAAAAAIACgAAAAAgAKAAAAAAAAAAAAAAAAAAAAAAACAAAAoAAhIAAIACgAAAAAgAAAKAAACAAoAIAAACgAgAKAAAAACAAoAAAAAAAAAAAAAAAIAAISCgAAAAAAAAAISAAAAAgAKAAAAACAAoAIACgAgAKAAISAAAAAAAAAAAACAAoAAAIACgAgAKAAAAAAAAACAAoAIACgAgAKACAAoAAAAAIACgAAAAAgAAISKAAAAACAAoAAAIACgAAAAAAAgAAAKAAAAAAAAAAACAAoAAAAAAAIACgAAAAAgB2CgAAAAAAAgAAAKAAAAAAAAAAAAAAAAAAAAACAAoAIACghIAAAAAAAAgAAAAAKAAAAACAAoAAAAAAAAAIACgAAAAAAAAAAAAAgAKAAAAAAAAAAAAAAACAAoAIAAACgAgAKAAAAAAACAAoAIAAAAAAACgAAAgAKAAAAISAAAAAISACEgAAACAAoAIACgAAAAAAAAAgAKAAAAACAAoAAAAAAAAAAAAAAAAAAAAAAAIACgAAAAAgAKAAAAAAAAAAAAAAAAACAAAAAAoAAAIAAACgAAAAAAAAAgAAAAAAAKISAAAAAACAAoAAAAAAB2AAAAAAAAAAAACAAoAIACgAAAAAgAKAAAAACAAoAAAB2gCAAoAAAIAAACgAAAAAAAAAAAgAAAKAAAAAAACAAoAAAIACgAAAAAAAAAAAAAAAAAgAKACAAoAAAAAIACgAAAgAKAAACAAoAAAAAAAAAAAIACiEgAAgAKAAAAAAAAACAAAAoAAAICEigAAAgAKAAAAAAACAAAAoAAAAAIAAACgAAAAAAhIAAAAgAKhIAAAAAAAACAAoAAAAAICEigAAAAAAAAAAAAAgAKACAAoAAAIACgAAAgAKAAACAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAIACgIBIAAAAAAAgAAAKAAACAAAIFSAAAAAAAAAIAAAAACgAgAKAAAAAAACISAoAIACgAgAAAKAAAAAAACAAAAAAAAAAoAIACgAAAAAAAgAAAAAKAAAAAAAAAAAAAA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AAAAAAAEABRCUAkAAAAAQAFAAAAAAABAAUAAAAAAAAAAAAAAAAAEABQAAAQAAAFAAAAAAAAABAAAAAAUAAAAAAAAAEABQAQAFAAQkAAAABAAUAEABQAAAQAFAAABAAAAUAAAEABQAAAQAFABAAUAAAEABQAAAAAAAAAQAFAAAAAAAAAAABAAUAAAAAEABQAAAAAAAQAFAAAAAAABAAUAEABQAAAAAQAFAAAAAAAAAAAAAAAABCQABAAAAUAAAAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAQAFAAAAABAAUAAAAAEAAAAABQAAAQAAAFAAAAAAABAAUAEABQAAAAAAAQAAAFAAAAAAAAABAAUAAAAAAAEABQAQAFAABCQAAAAAAABAAUAAAAAEABQAQAFAAAAABAAUAAAABAJAAAAAAAAABCQAAAAAAAAEAAAAAAAAAAABQAAAAAQAAAAAFAAAAABAAAAUAAAAAEABQAAAAAAAAAQQlAqQAAAAAEJAAAABAAUAAAAAAAEABQAAAAAAAAAQAFABAAAAAAUAAAAAEABQAAAQAFAAAAABAAUAAAAAAQkAAAAAAAAAAAAAAAAAAAAAAAAEABQAAAAAQAAAFAAABAAUAAAEAAAAABQAAAQAFAAAAAAABAAAAUAEAAAAABQAQAAAFABAAUAEAAABQAAAAAQAFABAAUAAAAAAAAAAAAAAAEAAAAAAABQAAAAAQAAAAAAAAAFABAAAAUAAAEABQAAAAAAAAAQAFABAAAAUAAAAAEABQAQAAAAAFAAAAAAAAAAAAAAABAAUAAAEAQKkAAAQAFAAAAAAABAAUAAAEAAAAABQAAAQAABAqQAAAABAAUAAAAAEAAAAABQAAAQAAAFAAABAAUAEAAABQAAAQAFABAAUAAAAAAAAAABAJAAAABAJAEABQAAAAAAAAAAAAAEJAAAAAAAQAFAAAAAAABAAUAAAEABQAAAAAQAFAAABAAUAAAAAEABQAAAAAQAFEJAABAAAECpAAAAAEAAAAABQAQAAAFAAABAAAAUAAAEABQAAAAAAAAAAAAARCQFAAABAAUAAAEABQAAAAAQAFAAAAABAAUAAAEABQAAQkAAAAAAAAAAAAQAFABAAUAAAAAEABQAQAFAAABAAUAAAAAEABQAQAFABAAUAEAAABQAQAAAAAFAAAAABAAAAAAUAAAAAAAAAAAAAEABQAAAAAAAAAAAAAAAAAQEJFAAAAAAAAAAAAABAAUAEAAABQAAAAAAAAAAAAAQAFAAAAAAABAAAAUAAAAAEABQAAABCQAAAAQAAAFAAAAABAAAAUAEAAABUJAAAAAAAAAAAQAFABAAUAEAAABQAAAAAAAQAFAAABAAUAAAAAAAAAEABQAQAFAAAAABAAAAUAAAAAAAAAEABQAAEAkAAAAAAAAAQAFAAAAABAAAAVCQAAAAAAEABQAAAAAAAQAAEJFAAAAABAAAAUAAAEABQAAAAAAAQAFAAAAQkAABAQkUAAAAAAAAAAAAAAAAAEABQAAAAAAAAAECQAAAAAAAAAAEJAAAAAQAAAFAAAAABAAUAAAAAAAAAAAAAEABQAQAFABAAAABCQUAAAAAEABQAAAQAAAAEJFBCQAAAAAAAAAAAAAAABAAUAEAAABQAAABCQAAAAAAQAAAAAFABAAUAAAAAAAEABQAQAFABAAUEJAAAAAAAAEABQAQAAAFABAAUAAEJAAEABQAAAEJAAAQAFBCQABAAUAAAAAAAAAEABQAAAAAQAFABAAUAAAAAAAAAAAAAAAAAAAAAAAAAkAABCQAAQAFABAAUAAAAAAAAAAAAAAAAAAQkAAAQkAAAAEABQAAAAAAAAEAkAAAAAQAAAFABAAAAUAAAAAAAAAEABQAAAAAAAAAAAAAAAAAQAFAAAAAAAAABAAUAAAAAAAAAAAAAEABQAAAAAAAAAAAQAFABAAUAAAAAEABQAAAAAQAFAAABAAUAEABQAAAAAAAQAAAFAAAAQkBAAUAAAAAAAAAAAAAEAAAAABQAAAAAAAQAFAAAAAAAAAAAAABAAUAEAAABQAAAAAAAAAAAAAAAAAAAAAAAQAFAAAAABAAAAUAAAAAAAAAAAAAAQAJAEABQAQAFABAAUAAAAAEAAABQAAAQAFAAAAAAAAABAAUAAAAAAAAAAAAAAAAAAAAAEABQAQAFAAAAABAAUQAJAAAAAAAAAAAAAAAAAAAEABQAAAAAAAAAAAAAQAFAAAAAAf/Z" style="height:46px;width:46px;object-fit:contain;border-radius:6px;" /> 158 <div> 159 <div class="logo-name"><span class="endo">ENDO</span><span class="light">LIGHT</span></div> 160 <div class="logo-sub">Formation</div> 161 </div> 162 <div class="badge">â Espace Formateur</div> 163</header> 164<div class="accent-bar"></div> 165 166<div class="container"> 167 168 <!-- STATS --> 169 <div class="stats-bar" id="stats-bar"> 170 <div class="stat-card"><div class="val" id="stat-formations">â</div><div class="lbl">Formations</div></div> 171 <div class="stat-card"><div class="val" id="stat-apprenants">â</div><div class="lbl">Apprenants</div></div> 172 <div class="stat-card"><div class="val" id="stat-docs">â</div><div class="lbl">Documents générés</div></div> 173 <div class="stat-card"><div class="val" id="stat-presentiel">â</div><div class="lbl">Présentiel</div></div> 174 <div class="stat-card"><div class="val" id="stat-distanciel">â</div><div class="lbl">Distanciel</div></div> 175 </div> 176 177 <!-- TABS --> 178 <div class="tabs"> 179 <button class="tab active" onclick="switchTab('formations')">ð Formations</button> 180 <button class="tab" onclick="switchTab('apprenants')">ð¥ Apprenants</button> 181 <button class="tab" onclick="switchTab('nouvelle')">â Nouvelle formation</button> 182 </div> 183 184 <!-- TAB : FORMATIONS --> 185 <div class="tab-content active" id="tab-formations"> 186 <div class="card"> 187 <div class="card-title" style="display:flex;justify-content:space-between;align-items:center;">
188 <span>ð Mes formations</span> 189 <button class="btn btn-blue btn-sm" onclick="switchTab('nouvelle')">+ Nouvelle</button> 190 </div> 191 <div id="formations-list"><div class="empty-state"><div class="loader"></div><p>Chargementâ¦</p></div></div> 192 </div> 193 </div> 194 195 <!-- TAB : APPRENANTS --> 196 <div class="tab-content" id="tab-apprenants"> 197 <div class="card"> 198 <div class="card-title" style="display:flex;justify-content:space-between;align-items:center;"> 199 <span>ð¥ Apprenants â <span id="ap-formation-title" style="color:var(--blue);font-weight:400;text-transform:none;letter-spacing:0;">Sélectionnez une formation</span></span> 200 <select id="ap-filter" onchange="loadApprenants()" style="font-size:.78rem;padding:6px 10px;max-width:220px;"> 201 <option value="">Toutes les formations</option> 202 </select> 203 </div> 204 <div id="apprenants-list"><div class="empty-state"><div class="icon">ð</div><p>Sélectionnez une formation dans l'onglet Formations pour voir ses apprenants.</p></div></div> 205 </div> 206 207 <!-- Ajouter apprenant --> 208 <div class="card" id="add-apprenant-card"> 209 <div class="card-title">â Ajouter un apprenant</div> 210 <div class="form-grid"> 211 <div class="form-group"><label>Prénom</label><input type="text" id="a-prenom" placeholder="Marie" /></div> 212 <div class="form-group"><label>Nom</label><input type="text" id="a-nom" placeholder="DUPONT" /></div> 213 <div class="form-group"><label>Email</label><input type="email" id="a-email" placeholder="[email protected]" /></div> 214 215 <div class="form-group full"><label>Adresse</label><input type="text" id="a-adresse" placeholder="12 rue des Fleurs, 69001 Lyon" /></div> 216 <div class="form-group full"> 217 <label>Formation</label> 218 <select id="a-formation"><option value="">â Choisir une formation â</option></select> 219 </div> 220 <div class="form-group full"> 221 <label>Documents à générer</label> 222 <div class="docs-grid" id="docs-grid"> 223 <label class="doc-check active" onclick="toggleDoc(this,'convocation')">ð Convocation</label> 224 <label class="doc-check active" onclick="toggleDoc(this,'convention')">ð Convention</label> 225 <label class="doc-check active" onclick="toggleDoc(this,'facture')">ð§¾ Facture</label> 226 <label class="doc-check active" onclick="toggleDoc(this,'attestation')">â Attestation</label> 227 <label class="doc-check active" onclick="toggleDoc(this,'certificat')">ð Certificat</label> 228 <label class="doc-check active" onclick="toggleDoc(this,'inscription')">ð Attestation d'inscription</label> 229 </div> 230 </div> 231 </div> 232 <div style="display:flex;gap:9px;margin-top:16px;flex-wrap:wrap;"> 233 <button class="btn btn-navy" onclick="ajouterApprenant()">Enregistrer & générer le lien</button> 234 <button class="btn btn-outline" onclick="resetApprenant()">Réinitialiser</button> 235 </div> 236 </div> 237 </div> 238 239 <!-- TAB : NOUVELLE FORMATION --> 240 <div class="tab-content" id="tab-nouvelle"> 241 <div class="card"> 242 <div class="card-title">ð Créer une formation</div> 243 <div class="form-grid"> 244 <div class="form-group full"><label>Intitulé</label><input type="text" id="f-titre" placeholder="Endodontie clinique â niveau 1" /></div> 245 <div class="form-group"><label>Référence</label><input type="text" id="f-ref" placeholder="EL-2025-06" /></div> 246 <div class="form-group"> 247 <label>Type</label> 248 <select id="f-type"> 249 <option value="Présentiel">Présentiel</option> 250 <option value="Distanciel">Distanciel</option> 251 </select> 252 </div> 253 <div class="form-group"><label>Date(s)</label><input type="text" id="f-dates" placeholder="12 et 13 juin 2025" /></div> 254 <div class="form-group"><label>Lieu</label><input type="text" id="f-lieu" placeholder="15 av. Pasteur, 75011 Paris" /></div> 255 <div class="form-group"><label>Formateur(s)</label><input type="text" id="f-formateur" placeholder="Dr Marie Dupont" /></div> 256 <div class="form-group"><label>Montant TTC (â¬)</label><input type="text" id="f-montant" placeholder="890" /></div> 257 <div class="form-group"> 258 <label>Statut</label> 259 <select id="f-statut"> 260 <option value="à venir">à venir</option> 261 <option value="En cours">En cours</option> 262 <option value="Terminée">Terminée</option> 263 </select> 264 </div> 265 <div class="form-group"><label>Durée</label><input type="text" id="f-duree" placeholder="Ex: 6h" /></div> 266 <div class="form-group full"><label>Organisme</label><input type="text" id="f-organisme" value="Endolight Formation" /></div> 267 <div class="form-group"><label>SIRET</label><input type="text" id="f-siret" value="98532401100016" /></div> 268 <div class="form-group"><label>Signataire</label><input type="text" id="f-signataire" value="Gregory Fejoz" /></div> 269 <div class="form-group"><label>Ville</label><input type="text" id="f-ville" value="Annecy" /></div> 270 <div class="form-group"><label>Adresse organisme</label><input type="text" id="f-adresse-org" value="9bis chemin des chapelaines 74940 ANNECY LE VIEUX" /></div> 271 <div class="form-group"><label>N° d'activité</label><input type="text" id="f-nda" value="84740489274" /></div> 272 </div> 273 <div style="margin-top:16px;display:flex;gap:9px;"> 274 <button class="btn btn-navy" onclick="creerFormation()">Créer la formation</button> 275 <button class="btn btn-outline" onclick="resetFormation()">Annuler</button> 276 </div> 277 </div> 278 </div> 279 280</div> 281 282<!-- MODAL DOCUMENTS FORMATION --> 283<div class="modal-docs-formation" id="modal-formation-overlay" style="display:none;" onclick="fermerModalFormation(event)"> 284 <div class="modal-formation" onclick="event.stopPropagation()"> 285 <h3>ð Documents de la formation</h3> 286 <p class="sub" id="mf-titre">Uploadez les documents à partager avec vos apprenants.</p> 287 288 <div id="mf-docs-list"></div> 289 <p style="font-size:.78rem;color:var(--muted);margin-top:14px;line-height:1.6;"> 290 ð¡ Pour ajouter des documents, uploadez-les directement dans <strong>Airtable</strong> 291 dans le champ <strong>Docs Formation</strong> (type Attachment) de la formation concernée.<br> 292 Ils apparaîtront automatiquement dans l'espace apprenant. 293 </p> 294 <div style="display:flex;gap:9px;justify-content:flex-end;margin-top:20px;"> 295 <button class="btn btn-navy" onclick="fermerModalFormation()">Fermer</button> 296 </div> 297 </div> 298</div> 299 300<!-- MODAL MODIFIER APPRENANT --> 301<div class="modal-edit" id="modal-edit-overlay" style="display:none;" onclick="fermerModalEdit(event)"> 302 <div class="modal-edit-inner" onclick="event.stopPropagation()"> 303 <h3>â Modifier l'apprenant</h3> 304 <p class="sub" id="me-titre">Modifiez les informations de l'apprenant.</p> 305 <div class="form-grid"> 306 <div class="form-group"><label>Prénom</label><input type="text" id="me-prenom" /></div> 307 <div class="form-group"><label>Nom</label><input type="text" id="me-nom" /></div> 308 <div class="form-group"><label>Email</label><input type="email" id="me-email" /></div> 309 <div class="form-group"><label>Adresse</label><input type="text" id="me-adresse" /></div> 310 <div class="form-group full"> 311 <label>Formation</label> 312 <select id="me-formation"></select> 313 </div> 314 </div> 315 <div style="display:flex;gap:9px;justify-content:flex-end;margin-top:20px;"> 316 <button class="btn btn-outline" onclick="fermerModalEdit()">Annuler</button> 317 <button class="btn btn-navy" onclick="sauvegarderApprenant()">ð¾ Enregistrer</button> 318 </div> 319 </div> 320</div> 321 322<!-- MODAL AJOUT DOCUMENTS --> 323<div class="modal-overlay" id="modal-overlay" style="display:none;" onclick="fermerModal(event)"> 324 <div class="modal" onclick="event.stopPropagation()"> 325 <h3>â Ajouter des documents</h3> 326 <p id="modal-apprenant-name">Sélectionnez les documents à générer :</p> 327 <div class="modal-docs" id="modal-docs-grid"></div> 328 <div class="modal-actions"> 329 <button class="btn btn-outline" onclick="fermerModal()">Annuler</button> 330 <button class="btn btn-navy" onclick="ajouterNouveauxDocs()">Générer & ajouter</button> 331 </div> 332 </div> 333</div> 334 335<div class="notif" id="notif"></div> 336
337<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
vendor: 1 bytes, line 337
337
338<script> 339const { jsPDF } = window.jspdf; 340 341// ââ AIRTABLE ââ 342// Les échanges passent par les fonctions Netlify /api/connexion (mot de passe) et 343// /api/admin (relais vers Airtable, qui détient le token) : aucune clé dans cette page. 344const API_CONNEXION = '/api/connexion'; 345const API_ADMIN = '/api/admin'; 346const CLE_SESSION = 'endo-session'; 347 348function lireSession() { 349 try { return sessionStorage.getItem(CLE_SESSION) || ''; } catch(e) { return ''; } 350} 351function ecrireSession(valeur) { 352 try { 353 if (valeur) sessionStorage.setItem(CLE_SESSION, valeur); 354 else sessionStorage.removeItem(CLE_SESSION); 355 } catch(e) {} 356} 357 358async function appelAdmin(requete) { 359 const r = await fetch(API_ADMIN, { 360 method: 'POST', 361 headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${lireSession()}` }, 362 body: JSON.stringify(requete), 363 }); 364 if (r.status === 401) { 365 afficherConnexion('Session expirée : reconnectez-vous.'); 366 throw new Error('Session expirée : reconnectez-vous.'); 367 } 368 if (!r.ok) throw new Error(await r.text()); 369 return r.json(); 370} 371async function atGet(table, qs='') { return appelAdmin({ methode:'GET', table, qs }); } 372async function atCreate(table, fields) { return appelAdmin({ methode:'POST', table, fields }); } 373async function atPatch(table, id, fields) { return appelAdmin({ methode:'PATCH', table, id, fields }); } 374async function atDelete(table, id) { return appelAdmin({ methode:'DELETE', table, id }); } 375 376// Date du jour au format AAAA-MM-JJ pour les champs Date d'Airtable : 377// le format français JJ/MM/AAAA y est refusé (« Cannot parse date value »). 378// Calculée en heure locale : toISOString() donnerait la veille entre minuit et 2 h. 379function dateAirtable(d = new Date()) { 380 const p = n => String(n).padStart(2, '0'); 381 return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; 382} 383 384// Journal de la table Documents. Il passe APRÃS l'enregistrement des PDF dans la 385// fiche apprenant : s'il échoue, on renvoie le message d'erreur sans tout bloquer. 386async function journaliserDocs(apprenantNom, types, formationTitre) { 387 try { 388 for (const d of types) { 389 await atCreate('Documents', { 390 Apprenant: apprenantNom, 391 Type: d, 392 Formation: formationTitre, 393 'Généré le': dateAirtable(), 394 Statut: 'Généré', 395 }); 396 } 397 return ''; 398 } catch (e) { 399 console.error('Journal Documents :', e); 400 return e.message; 401 } 402} 403 404// ââ STATE ââ 405let formations = []; 406let apprenants = []; 407let selectedDocs = ['convocation','convention','facture','attestation','certificat']; 408let currentFormationId = null; 409 410// ââ TABS ââ 411function switchTab(name) { 412 document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); 413 document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); 414 document.getElementById(`tab-${name}`).classList.add('active'); 415 const idx = {formations:0,apprenants:1,nouvelle:2}[name]; 416 document.querySelectorAll('.tab')[idx].classList.add('active'); 417 if (name === 'apprenants') loadApprenants(); 418} 419 420// ââ FORMATIONS ââ 421async function loadFormations() { 422 try { 423 const data = await atGet('Formations', '?sort[0][field]=Titre&sort[0][direction]=asc'); 424 formations = data.records || []; 425 renderFormations(); 426 updateStats(); 427 updateFormationSelects(); 428 } catch(e) { 429 showNotif('â Erreur chargement formations: ' + e.message); 430 } 431} 432 433function renderFormations() { 434 const el = document.getElementById('formations-list'); 435 if (!formations.length) { 436 el.innerHTML = `<div class="empty-state"><div class="icon">ð</div><p>Aucune formation créée.<br>Cliquez sur "+ Nouvelle formation" pour commencer.</p></div>`; 437 return; 438 } 439 const statusColor = { 'à venir':'dot-blue', 'En cours':'dot-green', 'Terminée':'dot-gray' }; 440 el.innerHTML = formations.map(f => { 441 const ff = f.fields; 442 const type = (ff.Type||'Présentiel').toLowerCase(); 443 return ` 444 <div class="formation-card ${currentFormationId===f.id?'selected':''}" onclick="selectFormation('${f.id}')"> 445 <div class="status-dot ${statusColor[ff.Statut]||'dot-gray'}"></div> 446 <div style="flex:1;"> 447 <div class="f-title">${ff.Titre||'Sans titre'}</div> 448 <div class="f-meta">${ff['Date(s)']||''} ${ff.Lieu ? '· '+ff.Lieu : ''}</div> 449 </div>
450 <span class="f-badge ${type}">${ff.Type||'Présentiel'}</span> 451 <div class="f-actions"> 452 <button class="btn btn-blue btn-sm" onclick="event.stopPropagation();selectFormation('${f.id}');switchTab('apprenants')">ð¥ Apprenants</button> 453 <button class="btn btn-outline btn-sm" onclick="event.stopPropagation();ouvrirModalFormationDocs('${f.id}')">ð Documents</button> 454 <button class="btn btn-danger btn-sm" onclick="event.stopPropagation();supprimerFormation('${f.id}')">â</button> 455 </div> 456 </div>`; 457 }).join(''); 458} 459 460function selectFormation(id) { 461 currentFormationId = id; 462 const f = formations.find(x => x.id === id); 463 if (f) document.getElementById('ap-formation-title').textContent = f.fields.Titre || ''; 464 renderFormations(); 465 document.getElementById('ap-filter').value = id; 466 loadApprenants(); 467 switchTab('apprenants'); 468} 469 470async function creerFormation() { 471 const titre = document.getElementById('f-titre').value.trim(); 472 if (!titre) { showNotif('â Titre obligatoire'); return; } 473 try { 474 showNotif('â³ Création en coursâ¦'); 475 await atCreate('Formations', { 476 Titre: titre, 477 Référence: document.getElementById('f-ref').value, 478 Type: document.getElementById('f-type').value, 479 'Date(s)': document.getElementById('f-dates').value, 480 Lieu: document.getElementById('f-lieu').value, 481 'Formateur(s)': document.getElementById('f-formateur').value, 482 'Montant TTC': parseFloat(document.getElementById('f-montant').value)||0, 483 Statut: document.getElementById('f-statut').value, 484 Durée: document.getElementById('f-duree').value, 485 Organisme: document.getElementById('f-organisme').value, 486 SIRET: document.getElementById('f-siret').value, 487 Signataire: document.getElementById('f-signataire').value, 488 Ville: document.getElementById('f-ville').value, 489 'Adresse organisme': document.getElementById('f-adresse-org').value, 490 NDA: document.getElementById('f-nda').value, 491 }); 492 await loadFormations(); 493 resetFormation(); 494 switchTab('formations'); 495 showNotif('â Formation créée avec succès'); 496 } catch(e) { showNotif('â Erreur: ' + e.message); } 497} 498 499async function supprimerFormation(id) { 500 if (!confirm('Supprimer cette formation ? Les apprenants liés ne seront pas supprimés.')) return; 501 try { 502 await atDelete('Formations', id); 503 if (currentFormationId === id) currentFormationId = null; 504 await loadFormations(); 505 showNotif('â Formation supprimée'); 506 } catch(e) { showNotif('â Erreur: ' + e.message); } 507} 508 509function resetFormation() { 510 ['f-titre','f-ref','f-dates','f-lieu','f-formateur','f-montant'].forEach(id => { 511 const el = document.getElementById(id); 512 if (el) el.value = id === 'f-organisme' ? 'Endolight Formation' : ''; 513 }); 514} 515 516function updateFormationSelects() { 517 const sel = document.getElementById('a-formation'); 518 const filter = document.getElementById('ap-filter'); 519 const opts = formations.map(f => `<option value="${f.id}">${f.fields.Titre||'Sans titre'}</option>`).join(''); 520 sel.innerHTML = '<option value="">â Choisir une formation â</option>' + opts; 521 filter.innerHTML = '<option value="">Toutes les formations</option>' + opts; 522 if (currentFormationId) { 523 sel.value = currentFormationId; 524 filter.value = currentFormationId; 525 } 526} 527 528// ââ APPRENANTS ââ 529async function loadApprenants() { 530 const filterId = document.getElementById('ap-filter').value || currentFormationId; 531 try { 532 let qs = '?sort[0][field]=Nom'; 533 if (filterId) { 534 const f = formations.find(x => x.id === filterId); 535 if (f) { 536 document.getElementById('ap-formation-title').textContent = f.fields.Titre || ''; 537 qs += `&filterByFormula=FIND("${filterId}",{Formation ID})`; 538 } 539 } 540 const data = await atGet('Apprenants', qs); 541 apprenants = data.records || []; 542 renderApprenants(); 543 updateStats(); 544 } catch(e) { 545 showNotif('â Erreur chargement apprenants: ' + e.message); 546 } 547} 548 549function renderApprenants() { 550 const el = document.getElementById('apprenants-list'); 551 if (!apprenants.length) { 552 el.innerHTML = `<div class="empty-state"><div class="icon">ð¤</div><p>Aucun apprenant pour cette formation.</p></div>`; 553 return; 554 } 555 const docLabels = {convocation:'Convocation',convention:'Convention',facture:'Facture',attestation:'Attestation',certificat:'Certificat',inscription:"Attestation d'inscription"};
556 el.innerHTML = apprenants.map((a,i) => { 557 const af = a.fields; 558 const docs = (af.Documents||'').split(',').filter(Boolean); 559 const link = af['Lien magique'] || ''; 560 return ` 561 <div class="apprenant-row"> 562 <div style="flex:1;"> 563 <div class="ap-name">Dr ${af.Prénom||''} ${af.Nom||''}</div> 564 <div class="ap-meta">${af.Email||'Pas d\'email'}</div> 565 <div class="doc-tags">${docs.map(d=>`<span class="doc-tag">${docLabels[d.trim()]||d}</span>`).join('')}</div> 566 ${link ? `<div class="link-box"><input type="text" value="${link}" readonly id="link-${i}"/><button class="copy-btn" id="copy-${i}" onclick="copyLink(${i},'${link}')">Copier</button></div>` : ''} 567 </div> 568 <div class="ap-actions"> 569 <button class="btn btn-blue btn-sm" onclick="telechargerDocs('${a.id}')">⬠Docs</button> 570 <button class="btn btn-outline btn-sm" onclick="ouvrirModalDocs('${a.id}')">+ Docs</button> 571 <button class="btn btn-outline btn-sm" onclick="ouvrirModalEditApprenant('${a.id}')">â</button> 572 <button class="btn btn-danger btn-sm" onclick="supprimerApprenant('${a.id}')">â</button> 573 </div> 574 </div>`; 575 }).join(''); 576} 577 578function toggleDoc(el, doc) { 579 el.classList.toggle('active'); 580 if (selectedDocs.includes(doc)) selectedDocs = selectedDocs.filter(d => d !== doc); 581 else selectedDocs.push(doc); 582} 583 584async function ajouterApprenant() { 585 const prenom = document.getElementById('a-prenom').value.trim(); 586 const nom = document.getElementById('a-nom').value.trim(); 587 const formId = document.getElementById('a-formation').value; 588 if (!prenom || !nom) { showNotif('â Prénom et nom obligatoires'); return; } 589 if (!formId) { showNotif('â Sélectionnez une formation'); return; } 590 591 const formation = formations.find(f => f.id === formId); 592 if (!formation) { showNotif('â Formation introuvable'); return; } 593 594 const id = Math.random().toString(36).substr(2,9).toUpperCase(); 595 const magicLink = `https://endolight-formation.netlify.app/endolight-apprenant.html?id=${id}`; 596 597 try { 598 showNotif('â³ Enregistrementâ¦'); 599 const apprenant = { 600 prenom, nom, 601 email: document.getElementById('a-email').value.trim(), 602 adresse: document.getElementById('a-adresse').value.trim(), 603 }; 604 605 // Générer les PDFs en base64 606 const ff = formation.fields; 607 const docsData = {}; 608 selectedDocs.forEach(d => { 609 const pdf = generatePDF(d, apprenant, ff); 610 docsData[d] = pdf.output('datauristring'); 611 }); 612 613 await atCreate('Apprenants', { 614 Prénom: prenom, 615 Nom: nom, 616 Email: apprenant.email, 617 Adresse: apprenant.adresse, 618 'Formation ID': formId, 619 'Formation': formation.fields.Titre, 620 Documents: selectedDocs.join(','), 621 'ID unique': id, 622 'Lien magique': magicLink, 623 'Docs JSON': JSON.stringify(docsData), 624 }); 625 626 // Log dans Documents (l'apprenant est déjà créé : un échec ici ne doit pas faire croire le contraire) 627 const erreurJournal = await journaliserDocs(`${prenom} ${nom}`, selectedDocs, formation.fields.Titre); 628 629 await loadApprenants(); 630 resetApprenant(); 631 showNotif(erreurJournal 632 ? `â ${prenom} ${nom} ajouté(e), mais journal Documents non mis à jour : ${erreurJournal}` 633 : `â ${prenom} ${nom} ajouté(e) !`); 634 } catch(e) { showNotif('â Erreur: ' + e.message); } 635} 636 637async function telechargerDocs(id) { 638 const a = apprenants.find(x => x.id === id); 639 if (!a) return; 640 try { 641 const docsJson = a.fields['Docs JSON']; 642 if (!docsJson) { showNotif('â Aucun document stocké'); return; } 643 const docsData = JSON.parse(docsJson); 644 Object.entries(docsData).forEach(([type, uri]) => { 645 const link = document.createElement('a'); 646 link.href = uri; 647 link.download = `${type}-${a.fields.Prénom}-${a.fields.Nom}.pdf`; 648 link.click(); 649 }); 650 } catch(e) { showNotif('â Erreur téléchargement'); } 651} 652 653async function supprimerApprenant(id) { 654 if (!confirm('Supprimer cet apprenant ?')) return; 655 try { 656 await atDelete('Apprenants', id); 657 await loadApprenants();
658 showNotif('â Apprenant supprimé'); 659 } catch(e) { showNotif('â Erreur: ' + e.message); } 660} 661 662function resetApprenant() { 663 ['a-prenom','a-nom','a-email','a-rpps','a-adresse'].forEach(id => { 664 const el = document.getElementById(id); 665 if (el) el.value = ''; 666 }); 667} 668 669function copyLink(i, link) { 670 navigator.clipboard.writeText(link).then(() => { 671 const btn = document.getElementById(`copy-${i}`); 672 btn.textContent = 'â Copié !'; 673 btn.classList.add('copied'); 674 setTimeout(() => { btn.textContent = 'Copier'; btn.classList.remove('copied'); }, 2000); 675 }); 676} 677 678// ââ STATS ââ 679async function updateStats() { 680 document.getElementById('stat-formations').textContent = formations.length; 681 document.getElementById('stat-apprenants').textContent = apprenants.length; 682 document.getElementById('stat-presentiel').textContent = formations.filter(f => f.fields.Type === 'Présentiel').length; 683 document.getElementById('stat-distanciel').textContent = formations.filter(f => f.fields.Type === 'Distanciel').length; 684 try { 685 const docs = await atGet('Documents', ''); 686 document.getElementById('stat-docs').textContent = (docs.records||[]).length; 687 } catch(e) {} 688} 689 690// ââ PDF GENERATORS ââ 691 692 693function pdfLine(doc,y){doc.setDrawColor(208,221,232);doc.line(14,y,196,y);} 694 695function generatePDF(type, apprenant, ff) { 696 const formation = { 697 titre: ff.Titre||'', dates: ff['Date(s)']||'', lieu: ff.Lieu||'', 698 formateur: ff['Formateur(s)']||'', montant: String(ff['Montant TTC']||0), 699 organisme: ff.Organisme||'Endolight Formation', num: ff.Référence||'' 700 }; 701 switch(type) { 702 case 'convocation': return makeConvocation(apprenant, formation); 703 case 'convention': return makeConvention(apprenant, formation); 704 case 'facture': return makeFacture(apprenant, formation); 705 case 'attestation': return makeAttestation(apprenant, formation); 706 case 'certificat': return makeCertificat(apprenant, formation); 707 case 'inscription': return makeInscription(apprenant, formation); 708 } 709} 710 711 712function pdfInit() { 713 const doc = new jsPDF({ unit: 'mm', format: 'a4' }); 714 return doc; 715} 716 717function pdfHeader(doc, titre) { 718 const pageW = 210; 719 doc.setFillColor(26,53,96); 720 doc.rect(0, 0, pageW, 36, 'F'); 721 // Logo texte 722 doc.setFont('helvetica','bold'); doc.setFontSize(16); doc.setTextColor(255,255,255); 723 doc.text('ENDOLIGHT', pageW/2, 22, {align:'center'}); 724 // Colorier LIGHT en bleu par-dessus 725 var eW = doc.getTextWidth('ENDO'); 726 var fullW = doc.getTextWidth('ENDOLIGHT'); 727 var sx = pageW/2 - fullW/2 + eW; 728 doc.setTextColor(46,168,213); 729 doc.text('LIGHT', sx, 22); 730 doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(168,216,238); doc.setCharSpace(3); 731 doc.text('FORMATION', pageW/2, 30, {align:'center'}); 732 doc.setCharSpace(0); 733 doc.setFillColor(46,168,213); 734 doc.rect(0, 36, pageW, 2, 'F'); 735 doc.setFont('helvetica','bold'); 736 doc.setFontSize(16); 737 doc.setTextColor(26,53,96); 738 doc.text(titre, pageW/2, 52, {align:'center'}); 739 doc.setFillColor(46,168,213); 740 doc.rect(pageW/2 - 20, 55, 40, 1.2, 'F'); 741} 742 743function pdfFooter(doc, adOrg) { 744 const pageW = 210, pageH = 297; 745 doc.setFillColor(26,53,96); 746 doc.rect(0, pageH-14, pageW, 14, 'F'); 747 doc.setFont('helvetica','normal'); 748 doc.setFontSize(7); 749 doc.setTextColor(168,216,238); 750 doc.text('ENDOLIGHT FORMATION â ' + (adOrg||'4 rue des bouvières 74940 ANNECY') + ' â [email protected]', pageW/2, pageH-6, {align:'center'}); 751} 752 753function pdfVilleDate(doc, ville) { 754 const pageW = 210; 755 const dateStr = new Date().toLocaleDateString('fr-FR', {day:'2-digit', month:'long', year:'numeric'}); 756 doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.setTextColor(100,100,100); 757 doc.text(ville||'Annecy', 20, 44); 758 doc.text(dateStr, pageW-20, 44, {align:'right'}); 759} 760 761 762function pdfInfoBloc(doc, rows, y) { 763 doc.setFillColor(228,244,251); 764 doc.roundedRect(16, y, 178, rows.length * 14 + 8, 3, 3, 'F'); 765 doc.setDrawColor(168,216,238); doc.setLineWidth(0.3); 766 doc.roundedRect(16, y, 178, rows.length * 14 + 8, 3, 3, 'S'); 767 let ry = y + 10; 768 rows.forEach(function([lbl, val]) { 769 doc.setFont('helvetica','bold'); doc.setFontSize(7); doc.setTextColor(46,168,213); 770 doc.text(lbl.toUpperCase(), 22, ry); 771 doc.setFont('helvetica','normal'); doc.setFontSize(9.5); doc.setTextColor(26,43,60); 772 doc.text(val||'â', 65, ry, {maxWidth:122}); 773 ry += 14; 774 }); 775 return ry + 4; 776} 777 778// Fonction utilitaire commune header/footer 779function addSignature(doc, x, y, w, h) { 780 doc.setFont('times', 'bolditalic'); 781 doc.setFontSize(11); 782 doc.setTextColor(26, 53, 96); 783 doc.text('Gregory Fejoz', x + 2, y + 9); 784 doc.setDrawColor(26, 53, 96); 785 doc.setLineWidth(0.4); 786 doc.line(x, y + 12, x + (w||70), y + 12); 787 doc.setFont('helvetica', 'normal'); 788 doc.setFontSize(10); 789 doc.setTextColor(26, 43, 60); 790 doc.setLineWidth(0.4); 791} 792 793function pdfHeader(doc, title) { 794 const pageW = doc.internal.pageSize.width; 795 // Header marine 796 doc.setFillColor(26,53,96); 797 doc.rect(0, 0, pageW, 38, 'F'); 798 // Logo texte centré 799 doc.setFont('helvetica','bold'); doc.setFontSize(18); doc.setTextColor(255,255,255); 800 doc.text('ENDO', pageW/2 - 1, 22, {align:'right'}); 801 doc.setTextColor(46,168,213); 802 doc.text('LIGHT', pageW/2 + 1, 22, {align:'left'}); 803 doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(168,216,238); doc.setCharSpace(3); 804 doc.text('FORMATION', pageW/2, 30, {align:'center'}); 805 doc.setCharSpace(0); 806 // Ligne bleue 807 doc.setFillColor(46,168,213); doc.rect(0, 38, pageW, 2.5, 'F'); 808 // Titre document 809 if (title) { 810 doc.setFont('helvetica','bold'); doc.setFontSize(20); doc.setTextColor(26,53,96); 811 doc.text(title, pageW/2, 70, {align:'center'}); 812 doc.setFillColor(46,168,213); doc.rect(pageW/2-30, 75, 60, 2, 'F'); 813 } 814} 815 816function pdfFooter(doc, adOrg) { 817 const pageW = doc.internal.pageSize.width; 818 const pageH = doc.internal.pageSize.height; 819 doc.setFillColor(26,53,96); doc.rect(0, pageH-18, pageW, 18, 'F'); 820 doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(168,216,238); 821 doc.text('ENDOLIGHT FORMATION â ' + (adOrg||'4 rue des bouvières 74940 ANNECY') + ' â [email protected]', pageW/2, pageH-8, {align:'center'}); 822} 823 824function pdfVilleDate(doc, ville, dateStr) { 825 const pageW = doc.internal.pageSize.width; 826 doc.setFont('helvetica','normal'); doc.setFontSize(9.5); doc.setTextColor(100,100,100); 827 doc.text(ville||'Annecy', 20, 52); 828 doc.text(dateStr, pageW-20, 52, {align:'right'}); 829} 830 831 832function pdfInfoBloc(doc, rows, y) { 833 const pageW = doc.internal.pageSize.width; 834 const h = rows.length * 16 + 12; 835 doc.setFillColor(228,244,251); doc.roundedRect(16, y, pageW-32, h, 4, 4, 'F'); 836 doc.setDrawColor(168,216,238); doc.setLineWidth(0.4); doc.roundedRect(16, y, pageW-32, h, 4, 4, 'S'); 837 let ry = y + 12; 838 rows.forEach(function([lbl, val], i) { 839 if (i > 0) { doc.setDrawColor(200,232,244); doc.setLineWidth(0.3); doc.line(24, ry-4, pageW-24, ry-4); } 840 doc.setFont('helvetica','bold'); doc.setFontSize(8); doc.setTextColor(46,168,213); 841 doc.text(lbl.toUpperCase(), 24, ry); 842 doc.setFont('helvetica','normal'); doc.setFontSize(10); doc.setTextColor(26,43,60); 843 doc.text(String(val||'â'), 95, ry, {maxWidth: pageW-115}); 844 ry += 16; 845 }); 846 return y + h + 6; 847} 848 849function makeConvocation(a, f) { 850 const doc = new jsPDF(); 851 const pageW = doc.internal.pageSize.width; 852 const adOrg = f.adresseOrg || '4 rue des bouvières 74940 ANNECY';
853 const ville = f.ville || 'Annecy'; 854 const dateStr = new Date().toLocaleDateString('fr-FR', {day:'2-digit', month:'long', year:'numeric'}); 855 856 pdfHeader(doc, 'CONVOCATION'); 857 pdfVilleDate(doc, ville, dateStr); 858 859 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 860 doc.text('Madame, Monsieur,', 20, 90); 861 doc.setFont('helvetica','bold'); doc.setFontSize(13); doc.setTextColor(26,53,96); 862 doc.text('Dr ' + a.nom + ' ' + a.prenom, 20, 100); 863 doc.setFont('helvetica','normal'); doc.setFontSize(10.5); doc.setTextColor(60,60,60); 864 doc.text("Nous avons le plaisir de vous convoquer à la formation dont les détails figurent ci-dessous.", 20, 114, {maxWidth:170}); 865 866 pdfInfoBloc(doc, [ 867 ['Formation', f.titre||''], 868 ['Date(s)', f.dates||''], 869 ['Lieu', f.lieu||''], 870 ['Formateur', f.formateur||''], 871 ], 124); 872 873 doc.setFont('helvetica','italic'); doc.setFontSize(9); doc.setTextColor(140,140,140); 874 doc.text('Merci de vous présenter 15 minutes avant le début de la séance avec ce document.', 20, 222, {maxWidth:170}); 875 876 pdfFooter(doc, adOrg); 877 return doc; 878} 879 880function makeConvention(a, f) { 881 const doc = new jsPDF(); 882 const pageW = doc.internal.pageSize.width; 883 const pageH = doc.internal.pageSize.height; 884 const adOrg = f.adresseOrg || '9bis chemin des chapelaines 74940 ANNECY LE VIEUX'; 885 const siret = f.siret || '98532401100016'; 886 const nda = f.nda || '84740489274'; 887 const ville = f.ville || 'Annecy le Vieux'; 888 const sign = f.signataire || 'Gregory Fejoz'; 889 const dateStr = new Date().toLocaleDateString('fr-FR'); 890 891 function pageFooter() { 892 doc.setFillColor(245,246,248); doc.rect(0, pageH-22, pageW, 22, 'F'); 893 doc.setFont('helvetica','bold'); doc.setFontSize(8); doc.setTextColor(26,53,96); 894 doc.text('ENDOLIGHT FORMATION', pageW/2, pageH-14, {align:'center'}); 895 doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(100,100,100); 896 doc.text(adOrg, pageW/2, pageH-9, {align:'center'}); 897 doc.text("N° SIRET : " + siret + " N° D'Activité : " + nda, pageW/2, pageH-4, {align:'center'}); 898 } 899 900 // ââ PAGE 1 : Page de garde ââ 901 doc.setFillColor(26,53,96); doc.rect(0, 0, pageW, pageH, 'F'); 902 // ENDOLIGHT FORMATION 903 doc.setFont('helvetica','bold'); doc.setFontSize(42); doc.setTextColor(46,168,213); 904 doc.text('ENDOLIGHT', pageW/2, 115, {align:'center'}); 905 doc.setTextColor(255,255,255); 906 doc.text('FORMATION', pageW/2, 148, {align:'center'}); 907 // Titre formation 908 doc.setFont('helvetica','bold'); doc.setFontSize(11); doc.setTextColor(46,168,213); doc.setCharSpace(1); 909 const titreUp = (f.titre||'').toUpperCase(); 910 const titreLines = doc.splitTextToSize(titreUp, 160); 911 doc.text(titreLines, pageW/2, 168, {align:'center'}); 912 doc.setCharSpace(0); 913 // CONVENTION DE FORMATION 914 doc.setFont('helvetica','bold'); doc.setFontSize(22); doc.setTextColor(46,168,213); 915 doc.text('CONVENTION DE FORMATION', pageW/2, 220, {align:'center'}); 916 // Pied 917 doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(100,130,180); 918 doc.text('ENDOLIGHT FORMATION â ' + adOrg, pageW/2, pageH-10, {align:'center'}); 919 doc.text("N° SIRET : " + siret + " â N° D'Activité : " + nda, pageW/2, pageH-4, {align:'center'}); 920 921 // ââ PAGE 2 ââ 922 doc.addPage(); 923 pdfHeader(doc, null); 924 let y = 50; 925 926 doc.setFont('helvetica','bold'); doc.setFontSize(11); doc.setTextColor(26,53,96); 927 doc.text('CONVENTION DE FORMATION PROFESSIONNELLE', pageW/2, y, {align:'center'}); y+=6; 928 doc.setFont('helvetica','normal'); doc.setFontSize(8.5); doc.setTextColor(80,80,80); 929 doc.text('(ARTICLE L.6353-1 ET ARTICLE D6353-1 DU CODE DU TRAVAIL)', pageW/2, y, {align:'center'}); y+=12; 930 931 doc.setFont('helvetica','bold'); doc.setFontSize(10); doc.setTextColor(26,53,96); 932 doc.text('CONVENTION N°1', 16, y); y+=10; 933 934 doc.setFont('helvetica','normal'); doc.setFontSize(9.5); doc.setTextColor(40,40,40); 935 doc.text('Entre les soussigné(e)s :', 16, y); y+=8; 936 doc.text('1. Dr ' + a.nom + ' ' + a.prenom + ', situé ' + (a.adresse||''), 16, y, {maxWidth:178}); y+=7; 937 doc.text('Ci-après dénommé(e) le Bénéficiaire,', 16, y); y+=10; 938 939 const lignes = [ 940 "2. ENDOLIGHT FORMATION (en cours d'enregistrement auprès du préfet de Haute Savoie),", 941 "Organisme Prestataire d'Actions concourant au développement des Compétences (OPAC),", 942 "situé au " + adOrg + "," 943 ];
944 lignes.forEach(function(l) { doc.text(l, 16, y, {maxWidth:178}); y+=6; }); 945 doc.text("Ci-après dénommé(e) l'OPAC, ou l'Organisme de formation,", 16, y); y+=8; 946 const intro = "est conclue la convention suivante, en application des dispositions du Livre III de la Sixième partie du Code du travail portant organisation de la formation professionnelle continue."; 947 const introLines = doc.splitTextToSize(intro, 178); 948 doc.text(introLines, 16, y); y += introLines.length * 5.5 + 10; 949 950 // Art 1 951 doc.setDrawColor(208,221,232); doc.setLineWidth(0.3); doc.line(16, y, 194, y); y+=6; 952 doc.setFont('helvetica','bold'); doc.setFontSize(10); doc.setTextColor(26,53,96); 953 doc.text('Article 1 : Objet de la convention', 16, y); y+=2; 954 doc.setDrawColor(46,168,213); doc.setLineWidth(0.5); doc.line(16, y, 101, y); y+=6; 955 956 doc.setFont('helvetica','normal'); doc.setFontSize(9.5); doc.setTextColor(40,40,40); 957 doc.text("L'OPAC ENDOLIGHT FORMATION organisera l'action de formation suivante :", 16, y, {maxWidth:178}); y+=8; 958 doc.setFont('helvetica','bold'); doc.text('- Intitulé de l\'action de formation :', 16, y); y+=5; 959 doc.setFont('helvetica','normal'); doc.text(f.titre||'', 20, y, {maxWidth:174}); y+=8; 960 961 doc.setFont('helvetica','bold'); doc.text('- Objectifs généraux :', 16, y); y+=5; 962 doc.setFont('helvetica','normal'); 963 const obj1 = "Diagnostic de l'état pulpaire et choix de la thérapie de maintien de la vitalité pulpaire adaptée à la situation clinique sur dents matures permanentes."; 964 const obj2 = "Obtenir les conditions optimales de réalisation de la pulpotomie partielle et totale, et connaître les facteurs influençant leur succès."; 965 const obj3 = "Gestion clinique des différentes techniques de préservation pulpaire. Choix du biomatériau et de la restauration coronaire associée. Suivi et gestion des échecs."; 966 [obj1, obj2, obj3].forEach(function(o) { 967 const ls = doc.splitTextToSize(o, 174); 968 doc.text(ls, 20, y); y += ls.length * 5.2 + 3; 969 }); 970 971 doc.setFont('helvetica','bold'); doc.text("- Catégorie d'action concourant au développement des compétences (article L.6313-1) :", 16, y, {maxWidth:178}); y+=8; 972 doc.setFont('helvetica','normal'); 973 const cat = "Cette action est une action de formation au regard de la définition donnée par le code du travail (article L.6313-2) : \u00ab L'action de formation mentionn\u00e9e au 1\u00b0 de l'article L. 6313-1 se d\u00e9finit comme un parcours p\u00e9dagogique permettant d'atteindre un objectif professionnel. \u00bb La pr\u00e9sente action vise \u00e0 \u00ab favoriser l'adaptation des travailleurs \u00e0 leur poste de travail, \u00e0 l'\u00e9volution des emplois ainsi que leur maintien dans l'emploi et de participer au d\u00e9veloppement de leurs comp\u00e9tences en lien ou non avec leur poste de travail. \u00bb (article L.6313-3 du Code du travail)."; 974 const catLines = doc.splitTextToSize(cat, 178); 975 doc.text(catLines, 16, y); y += catLines.length * 5 + 4; 976 977 pageFooter(); 978 979 // ââ PAGE 3 ââ 980 doc.addPage(); 981 pdfHeader(doc, null); 982 y = 50; 983 984 const articles = [ 985 { 986 titre: 'Article 2 : Modalités de déroulement (suivi et sanction)', 987 texte: '- Programme et méthodes :\nCf. programme de formation ci joint.' 988 }, 989 { 990 titre: 'Article 3 : Dates et lieu', 991 texte: '- Durée : ' + (f.dates||'') + '\n- Lieu : ' + (f.lieu||'') 992 }, 993 { 994 titre: 'Article 3 : Formateurs et Effectif formé', 995 texte: "L'action de formation sera dispensée par : le DR " + sign 996 }, 997 { 998 titre: 'Article 4 : Dispositions financières', 999 texte: "En contrepartie de cette action de formation, le Bénéficiaire s'acquittera des coûts suivants : " + (f.montant||'') + " euros." 1000 }, 1001 { 1002 titre: 'Article 5 : Modalités de règlement', 1003 texte: "Une facture sera envoyée au bénéficiaire.\n\nLe paiement sera dû au moment de l'inscription." 1004 }, 1005 { 1006 titre: 'Article 6 : Dédit ou abandon', 1007 texte: "Tout module commencé est dû.\n\nEn cas de dédit par le Bénéficiaire à moins de 2 jours francs avant le début de l'action mentionnée à l'article 1, l'organisme remboursera sur le coût total." 1008 }, 1009 { 1010 titre: 'Article 8 : Différends éventuels', 1011 texte: "Dans l'hypothèse où un différend surviendrait portant sur la validité, l'interprétation, l'exécution ou la fin pour quelle que cause que ce soit des présentes dispositions, les parties conviennent de rechercher toute solution amiable.\n\nSi une contestation ou un différend ne peuvent être réglés à l'amiable, le Tribunal de Versailles sera seul compétent pour régler le litige." 1012 } 1013 ]; 1014
1015 articles.forEach(function(art) { 1016 if (y > pageH - 55) { 1017 pageFooter(); 1018 doc.addPage(); 1019 pdfHeader(doc, null); 1020 y = 50; 1021 } 1022 doc.setDrawColor(208,221,232); doc.setLineWidth(0.3); doc.line(16, y, 194, y); y+=6; 1023 doc.setFont('helvetica','bold'); doc.setFontSize(10); doc.setTextColor(26,53,96); 1024 doc.text(art.titre, 16, y); y+=2; 1025 doc.setDrawColor(46,168,213); doc.setLineWidth(0.5); 1026 doc.line(16, y, Math.min(16 + doc.getTextWidth(art.titre), 194), y); y+=6; 1027 doc.setFont('helvetica','normal'); doc.setFontSize(9.5); doc.setTextColor(40,40,40); 1028 const ls = doc.splitTextToSize(art.texte, 178); 1029 doc.text(ls, 16, y); y += ls.length * 5.5 + 10; 1030 }); 1031 1032 // Signatures 1033 y += 4; 1034 doc.setFont('helvetica','normal'); doc.setFontSize(9.5); doc.setTextColor(40,40,40); 1035 doc.text('Fait en double exemplaire,', 16, y); y+=8; 1036 doc.text('A ' + ville + ' â¦â¦â¦â¦â¦â¦â¦â¦â¦â¦â¦â¦â¦.., le ' + dateStr, 16, y); y+=12; 1037 doc.setFont('helvetica','bold'); doc.setFontSize(9.5); doc.setTextColor(26,53,96); 1038 doc.text('Pour Le Bénéficiaire', 46, y, {align:'center'}); 1039 doc.text("Pour l'Organisme de formation", 160, y, {align:'center'}); y+=5; 1040 doc.setDrawColor(180,180,180); doc.setLineWidth(0.3); 1041 doc.rect(16, y, 60, 30); doc.rect(110, y, 80, 30); 1042 addSignature(doc, 112, y+4, 76, 20); 1043 1044 pageFooter(); 1045 return doc; 1046} 1047 1048 1049function makeFacture(a, f) { 1050 const doc = new jsPDF(); 1051 const pageW = doc.internal.pageSize.width; 1052 const adOrg = f.adresseOrg || '4 rue des bouvières 74940 ANNECY'; 1053 const siret = f.siret || '98532401100016'; 1054 const ville = f.ville || 'Annecy'; 1055 const sign = f.signataire || 'Gregory Fejoz'; 1056 const dateStr = new Date().toLocaleDateString('fr-FR', {day:'2-digit', month:'long', year:'numeric'}); 1057 const factureNum = 'F-' + (f.num||'EL') + '-' + Math.random().toString(36).substr(2,4).toUpperCase(); 1058 1059 pdfHeader(doc, 'FACTURE'); 1060 pdfVilleDate(doc, ville, dateStr); 1061 1062 // Numéro facture 1063 doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.setTextColor(100,100,100); 1064 doc.text('N° ' + factureNum, 20, 88); 1065 1066 // Bloc émetteur / destinataire 1067 doc.setFillColor(228,244,251); doc.roundedRect(16, 96, 82, 38, 3, 3, 'F'); 1068 doc.setFillColor(245,246,248); doc.roundedRect(106, 96, 88, 38, 3, 3, 'F'); 1069 1070 doc.setFont('helvetica','bold'); doc.setFontSize(7.5); doc.setTextColor(46,168,213); 1071 doc.text('ÃMETTEUR', 22, 104); 1072 doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.setTextColor(26,43,60); 1073 doc.text('Endolight Formation', 22, 111); 1074 doc.text(adOrg, 22, 118, {maxWidth:72}); 1075 doc.text('[email protected]', 22, 126); 1076 1077 doc.setFont('helvetica','bold'); doc.setFontSize(7.5); doc.setTextColor(46,168,213); 1078 doc.text('DESTINATAIRE', 112, 104); 1079 doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.setTextColor(26,43,60); 1080 doc.text('Dr ' + a.nom + ' ' + a.prenom, 112, 111); 1081 doc.text(a.adresse||'', 112, 118, {maxWidth:76}); 1082 1083 // Tableau 1084 let y = 152; 1085 doc.setFillColor(26,53,96); doc.rect(16, y, 178, 11, 'F'); 1086 doc.setFont('helvetica','bold'); doc.setFontSize(8.5); doc.setTextColor(255,255,255); 1087 doc.text('DÃSIGNATION', 22, y+7.5); 1088 doc.text('QTÃ', 138, y+7.5, {align:'center'}); 1089 doc.text('P.U. TTC', 165, y+7.5, {align:'center'}); 1090 doc.text('TOTAL TTC', 192, y+7.5, {align:'right'}); 1091 y += 11; 1092 1093 doc.setFillColor(248,250,252); doc.rect(16, y, 178, 13, 'F'); 1094 doc.setFont('helvetica','normal'); doc.setFontSize(9.5); doc.setTextColor(26,43,60); 1095 doc.text(f.titre||'', 22, y+9, {maxWidth:108}); 1096 doc.text('1', 138, y+9, {align:'center'}); 1097 doc.text(String(f.montant||0) + ' â¬', 165, y+9, {align:'center'}); 1098 doc.text(String(f.montant||0) + ' â¬', 192, y+9, {align:'right'}); 1099 y += 18; 1100 1101 doc.setDrawColor(208,221,232); doc.line(16, y, 194, y); y+=8; 1102 const ttc = parseFloat(f.montant)||0; 1103 const ht = ttc / 1.2; 1104 const tva = ttc - ht; 1105 1106 doc.setFont('helvetica','normal'); doc.setFontSize(9.5); 1107 doc.setFillColor(26,53,96); doc.rect(132, y-2, 62, 12, 'F'); 1108 doc.setFont('helvetica','bold'); doc.setTextColor(255,255,255); 1109 doc.text('TOTAL TTC :', 148, y+6.5); doc.text(ttc.toFixed(2) + ' â¬', 192, y+6.5, {align:'right'}); 1110 y += 20; 1111 1112 // Corps texte 1113 doc.setFont('helvetica','normal'); doc.setFontSize(10.5); doc.setTextColor(40,40,40); 1114 const corps = 'Je, soussigné ' + sign + ", représentant d'ENDOLIGHT FORMATION, numéro SIRET " + siret + ', domicilié au ' + adOrg + ','; 1115 doc.text(corps, 20, y+14, {maxWidth:170}); 1116 doc.setFont('helvetica','normal'); doc.text('Atteste que le ', 20, y+28); 1117 const atW = doc.getTextWidth('Atteste que le '); 1118 doc.setFont('helvetica','bold'); doc.setTextColor(26,53,96); 1119 doc.text('Dr ' + a.nom + ' ' + a.prenom, 20 + atW, y+28); 1120 doc.setFont('helvetica','normal'); doc.setTextColor(40,40,40); 1121 doc.text('a réglé sa cotisation de ' + (f.montant||'') + ' euros TTC pour la formation « ' + (f.titre||'') + ' » du ' + (f.dates||''), 20, y+38, {maxWidth:170}); 1122 doc.text('Fait à ' + ville, 20, y+52); 1123 // Cadre signature bleu clair
1124 doc.setFillColor(248,252,255); doc.roundedRect(18, y+56, 68, 16, 2, 2, 'F'); 1125 doc.setDrawColor(168,216,238); doc.setLineWidth(0.4); doc.roundedRect(18, y+56, 68, 16, 2, 2, 'S'); 1126 addSignature(doc, 21, y+58, 62, 14); 1127 1128 pdfFooter(doc, adOrg); 1129 return doc; 1130} 1131 1132function makeAttestation(a, f) { 1133 const doc = new jsPDF(); 1134 const pageW = doc.internal.pageSize.width; 1135 const adOrg = f.adresseOrg || '4 rue des bouvières 74940 ANNECY'; 1136 const ville = f.ville || 'Annecy'; 1137 const sign = f.signataire || 'Gregory Fejoz'; 1138 const dateStr = new Date().toLocaleDateString('fr-FR', {day:'2-digit', month:'long', year:'numeric'}); 1139 1140 pdfHeader(doc, 'ATTESTATION DE PRÃSENCE'); 1141 pdfVilleDate(doc, ville, dateStr); 1142 1143 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1144 doc.text("Je, soussigné(e) " + sign + ", représentant(e) d'ENDOLIGHT FORMATION,", 20, 92, {maxWidth:170}); 1145 doc.text("atteste que le/la stagiaire :", 20, 103); 1146 1147 // Nom encadré 1148 doc.setFillColor(26,53,96); doc.roundedRect(50, 110, 110, 16, 4, 4, 'F'); 1149 doc.setFont('helvetica','bold'); doc.setFontSize(13); doc.setTextColor(255,255,255); 1150 doc.text('Dr ' + a.nom + ' ' + a.prenom, pageW/2, 120.5, {align:'center'}); 1151 1152 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1153 doc.text("a bien participé à la totalité de la formation :", 20, 136); 1154 doc.setFont('helvetica','bold'); doc.setFontSize(12); doc.setTextColor(46,168,213); 1155 doc.text('« ' + (f.titre||'') + ' »', 20, 148, {maxWidth:170}); 1156 1157 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1158 doc.text("qui s'est déroulée le " + (f.dates||'') + " à " + (f.lieu||'') + ".", 20, 160, {maxWidth:170}); 1159 1160 doc.setDrawColor(208,221,232); doc.setLineWidth(0.4); doc.line(20, 176, pageW-20, 176); 1161 1162 doc.text('Fait à ' + ville + ', le ' + dateStr, 20, 190); 1163 doc.setFont('helvetica','bold'); doc.setFontSize(9); doc.setTextColor(26,53,96); 1164 doc.text('Pour le Bénéficiaire', 48, 202, {align:'center'}); 1165 doc.text("Pour l'Organisme", 162, 202, {align:'center'}); 1166 doc.setDrawColor(180,180,180); doc.rect(16, 205, 60, 30); doc.rect(112, 205, 78, 30); 1167 addSignature(doc, 114, 207, 74, 20); 1168 1169 pdfFooter(doc, adOrg); 1170 return doc; 1171} 1172 1173function makeCertificat(a, f) { 1174 const doc = new jsPDF(); 1175 const pageW = doc.internal.pageSize.width; 1176 const pageH = doc.internal.pageSize.height; 1177 const adOrg = f.adresseOrg || '4 rue des bouvières 74940 ANNECY'; 1178 const ville = f.ville || 'Annecy'; 1179 const dateStr = new Date().toLocaleDateString('fr-FR', {day:'2-digit', month:'long', year:'numeric'}); 1180 1181 // Header marine étendu 1182 doc.setFillColor(26,53,96); doc.rect(0, 0, pageW, 38, 'F'); 1183 doc.setFont('helvetica','bold'); doc.setFontSize(18); doc.setTextColor(255,255,255); 1184 doc.text('ENDO', pageW/2 - 2, 22, {align:'right'}); 1185 doc.setTextColor(46,168,213); doc.text('LIGHT', pageW/2 + 2, 22, {align:'left'}); 1186 doc.setFont('helvetica','normal'); doc.setFontSize(7); doc.setTextColor(168,216,238); doc.setCharSpace(3); 1187 doc.text('FORMATION', pageW/2, 30, {align:'center'}); 1188 doc.setCharSpace(0); 1189 doc.setFillColor(46,168,213); doc.rect(0, 38, pageW, 2.5, 'F'); 1190 1191 // Titre centré 1192 doc.setFont('helvetica','bold'); doc.setFontSize(18); doc.setTextColor(26,53,96); 1193 doc.text('CERTIFICAT DE RÃALISATION', pageW/2, 68, {align:'center'}); 1194 doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.setTextColor(120,120,120); 1195 doc.text('de formation professionnelle continue', pageW/2, 77, {align:'center'}); 1196 doc.setFillColor(46,168,213); doc.rect(pageW/2-35, 82, 70, 2, 'F'); 1197 1198 // Destinataire 1199 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1200 doc.text('Ce certificat est remis à ', pageW/2, 98, {align:'center'}); 1201 doc.setFont('helvetica','bold'); doc.setFontSize(18); doc.setTextColor(26,53,96); 1202 doc.text('Dr ' + a.nom + ' ' + a.prenom, pageW/2, 112, {align:'center'}); 1203 doc.setFillColor(46,168,213); doc.rect(pageW/2-35, 116, 70, 1.5, 'F'); 1204 1205 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1206 doc.text('en reconnaissance de sa participation complète à ', pageW/2, 130, {align:'center'}); 1207 doc.setFont('helvetica','bold'); doc.setFontSize(13); doc.setTextColor(46,168,213); 1208 doc.text('« ' + (f.titre||'') + ' »', pageW/2, 142, {align:'center', maxWidth:170}); 1209 1210 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1211 doc.text('organisée par Endolight Formation', pageW/2, 155, {align:'center'}); 1212 doc.text('les ' + (f.dates||'') + ' â ' + (f.lieu||''), pageW/2, 165, {align:'center'}); 1213 1214 // Médaillon décoratif 1215 doc.setFillColor(228,244,251); doc.circle(pageW/2, 200, 22, 'F'); 1216 doc.setDrawColor(46,168,213); doc.setLineWidth(1.5); doc.circle(pageW/2, 200, 22, 'S'); 1217 doc.setFont('helvetica','bold'); doc.setFontSize(8); doc.setTextColor(26,53,96); 1218 doc.text('CERTIFIÃ', pageW/2, 197, {align:'center'}); 1219 doc.text('CONFORME', pageW/2, 204, {align:'center'}); 1220 1221 doc.setDrawColor(208,221,232); doc.setLineWidth(0.4); doc.line(20, 234, pageW-20, 234); 1222 doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.setTextColor(60,60,60); 1223 doc.text('Fait à ' + ville + ', le ' + dateStr, 20, 246); 1224 doc.setFont('helvetica','bold'); doc.setFontSize(9); doc.setTextColor(26,53,96); 1225 doc.text('Signature du responsable :', 20, 258); 1226 doc.setDrawColor(180,180,180); doc.rect(20, 261, 72, 30); 1227 addSignature(doc, 22, 263, 68, 22); 1228 1229 pdfFooter(doc, adOrg); 1230 return doc; 1231} 1232 1233 1234 1235function makeInscription(a, f) { 1236 const doc = new jsPDF(); 1237 const pageW = doc.internal.pageSize.width; 1238 const adOrg = f.adresseOrg || '4 rue des bouvières 74940 ANNECY';
1239 const ville = f.ville || 'Annecy'; 1240 const dateStr = new Date().toLocaleDateString('fr-FR', {day:'2-digit', month:'long', year:'numeric'}); 1241 1242 pdfHeader(doc, "ATTESTATION D'INSCRIPTION"); 1243 pdfVilleDate(doc, ville, dateStr); 1244 1245 let y = 92; 1246 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1247 doc.text("Je soussigné, Directeur d'Endolight Formation, certifie que", 20, y, {maxWidth:170}); y+=18; 1248 1249 // Nom apprenant 1250 doc.setFont('helvetica','bold'); doc.setFontSize(13); doc.setTextColor(26,53,96); 1251 doc.text('Dr ' + a.nom + ' ' + a.prenom, pageW/2, y, {align:'center'}); y+=8; 1252 1253 // Adresse 1254 if (a.adresse) { 1255 doc.setFont('helvetica','bold'); doc.setFontSize(11); doc.setTextColor(26,53,96); 1256 const addrLines = doc.splitTextToSize(a.adresse, 160); 1257 doc.text(addrLines, pageW/2, y, {align:'center'}); y += addrLines.length * 6 + 4; 1258 } 1259 y += 10; 1260 1261 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1262 doc.text("Est inscrit(e) dans notre établissement pour y suivre", pageW/2, y, {align:'center'}); y+=8; 1263 1264 doc.setFont('helvetica','bold'); doc.setFontSize(12); doc.setTextColor(26,53,96); 1265 const titreLines = doc.splitTextToSize(f.titre||'', 160); 1266 doc.text(titreLines, pageW/2, y, {align:'center'}); y += titreLines.length * 7 + 4; 1267 1268 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1269 if (f.duree) { 1270 doc.text('De ' + f.duree + ' en ' + (f.type||'présentiel').toLowerCase(), pageW/2, y, {align:'center'}); y+=8; 1271 } 1272 doc.setFont('helvetica','normal'); doc.text('Le ', pageW/2 - doc.getTextWidth('Le ')/2 - doc.getTextWidth(f.dates||'')/2, y); 1273 doc.setFont('helvetica','bold'); doc.setTextColor(26,53,96); 1274 doc.text(f.dates||'', pageW/2 + doc.getTextWidth('Le ')/2, y, {align:'left'}); y+=18; 1275 1276 doc.setFont('helvetica','normal'); doc.setFontSize(11); doc.setTextColor(60,60,60); 1277 doc.text("En foi de quoi, je lui délivre la présente attestation.", pageW/2, y, {align:'center'}); y+=18; 1278 1279 doc.setFont('helvetica','normal'); doc.text('Fait à ' + ville + ', ', 20, y); 1280 const faitW = doc.getTextWidth('Fait à ' + ville + ', '); 1281 doc.setFont('helvetica','bold'); doc.setTextColor(26,53,96); 1282 doc.text('le ' + dateStr, 20 + faitW, y); y+=16; 1283 addSignature(doc, 20, y, 80, 22); 1284 1285 pdfFooter(doc, adOrg); 1286 return doc; 1287} 1288 1289// ââ UTILS ââ 1290function showNotif(msg) { 1291 const n = document.getElementById('notif'); 1292 n.textContent = msg; n.classList.add('show'); 1293 setTimeout(() => n.classList.remove('show'), 3500); 1294} 1295 1296// ââ MODAL MODIFIER APPRENANT ââ 1297let modalEditApprenantId = null; 1298 1299function ouvrirModalEditApprenant(id) { 1300 const a = apprenants.find(x => x.id === id); 1301 if (!a) return; 1302 modalEditApprenantId = id; 1303 const af = a.fields; 1304 1305 document.getElementById('me-titre').textContent = 1306 `Dr ${af.Prénom||''} ${af.Nom||''}`; 1307 document.getElementById('me-prenom').value = af.Prénom || ''; 1308 document.getElementById('me-nom').value = af.Nom || ''; 1309 document.getElementById('me-email').value = af.Email || ''; 1310 document.getElementById('me-adresse').value = af.Adresse || ''; 1311 1312 // Remplir le select formations 1313 const sel = document.getElementById('me-formation'); 1314 sel.innerHTML = formations.map(f => 1315 `<option value="${f.id}" ${f.id === af['Formation ID'] ? 'selected' : ''}>${f.fields.Titre||'Sans titre'}</option>` 1316 ).join(''); 1317 1318 document.getElementById('modal-edit-overlay').style.display = 'flex'; 1319} 1320 1321async function sauvegarderApprenant() { 1322 const prenom = document.getElementById('me-prenom').value.trim(); 1323 const nom = document.getElementById('me-nom').value.trim(); 1324 if (!prenom || !nom) { showNotif('â Prénom et nom obligatoires'); return; } 1325 1326 const formId = document.getElementById('me-formation').value; 1327 const formation = formations.find(f => f.id === formId); 1328 1329 try { 1330 showNotif('â³ Enregistrementâ¦'); 1331 await atPatch('Apprenants', modalEditApprenantId, { 1332 Prénom: prenom, 1333 Nom: nom, 1334 Email: document.getElementById('me-email').value.trim(), 1335 Adresse: document.getElementById('me-adresse').value.trim(), 1336 'Formation ID': formId, 1337 Formation: formation ? formation.fields.Titre : '', 1338 }); 1339 await loadApprenants(); 1340 fermerModalEdit();
1341 showNotif('â Apprenant mis à jour !'); 1342 } catch(e) { showNotif('â Erreur: ' + e.message); } 1343} 1344 1345function fermerModalEdit(e) { 1346 if (!e || e.target === document.getElementById('modal-edit-overlay')) { 1347 document.getElementById('modal-edit-overlay').style.display = 'none'; 1348 modalEditApprenantId = null; 1349 } 1350} 1351 1352// ââ MODAL DOCUMENTS FORMATION ââ 1353let modalFormationId = null; 1354 1355 1356const DOC_FORMATION_LABELS = { 1357 livret: "ð Livret d'accueil", 1358 reglement: "ð Règlement intérieur", 1359 programme: "ð Programme de formation", 1360 cgv: "ð Conditions générales", 1361 questionnaire: "ð Questionnaire pré-formation", 1362}; 1363 1364function ouvrirModalFormationDocs(id) { 1365 modalFormationId = id; 1366 pendingUploads = {}; 1367 const f = formations.find(x => x.id === id); 1368 if (!f) return; 1369 1370 document.getElementById('mf-titre').textContent = f.fields.Titre || ''; 1371 1372 // Reset inputs 1373 ['livret','reglement','programme','cgv','questionnaire'].forEach(k => { 1374 document.getElementById(`up-${k}`).value = ''; 1375 document.getElementById(`info-${k}`).textContent = 'Aucun fichier sélectionné'; 1376 document.getElementById(`info-${k}`).classList.remove('ok'); 1377 }); 1378 1379 // Afficher docs existants 1380 afficherDocsExistants(f); 1381 document.getElementById('modal-formation-overlay').style.display = 'flex'; 1382} 1383 1384function afficherDocsExistants(f) { 1385 let docsJson = {}; 1386 try { docsJson = JSON.parse(f.fields['Docs JSON Formation'] || '{}'); } catch(e) {} 1387 const keys = Object.keys(docsJson); 1388 const el = document.getElementById('mf-docs-list'); 1389 if (!keys.length) { el.innerHTML = ''; return; } 1390 1391 let html = '<div class="card-title" style="font-size:.75rem;margin-bottom:10px;border:none;padding:0;">Documents déjà uploadés</div>'; 1392 keys.forEach(key => { 1393 const doc = docsJson[key]; 1394 const label = DOC_FORMATION_LABELS[key] || key; 1395 html += '<div class="doc-existant">' 1396 + '<span>' + label + '</span>' 1397 + '<button class="upload-btn" style="padding:4px 10px;font-size:.7rem;" onclick="telechargerDocLocal(\"' + key + '\")">â¬</button>' 1398 + '<button class="del-doc" onclick="supprimerDocFormation(\"' + key + '\")">â</button>' 1399 + '</div>'; 1400 }); 1401 html += '<div style="height:1px;background:var(--border);margin:14px 0;"></div>'; 1402 el.innerHTML = html; 1403} 1404 1405function telechargerDocLocal(key) { 1406 const f = formations.find(x => x.id === modalFormationId); 1407 let docsJson = {}; 1408 try { docsJson = JSON.parse(f.fields['Docs JSON Formation'] || '{}'); } catch(e) {} 1409 const doc = docsJson[key]; 1410 if (!doc) return; 1411 const a = document.createElement('a'); 1412 a.href = doc.data; 1413 a.download = doc.name || key + '.pdf'; 1414 a.click(); 1415} 1416 1417 1418 1419async function sauvegarderDocsFormation() { 1420 if (!Object.keys(pendingUploads).length) { showNotif('â Aucun fichier sélectionné'); return; } 1421 1422 const btn = document.getElementById('btn-save-docs'); 1423 btn.disabled = true; 1424 btn.textContent = 'â³ Envoiâ¦'; 1425 1426 try { 1427 const f = formations.find(x => x.id === modalFormationId); 1428 1429 // Récupérer les docs existants depuis le champ JSON 1430 let docsJson = {}; 1431 try { docsJson = JSON.parse(f.fields['Docs JSON Formation'] || '{}'); } catch(e) {} 1432 1433 // Fusionner avec les nouveaux (écraser si même clé) 1434 Object.entries(pendingUploads).forEach(([key, val]) => { 1435 docsJson[key] = { data: val.data, name: val.name }; 1436 }); 1437 1438 await atPatch('Formations', modalFormationId, { 1439 'Docs JSON Formation': JSON.stringify(docsJson) 1440 }); 1441 1442 await loadFormations(); 1443 const updatedF = formations.find(x => x.id === modalFormationId); 1444 if (updatedF) afficherDocsExistants(updatedF); 1445 1446 pendingUploads = {}; 1447 ['livret','reglement','programme','cgv','questionnaire'].forEach(k => { 1448 document.getElementById('up-' + k).value = ''; 1449 document.getElementById('info-' + k).textContent = 'Aucun fichier sélectionné'; 1450 document.getElementById('info-' + k).classList.remove('ok'); 1451 }); 1452 showNotif('â Documents enregistrés !'); 1453 } catch(e) { 1454 showNotif('â Erreur: ' + e.message); 1455 } finally { 1456 btn.disabled = false;
1457 btn.textContent = 'ð¾ Enregistrer'; 1458 } 1459} 1460 1461 1462 1463async function supprimerDocFormation(key) { 1464 if (!confirm('Supprimer ce document ?')) return; 1465 const f = formations.find(x => x.id === modalFormationId); 1466 let docsJson = {}; 1467 try { docsJson = JSON.parse(f.fields['Docs JSON Formation'] || '{}'); } catch(e) {} 1468 delete docsJson[key]; 1469 try { 1470 await atPatch('Formations', modalFormationId, { 'Docs JSON Formation': JSON.stringify(docsJson) }); 1471 await loadFormations(); 1472 const updatedF = formations.find(x => x.id === modalFormationId); 1473 if (updatedF) afficherDocsExistants(updatedF); 1474 showNotif('â Document supprimé'); 1475 } catch(e) { showNotif('â Erreur: ' + e.message); } 1476} 1477 1478function fermerModalFormation(e) { 1479 if (!e || e.target === document.getElementById('modal-formation-overlay')) { 1480 document.getElementById('modal-formation-overlay').style.display = 'none'; 1481 modalFormationId = null; 1482 pendingUploads = {}; 1483 } 1484} 1485 1486// ââ MODAL AJOUT DOCS ââ 1487const DOC_LABELS = {convocation:'ð Convocation', convention:'ð Convention', facture:'ð§¾ Facture', attestation:'â Attestation', certificat:'ð Certificat', inscription:"ð Attestation d'inscription"}; 1488let modalApprenantId = null; 1489let modalSelectedDocs = []; 1490 1491function ouvrirModalDocs(id) { 1492 const a = apprenants.find(x => x.id === id); 1493 if (!a) return; 1494 modalApprenantId = id; 1495 modalSelectedDocs = []; 1496 1497 // Docs déjà générés 1498 let docsData = {}; 1499 try { docsData = JSON.parse(a.fields['Docs JSON'] || '{}'); } catch(e) {} 1500 const dejaDocs = Object.keys(docsData); 1501 1502 // Afficher uniquement les docs pas encore générés 1503 const tousLesDocs = ['convocation','convention','facture','attestation','certificat']; 1504 const restants = tousLesDocs.filter(d => !dejaDocs.includes(d)); 1505 1506 document.getElementById('modal-apprenant-name').textContent = 1507 `Dr ${a.fields.Prénom||''} ${a.fields.Nom||''} â choisissez les documents à ajouter :`; 1508 1509 const grid = document.getElementById('modal-docs-grid'); 1510 if (restants.length === 0) { 1511 grid.innerHTML = '<p style="color:var(--muted);font-size:.82rem;">Tous les documents ont déjà été générés.</p>'; 1512 } else { 1513 grid.innerHTML = restants.map(d => ` 1514 <label class="doc-check" id="mc-${d}" onclick="toggleModalDoc('${d}',this)"> 1515 ${DOC_LABELS[d]} 1516 </label>`).join(''); 1517 } 1518 1519 document.getElementById('modal-overlay').style.display = 'flex'; 1520} 1521 1522function toggleModalDoc(doc, el) { 1523 el.classList.toggle('active'); 1524 if (modalSelectedDocs.includes(doc)) modalSelectedDocs = modalSelectedDocs.filter(d => d !== doc); 1525 else modalSelectedDocs.push(doc); 1526} 1527 1528function fermerModal(e) { 1529 if (!e || e.target === document.getElementById('modal-overlay')) { 1530 document.getElementById('modal-overlay').style.display = 'none'; 1531 modalApprenantId = null; 1532 modalSelectedDocs = []; 1533 } 1534} 1535 1536async function ajouterNouveauxDocs() { 1537 if (!modalSelectedDocs.length) { showNotif('â Sélectionnez au moins un document'); return; } 1538 const a = apprenants.find(x => x.id === modalApprenantId); 1539 if (!a) return; 1540 1541 const formation = formations.find(f => f.id === a.fields['Formation ID']); 1542 if (!formation) { showNotif('â Formation introuvable'); return; } 1543 1544 showNotif('â³ Génération en coursâ¦'); 1545 1546 // Récupérer les docs existants 1547 let docsData = {}; 1548 try { docsData = JSON.parse(a.fields['Docs JSON'] || '{}'); } catch(e) {} 1549 const docsExistants = (a.fields.Documents || '').split(',').filter(Boolean); 1550 1551 // Générer les nouveaux PDFs 1552 const apprenant = { 1553 prenom: a.fields.Prénom || '', 1554 nom: a.fields.Nom || '', 1555 email: a.fields.Email || '', 1556 adresse:a.fields.Adresse|| '', 1557 }; 1558 const ff = formation.fields; 1559 1560 modalSelectedDocs.forEach(d => { 1561 const pdf = generatePDF(d, apprenant, ff); 1562 docsData[d] = pdf.output('datauristring'); 1563 }); 1564 1565 // Mettre à jour dans Airtable 1566 const tousLesDocs = [...new Set([...docsExistants, ...modalSelectedDocs])]; 1567 try { 1568 await atPatch('Apprenants', a.id, { 1569 Documents: tousLesDocs.join(','), 1570 'Docs JSON': JSON.stringify(docsData), 1571 }); 1572 1573 // Log dans Documents (les PDF sont déjà dans la fiche : un échec ici ne doit pas faire croire le contraire) 1574 const erreurJournal = await journaliserDocs(`${apprenant.prenom} ${apprenant.nom}`, modalSelectedDocs, ff.Titre || ''); 1575 1576 const nbDocs = modalSelectedDocs.length; // à lire avant fermerModal(), qui vide la sélection 1577 await loadApprenants(); 1578 fermerModal(); 1579 showNotif(erreurJournal 1580 ? `â ${nbDocs} document(s) ajouté(s), mais journal Documents non mis à jour : ${erreurJournal}` 1581 : `â ${nbDocs} document(s) ajouté(s) avec succès !`); 1582 } catch(e) { 1583 showNotif('â Erreur: ' + e.message); 1584 } 1585} 1586 1587// ââ AUTHENTIFICATION ââ 1588// Le mot de passe est vérifié par le serveur (variable ADMIN_PASSWORD sur Netlify), 1589// qui renvoie une session signée valable 12 h, gardée le temps de l'onglet. 1590function montrerErreurConnexion(message) { 1591 const err = document.getElementById('login-error'); 1592 err.textContent = message; 1593 err.style.display = 'block'; 1594 setTimeout(() => err.style.display = 'none', 4000); 1595} 1596 1597function afficherConnexion(message) { 1598 ecrireSession(''); 1599 document.getElementById('login-overlay').style.display = ''; 1600 if (message) montrerErreurConnexion(message); 1601} 1602 1603async function verifierMdp() { 1604 const champ = document.getElementById('login-input'); 1605 try { 1606 const r = await fetch(API_CONNEXION, { 1607 method: 'POST', 1608 headers: { 'Content-Type': 'application/json' }, 1609 body: JSON.stringify({ motDePasse: champ.value }), 1610 }); 1611 const data = await r.json().catch(() => ({})); 1612 if (!r.ok || !data.session) { 1613 throw new Error(r.status === 401 ? 'Mot de passe incorrect' 1614 : r.status === 429 ? 'Trop de tentatives : réessayez dans une minute' 1615 : ((data.error && data.error.message) || `Serveur indisponible (${r.status})`)); 1616 } 1617 ecrireSession(data.session); 1618 champ.value = ''; 1619 document.getElementById('login-overlay').style.display = 'none'; 1620 loadFormations(); 1621 } catch(e) { 1622 champ.value = ''; 1623 montrerErreurConnexion(e.message); 1624 } 1625} 1626 1627// ââ INIT ââ 1628if (lireSession()) { 1629 document.getElementById('login-overlay').style.display = 'none'; 1630 loadFormations(); 1631} 1632</script>
1632 1633</body> 1634</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.