1063 lines
No EOL
46 KiB
HTML
1063 lines
No EOL
46 KiB
HTML
<!doctype html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||
<meta name="robots" content="noindex, nofollow">
|
||
<title>Espace Signature Employeur Odentas</title>
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--bg:#0b0e14; --panel:#111623; --muted:#1a2235; --card:#141b2d; --accent:#6ee7b7;
|
||
--text:#e6e8ef; --sub:#b6bed1; --brand:#7c3aed; --warn:#f59e0b; --ok:#10b981; --err:#ef4444;
|
||
--radius:16px;
|
||
}
|
||
*{box-sizing:border-box}
|
||
html,body{height:100%}
|
||
body{
|
||
margin:0; font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
|
||
color:var(--text); background:linear-gradient(180deg,#0b0e14 0%, #0b0e14 35%, #0e1320 100%);
|
||
}
|
||
.wrap{max-width:1100px;margin:32px auto;padding:0 20px}
|
||
header{
|
||
display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;
|
||
}
|
||
.brand{display:flex;gap:12px;align-items:center}
|
||
.logo{
|
||
height:60px; width:auto; border-radius:12px; display:block; object-fit:contain;
|
||
box-shadow:none;
|
||
}
|
||
/* Swap logos depending on theme */
|
||
.logo-light{ display:none }
|
||
.logo-dark{ display:block }
|
||
body.theme-light .logo-light{ display:block }
|
||
body.theme-light .logo-dark{ display:none }
|
||
body.theme-dark .logo-dark{ display:block }
|
||
body.theme-dark .logo-light{ display:none }
|
||
h1{font-size:20px;margin:0}
|
||
.badge{font-size:12px;padding:6px 10px;border-radius:999px;background:#1e293b;color:#c4d0ff;border:1px solid #2b3650}
|
||
.sync{margin-left:8px;padding:2px 8px;border-radius:999px;border:1px solid #2b3650;font-size:11px;white-space:nowrap}
|
||
.sync.ok{border-color:#115e45;color:#bbf7d0;background:rgba(16,185,129,.08)}
|
||
.sync.warn{border-color:#7c5d15;color:#fde68a;background:rgba(245,158,11,.08)}
|
||
|
||
.panel{
|
||
background:linear-gradient(180deg,#0f1424 0%, #0d1220 100%);
|
||
border:1px solid #1f2a44;border-radius:var(--radius);padding:18px;box-shadow:0 10px 30px rgba(0,0,0,.35);
|
||
}
|
||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:16px}
|
||
.stats-grid{grid-template-columns:repeat(5, minmax(0,1fr)); gap:12px}
|
||
.stats-grid > .col-3{grid-column:span 1}
|
||
.stats-grid > .col-4{grid-column:span 1}
|
||
.stats-grid > .col-8{grid-column:span 1}
|
||
.stats-grid .stat{padding:12px}
|
||
.stats-grid .stat .n{font-size:18px}
|
||
.stats-grid .stat .s{font-size:12px}
|
||
.col-4{grid-column:span 4}
|
||
.col-8{grid-column:span 8}
|
||
.col-3{grid-column:span 3}
|
||
.stat{display:flex;gap:14px;align-items:center;background:var(--card);border:1px solid #1f2a44;padding:14px;border-radius:14px}
|
||
.stat .pill{width:38px;height:38px;border-radius:12px;background:#1f2a44;display:grid;place-items:center}
|
||
.stat .n{font-size:22px;font-weight:700}
|
||
.filterbar{display:flex;gap:8px;align-items:center;margin-top:8px}
|
||
/* Modern theme toggle */
|
||
.theme-switch{ display:inline-block; position:relative; line-height:0 }
|
||
.theme-switch input{ position:absolute; inset:0; opacity:0; pointer-events:none }
|
||
.theme-switch .track{
|
||
width:56px; height:30px; background:#1f2a44; border:1px solid #33406a; border-radius:999px;
|
||
display:inline-flex; align-items:center; justify-content:center; position:relative; padding:3px; transition:.2s;
|
||
box-shadow:inset 0 2px 6px rgba(0,0,0,.25);
|
||
}
|
||
.theme-switch .knob{ width:24px; height:24px; border-radius:999px; background:#ffffff; position:absolute; left:3px; top:3px; transition:transform .2s ease; box-shadow:0 2px 6px rgba(0,0,0,.35) }
|
||
.theme-switch .icon{ font-size:14px; opacity:.9; pointer-events:none }
|
||
.theme-switch .icon.sun{ position:absolute; left:8px }
|
||
.theme-switch .icon.moon{ position:absolute; right:8px }
|
||
.theme-switch input:checked + .track .knob{ transform: translateX(26px) }
|
||
|
||
body.theme-light .theme-switch .track{ background:#e5e7eb; border-color:#cbd5e1; box-shadow:inset 0 2px 6px rgba(0,0,0,.08) }
|
||
body.theme-light .theme-switch .knob{ background:#ffffff }
|
||
/* Light theme variables */
|
||
body.theme-light{
|
||
--bg:#f4f6fb; --panel:#ffffff; --muted:#eef2f6; --card:#ffffff; --accent:#0ea5e9;
|
||
--text:#0f172a; --sub:#425066; --brand:#3b82f6; --warn:#b45309; --ok:#059669; --err:#b91c1c;
|
||
}
|
||
|
||
/* Global background for light */
|
||
body.theme-light{
|
||
background:linear-gradient(180deg,#f7f9fc 0%, #f6f8fb 35%, #eef2f6 100%);
|
||
}
|
||
|
||
/* Panels & cards */
|
||
body.theme-light .panel{ background:linear-gradient(180deg,#ffffff 0%, #ffffff 100%); border-color:#e6eaf2; box-shadow:0 6px 20px rgba(0,0,0,.08) }
|
||
body.theme-light .card{ background:#ffffff; border-color:#e6eaf2 }
|
||
body.theme-light .stat{ background:#ffffff; border-color:#e6eaf2 }
|
||
body.theme-light .pill{ background:#eef2f8 !important }
|
||
/* Light: emphasise Matricule & Salarié values, keep Template muted */
|
||
body.theme-light .card .meta{ color:#516070 } /* labels */
|
||
body.theme-light .card .meta .emph-val{ color:#0f172a !important } /* valeurs plus foncées mais moins dures */
|
||
body.theme-light .card .meta code,
|
||
body.theme-light .card .meta .muted-val{ color:#6b7280 !important } /* template un peu plus lisible mais discret */
|
||
|
||
/* Text tweaks */
|
||
body.theme-light h1, body.theme-light .title, body.theme-light .ref{ color:#0f172a }
|
||
body.theme-light .ref{ color:#334155; }
|
||
|
||
/* Inputs & buttons */
|
||
body.theme-light input[type="text"]{ background:#ffffff; border:1px solid #e6eaf2; color:#0f172a }
|
||
body.theme-light .btn{ background:#ffffff; color:#0f172a; border:1px solid #e6eaf2 }
|
||
body.theme-light .btn.sub{ background:#ffffff; border:1px solid #e6eaf2 }
|
||
|
||
/* Status chips */
|
||
body.theme-light .status{ background:#f1f5f9; border-color:#e5e7eb; color:#0b1220 }
|
||
body.theme-light .status.ok{ border-color:#86efac; background:#ecfdf5; color:#065f46 }
|
||
body.theme-light .status.warn{ border-color:#fde68a; background:#fffbeb; color:#92400e }
|
||
body.theme-light .status.err{ border-color:#fecaca; background:#fef2f2; color:#7f1d1d }
|
||
|
||
/* Header badges */
|
||
body.theme-light .badge{ background:#eef2f8; color:#0b1220; border-color:#e5e7eb }
|
||
body.theme-light .sync.ok{ border-color:#86efac; color:#065f46; background:#ecfdf5 }
|
||
body.theme-light .sync.warn{ border-color:#facc15; color:#92400e; background:#fffbeb }
|
||
|
||
/* Tooltips */
|
||
body.theme-light .tt[data-tip]::after{ background:#ffffff; color:#0b1220; border-color:#e5e7eb }
|
||
body.theme-light .tt[data-tip]::before{ border-top-color:#e5e7eb }
|
||
|
||
/* Dialogs */
|
||
body.theme-light dialog{ background:#ffffff; color:#0b1220; border-color:#e5e7eb; box-shadow:0 24px 60px rgba(0,0,0,.15) }
|
||
body.theme-light .modal-head{ background:#f8fafc; border-bottom-color:#e5e7eb }
|
||
body.theme-light .modal-body{ background:#ffffff }
|
||
body.theme-light .close{ color:#475569 }
|
||
|
||
/* Contact form fields */
|
||
body.theme-light select, body.theme-light textarea{ background:#ffffff !important; border:1px solid #e5e7eb !important; color:#0b1220 !important }
|
||
|
||
/* Page loader & overlays */
|
||
body.theme-light #page-loader{ background:rgba(255,255,255,.72) }
|
||
body.theme-light #page-loader > div{ background:#ffffff; border-color:#e5e7eb }
|
||
/* Inactivity overlay theming (light) */
|
||
body.theme-light #poll-timeout-overlay{ background:rgba(255,255,255,.72) }
|
||
body.theme-light #poll-timeout-overlay .inact-box{ background:#ffffff; border-color:#e5e7eb }
|
||
body.theme-light #poll-timeout-overlay .inact-head{ background:#f8fafc; border-bottom-color:#e5e7eb; color:#0b1220; border-top-left-radius:16px; border-top-right-radius:16px }
|
||
body.theme-light #poll-timeout-overlay .inact-head .pill{ background:#eef2f8 !important; color:#0b1220 }
|
||
body.theme-light #poll-timeout-overlay .inact-body{ color:#0b1220 }
|
||
body.theme-light #poll-timeout-overlay .inact-foot{ border-top-color:#e5e7eb; color:#475569 }
|
||
|
||
input[type="text"]{
|
||
background:#0e1320;border:1px solid #1f2a44;color:var(--text);padding:10px 12px;border-radius:10px;outline:none;width:240px
|
||
}
|
||
.btn{
|
||
appearance:none;border:0;background:#222b45;color:#e5e9ff;padding:10px 14px;border-radius:10px;
|
||
cursor:pointer;border:1px solid #33406a;transition:.2s; font-weight:600
|
||
}
|
||
.btn:hover{transform:translateY(-1px);box-shadow:0 8px 20px rgba(0,0,0,.25)}
|
||
.btn:disabled{ cursor:not-allowed !important; opacity:.75 }
|
||
.btn:disabled:hover{ transform:none; box-shadow:none }
|
||
|
||
/* Loader spinner */
|
||
.spinner{ width:36px; height:36px; border-radius:999px; border:3px solid rgba(203,213,225,.25); border-top-color:#6ee7b7; animation:spin 0.8s linear infinite }
|
||
@keyframes spin{ to { transform: rotate(360deg) } }
|
||
|
||
/* Modern tooltips */
|
||
.tt{ position:relative; display:inline-flex; align-items:center }
|
||
.tt[data-tip]{ cursor:help }
|
||
.tt[data-tip]::after{
|
||
content: attr(data-tip);
|
||
position:absolute; right:0; bottom:calc(100% + 10px);
|
||
background:#0b1220; color:#e6e8ef; border:1px solid #283252;
|
||
padding:8px 10px; border-radius:8px; font-size:12px; line-height:1.35;
|
||
box-shadow:0 12px 30px rgba(0,0,0,.35);
|
||
max-width:280px; width:max-content; z-index:1000;
|
||
opacity:0; transform:translateY(4px); pointer-events:none; transition:opacity .15s ease, transform .15s ease;
|
||
white-space:normal;
|
||
}
|
||
.tt[data-tip]::before{
|
||
content:""; position:absolute; right:12px; bottom:100%;
|
||
border:6px solid transparent; border-top-color:#283252; transform:translateY(0);
|
||
opacity:0; transition:opacity .15s ease;
|
||
}
|
||
.tt[data-tip]:hover::after,
|
||
.tt[data-tip]:hover::before,
|
||
.tt[data-tip]:focus-within::after,
|
||
.tt[data-tip]:focus-within::before{ opacity:1; transform:translateY(0) }
|
||
/* Inactivity overlay default (dark theme) */
|
||
#poll-timeout-overlay{ background:rgba(7,10,18,.72); backdrop-filter:blur(2px) }
|
||
#poll-timeout-overlay .inact-box{ background:#0f1424; border:1px solid #1f2a44; border-radius:16px; box-shadow:0 20px 60px rgba(0,0,0,.55) }
|
||
#poll-timeout-overlay .inact-head{ background:#0f1424; border-bottom:1px solid #1f2a44; color:#e6e8ef; border-top-left-radius:16px; border-top-right-radius:16px; position:relative; z-index:1 }
|
||
#poll-timeout-overlay .inact-body{ color:#cbd5e1 }
|
||
#poll-timeout-overlay .inact-foot{ color:#94a3b8; border-top:1px solid #1f2a44 }
|
||
.btn.accent{background:linear-gradient(90deg,#6ee7b7 0,#3b82f6 100%);color:#091123;border:0}
|
||
.btn.sub{background:#0f172a;border:1px solid #293354}
|
||
.fab{
|
||
position:absolute;right:12px;bottom:12px;width:38px;height:38px;
|
||
border-radius:999px;display:grid;place-items:center;
|
||
border:1px solid #33406a;background:#0f172a;color:#cbd5e1;cursor:pointer
|
||
}
|
||
.fab:hover{transform:translateY(-1px);box-shadow:0 8px 20px rgba(0,0,0,.25)}
|
||
/* Light theme variant for contact button */
|
||
body.theme-light .fab{
|
||
background:#ffffff;
|
||
color:#0f172a;
|
||
border:1px solid #e6eaf2;
|
||
box-shadow:0 4px 12px rgba(0,0,0,.06);
|
||
}
|
||
body.theme-light .fab:hover{
|
||
transform:translateY(-1px);
|
||
box-shadow:0 8px 18px rgba(0,0,0,.10);
|
||
}
|
||
.fab svg{width:18px;height:18px;display:block}
|
||
.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:16px;margin-top:16px}
|
||
.card{
|
||
background:linear-gradient(180deg,#12182a 0%, #0f1424 100%);
|
||
border:1px solid #1f2a44;border-radius:16px;padding:14px;
|
||
display:flex;flex-direction:column;gap:12px;position:relative;
|
||
}
|
||
.row{display:flex;justify-content:space-between;align-items:center;gap:8px}
|
||
.title{font-weight:500}
|
||
.ref{color:#aab6ff;font-weight:500;line-height:1.2;margin-top:2px}
|
||
.card .emph-val{ font-weight:500 }
|
||
.status{font-size:12px;padding:6px 10px;border-radius:999px;border:1px solid #2b3650;background:#0e1628;color:#cbd5e1;white-space:nowrap}
|
||
.status.ok{border-color:#115e45;color:#bbf7d0;background:rgba(16,185,129,.08)}
|
||
.status.warn{border-color:#7c5d15;color:#fde68a;background:rgba(245,158,11,.08)}
|
||
.status.err{border-color:#7a1f23;color:#fecaca;background:rgba(239,68,68,.08)}
|
||
.empty{opacity:.7;padding:16px;text-align:center}
|
||
|
||
/* modal */
|
||
dialog{
|
||
width:min(900px,95vw);border:1px solid #2b3650;border-radius:16px;background:#0d1220;color:var(--text);
|
||
box-shadow:0 20px 60px rgba(0,0,0,.55);padding:0;overflow:hidden
|
||
}
|
||
.modal-head{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;border-bottom:1px solid #1f2a44;background:#0f1424}
|
||
.modal-body{padding:0;height:min(80vh,720px);background:#0a0f1a;overflow:auto}
|
||
iframe{width:100%;height:100%;border:0}
|
||
docuseal-form{display:block;width:100%;min-height:100%}
|
||
.close{background:transparent;border:0;color:#aab6d6;font-size:22px;cursor:pointer}
|
||
footer{margin-top:20px;text-align:center;color:#7e8bb6;font-size:12px;opacity:.8}
|
||
@media (max-width:860px){.col-4,.col-8{grid-column:span 12}}
|
||
@media (max-width:1024px){.stats-grid{grid-template-columns:repeat(3, minmax(0,1fr))}}
|
||
@media (max-width:640px){.stats-grid{grid-template-columns:repeat(2, minmax(0,1fr))}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<header>
|
||
<div class="brand">
|
||
<!-- Logo versions: dark & light -->
|
||
<img class="logo logo-dark" src="https://newstaging.odentas.fr/wp-content/uploads/2025/09/Odentas-Logo-Bleu-Fond-Transparent-5.png" alt="Logo Odentas (sombre)" />
|
||
<img class="logo logo-light" src="https://newstaging.odentas.fr/wp-content/uploads/2025/08/Odentas-Logo-Bleu-Fond-Transparent-4-1.png" alt="Logo Odentas (clair)" />
|
||
<div>
|
||
<h1 id="page-title">Espace Signature Employeur Odentas<span id="employer-name" class="badge" style="margin-left:8px; display:none"></span></h1>
|
||
<div id="batch-sub" class="badge">
|
||
Chargement… <span id="sync-status" class="sync ok" style="display:none">Synchronisation activée</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="filterbar">
|
||
<label class="theme-switch" aria-label="Basculer le thème clair/sombre">
|
||
<input id="theme-toggle" type="checkbox" />
|
||
<span class="track">
|
||
<span class="knob"></span>
|
||
<span class="icon sun" aria-hidden="true">🌞</span>
|
||
<span class="icon moon" aria-hidden="true">🌙</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="panel">
|
||
<div class="grid stats-grid">
|
||
<div class="col-3">
|
||
<div class="stat">
|
||
<div class="pill">🗂️</div>
|
||
<div><div class="n" id="stat-total">—</div><div class="s">Contrats dans le batch</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-3">
|
||
<div class="stat">
|
||
<div class="pill">✍️</div>
|
||
<div><div class="n" id="stat-ready">—</div><div class="s">À signer par l'employeur</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-3">
|
||
<div class="stat">
|
||
<div class="pill">✅</div>
|
||
<div><div class="n" id="stat-done">—</div><div class="s">Signés par l'employeur</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-3">
|
||
<div class="stat">
|
||
<div class="pill">🧾</div>
|
||
<div><div class="n" id="stat-emp-todo">—</div><div class="s">À signer par les salariés</div></div>
|
||
</div>
|
||
</div>
|
||
<div class="col-3">
|
||
<div class="stat">
|
||
<div class="pill">🏁</div>
|
||
<div><div class="n" id="stat-finished">—</div><div class="s">Signés par toutes les parties</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel" style="margin-top:16px">
|
||
<div id="cards" class="cards"></div>
|
||
<div id="empty" class="empty" style="display:none">Aucun contrat trouvé pour ce batch.</div>
|
||
</section>
|
||
|
||
<footer>© Odentas Media SAS 2021 - 2025 | Tous droits réservés.</footer>
|
||
</div>
|
||
|
||
<dialog id="dlg-contact">
|
||
<div class="modal-head">
|
||
<strong>Contacter le gestionnaire</strong>
|
||
<button class="close" aria-label="Fermer" onclick="qs('#dlg-contact').close()">×</button>
|
||
</div>
|
||
<div class="modal-body" style="padding:16px;">
|
||
<form id="contact-form" style="display:grid;gap:12px">
|
||
<div style="font-size:12px;color:#aab6d6">
|
||
Contrat : <strong id="contact-ref">—</strong> · Matricule : <strong id="contact-mat">—</strong>
|
||
</div>
|
||
|
||
<div style="display:grid;gap:6px">
|
||
<label for="contact-reason" style="font-size:12px;color:#aab6d6">Motif</label>
|
||
<select id="contact-reason" required
|
||
style="background:#0e1320;border:1px solid #1f2a44;color:#e6e8ef;padding:10px;border-radius:10px">
|
||
<option value="Erreur">Signaler une erreur</option>
|
||
<option value="Modification">Demander une modification</option>
|
||
<option value="Annulation">Demander une annulation</option>
|
||
<option value="Avenant">Demander un avenant</option>
|
||
<option value="Autre">Autre</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="display:grid;gap:6px">
|
||
<label for="contact-msg" style="font-size:12px;color:#aab6d6">Votre message</label>
|
||
<textarea id="contact-msg" rows="6" placeholder="Décrivez votre demande..."
|
||
style="background:#0e1320;border:1px solid #1f2a44;color:#e6e8ef;padding:10px;border-radius:10px"></textarea>
|
||
</div>
|
||
|
||
<div style="font-size:12px;color:#aab6d6">
|
||
Message envoyé à votre gestionnaire: <strong id="contact-to">—</strong>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:6px">
|
||
<button type="button" class="btn sub" onclick="qs('#dlg-contact').close()">Annuler</button>
|
||
<button type="submit" class="btn accent">Envoyer</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</dialog>
|
||
|
||
<dialog id="dlg">
|
||
<div class="modal-head">
|
||
<strong id="dlg-title">Signature</strong>
|
||
<button class="close" aria-label="Fermer" onclick="dlg.close()">×</button>
|
||
</div>
|
||
<div class="modal-body" id="dlg-body"></div>
|
||
</dialog>
|
||
|
||
<!-- Page loader overlay -->
|
||
<div id="page-loader" style="display:none;position:fixed;inset:0;background:rgba(7,10,18,.82);backdrop-filter:blur(2px);z-index:10000">
|
||
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;gap:14px;background:#0f1424;border:1px solid #1f2a44;border-radius:16px;padding:24px 28px;box-shadow:0 20px 60px rgba(0,0,0,.55);min-width:260px">
|
||
<div class="spinner" aria-hidden="true"></div>
|
||
<div id="page-loader-text" style="color:#cbd5e1">Connexion aux serveurs Odentas…</div>
|
||
<div id="page-loader-subtext" style="color:#94a3b8;font-size:12px;margin-top:4px;display:none">C’est un peu plus long que d’habitude… merci de patienter quelques secondes.</div>
|
||
</div>
|
||
</div>
|
||
<!-- Overlay d'inactivité / fin de polling -->
|
||
<div id="poll-timeout-overlay" style="display:none;position:fixed;inset:0;backdrop-filter:blur(2px);z-index:9999">
|
||
<div class="inact-box" style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(520px,92vw);">
|
||
<div class="inact-head" style="padding:14px 16px;display:flex;align-items:center;gap:10px">
|
||
<div class="pill" style="background:#1f2a44;width:34px;height:34px;border-radius:10px;display:grid;place-items:center">⏸️</div>
|
||
<div style="font-weight:700">Synchronisation en pause</div>
|
||
</div>
|
||
<div class="inact-body" style="padding:16px">
|
||
<p style="margin:0 0 10px 0">La synchronisation avec nos serveurs est en pause.</p>
|
||
<p style="margin:0 0 16px 0">Cliquez sur <strong>Réactiver la synchronisation</strong> ou n'importe où sur la page pour reprendre le suivi automatique.</p>
|
||
<div style="display:flex;gap:8px;justify-content:center">
|
||
<button id="btn-resume-poll" class="btn accent">Réactiver la synchronisation</button>
|
||
</div>
|
||
</div>
|
||
<div class="inact-foot" style="padding:10px 16px;font-size:12px;line-height:1.4">
|
||
🌿 Pour limiter les connexions répétées et économiser de l’énergie, la synchronisation se met en pause après un moment d’inactivité ou lorsque l’onglet est en arrière‑plan.
|
||
<br><br>L'Espace Paie Odentas est hébergé dans un data-center situé en Union Européenne et alimenté à 100% en énergies vertes.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script async src="https://cdn.docuseal.com/js/form.js"></script>
|
||
<script>
|
||
/* ================================
|
||
CONFIG (remplacer pour vos tests)
|
||
================================= */
|
||
const CONFIG = {
|
||
AIRTABLE: {
|
||
BASE_ID: 'appyVHP0ZQdQzXFPn', // ← Base ID
|
||
TABLE_CONTRATS: 'Contrats de travail', // ← Nom table
|
||
TABLE_BATCHES: 'Batches Signature', // ← Nom table
|
||
API_KEY: 'patrNer5Dqyff2KEJ.aeebec95fc07db378dc92867d81ceba64e5e625b76ab0153395172595d8aa50c' // ← ⚠️ clé perso (évitez côté client)
|
||
},
|
||
PROXY: {
|
||
DS_BASE: 'https://bsezjh3d43.execute-api.eu-west-3.amazonaws.com/default/docuseal' // ← mets ici l'URL de ta Lambda/API Gateway
|
||
},
|
||
POLL_INTERVAL_MS: 8000, // rafraîchit toutes les 8s (ajuste si besoin)
|
||
POLL_MAX_MS: 300000, // ⏳ durée max de polling (10 min). Met 0 pour désactiver l'arrêt auto
|
||
CONTACT: {
|
||
WEBHOOK_URL: 'https://n8n.odentas.fr/webhook-test/947d5d4e-7301-42f6-810a-9136eaac84f4', // ← colle ici l'URL de ton webhook n8n (Public Webhook)
|
||
SECRET: '' // ← optionnel: secret partagé; sera envoyé en Authorization: Bearer <SECRET>
|
||
}
|
||
};
|
||
window.CONFIG = CONFIG;
|
||
|
||
/* ================================
|
||
THEME (light/dark with persistence)
|
||
================================= */
|
||
function getSystemPref(){
|
||
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark';
|
||
}
|
||
function applyTheme(t){
|
||
const body = document.body;
|
||
body.classList.toggle('theme-light', t==='light');
|
||
body.classList.toggle('theme-dark', t==='dark');
|
||
const chk = document.getElementById('theme-toggle');
|
||
if (chk) chk.checked = (t==='light');
|
||
}
|
||
function initTheme(){
|
||
const saved = localStorage.getItem('hub_theme');
|
||
const theme = saved || getSystemPref();
|
||
applyTheme(theme);
|
||
}
|
||
function toggleTheme(){
|
||
const isLight = document.body.classList.contains('theme-light');
|
||
const next = isLight ? 'dark' : 'light';
|
||
applyTheme(next);
|
||
localStorage.setItem('hub_theme', next);
|
||
}
|
||
|
||
/* ================================
|
||
HELPERS
|
||
================================= */
|
||
function showPageLoader(message){
|
||
const el = qs('#page-loader'); if (el) el.style.display = 'block';
|
||
const t = qs('#page-loader-text'); if (t && message) t.textContent = message;
|
||
const st = qs('#page-loader-subtext'); if (st) st.style.display = 'none';
|
||
}
|
||
function hidePageLoader(){
|
||
const el = qs('#page-loader'); if (el) el.style.display = 'none';
|
||
}
|
||
// Affichage immédiat (pas de délai), accepte un message
|
||
function showPageLoaderDelayed(message){
|
||
showPageLoader(message);
|
||
}
|
||
function clearPageLoaderDelayed(){
|
||
if (window.__loaderTimer){ clearTimeout(window.__loaderTimer); window.__loaderTimer = null; }
|
||
if (window.__loaderSubTimer){ clearTimeout(window.__loaderSubTimer); window.__loaderSubTimer = null; }
|
||
const st = qs('#page-loader-subtext'); if (st) st.style.display = 'none';
|
||
hidePageLoader();
|
||
}
|
||
function scheduleLoaderSubtext(delayMs = 3500){
|
||
if (window.__loaderSubTimer) clearTimeout(window.__loaderSubTimer);
|
||
window.__loaderSubTimer = setTimeout(()=>{
|
||
const st = qs('#page-loader-subtext'); if (st) st.style.display = 'block';
|
||
}, delayMs);
|
||
}
|
||
async function refreshNow(batchId, message = 'Reconnexion en cours…'){
|
||
try{
|
||
showPageLoaderDelayed(message);
|
||
scheduleLoaderSubtext(3500); // affiche une phrase de patience si c'est un peu long
|
||
await loadAndRender(batchId);
|
||
} finally {
|
||
clearPageLoaderDelayed();
|
||
}
|
||
}
|
||
const qs = (sel, el=document)=>el.querySelector(sel);
|
||
const qsa = (sel, el=document)=>[...el.querySelectorAll(sel)];
|
||
const escapeAirtable = s => String(s).replace(/'/g, "\\'");
|
||
const asHeadersAirtable = { 'Authorization': `Bearer ${CONFIG.AIRTABLE.API_KEY}` };
|
||
const asHeadersJSON = { 'Content-Type':'application/json' };
|
||
|
||
const dlg = document.getElementById('dlg');
|
||
function openDlgSafely() {
|
||
if (dlg && typeof dlg.showModal === 'function') {
|
||
dlg.showModal();
|
||
} else if (dlg) {
|
||
dlg.setAttribute('open', '');
|
||
}
|
||
}
|
||
|
||
window.addEventListener('load', () => {
|
||
const ok = (window.customElements && customElements.get('docuseal-form')) ? 'ready' : 'pending';
|
||
console.log('[Hub] DocuSeal web component:', ok);
|
||
});
|
||
console.log('[Hub] Contact webhook:', CONFIG.CONTACT.WEBHOOK_URL ? 'set' : 'not set');
|
||
|
||
function paramsFromUrl() {
|
||
const u = new URL(location.href);
|
||
|
||
// 1) Query params (case/variant tolerant)
|
||
let batchId =
|
||
u.searchParams.get('batchId') ||
|
||
u.searchParams.get('batchID') ||
|
||
u.searchParams.get('batch_id') ||
|
||
null;
|
||
|
||
// 2) Hash fallback (#batchId=... or #batchID=...)
|
||
if (!batchId && u.hash) {
|
||
const m = u.hash.match(/(?:batchId|batchID|batch_id)=([^&]+)/);
|
||
if (m) batchId = decodeURIComponent(m[1]);
|
||
}
|
||
|
||
// 3) Path fallback (dernier segment) UNIQUEMENT si ça ressemble à un ID
|
||
if (!batchId) {
|
||
const last = (u.pathname.split('/').filter(Boolean).pop() || '').trim();
|
||
const isHtml = /\.html?$/i.test(last);
|
||
const looksLikeId = /^[0-9a-fA-F-]{20,}$/.test(last) || /^[A-Za-z0-9_-]{12,}$/.test(last);
|
||
if (!isHtml && looksLikeId && !/^hub_?signatures?$/i.test(last)) {
|
||
batchId = last;
|
||
}
|
||
}
|
||
|
||
return {
|
||
batchId: batchId || null,
|
||
token: u.searchParams.get('token') || u.searchParams.get('t') || null
|
||
};
|
||
}
|
||
function fmtStatusEmployeur(val){
|
||
if(!val) return {label:'—',cls:''};
|
||
const n = String(val).toLowerCase();
|
||
if(n==='oui') return {label:'Signé par employeur', cls:'ok'};
|
||
if(n==='non') return {label:'En attente employeur', cls:'warn'};
|
||
return {label:val, cls:''};
|
||
}
|
||
|
||
function setSyncStatus(state){
|
||
const el = qs('#sync-status');
|
||
if (!el) return;
|
||
if (state === 'paused'){
|
||
el.textContent = 'Synchronisation en pause';
|
||
el.classList.remove('ok'); el.classList.add('warn');
|
||
el.style.display = 'inline-block';
|
||
} else {
|
||
el.textContent = 'Synchronisation activée';
|
||
el.classList.remove('warn'); el.classList.add('ok');
|
||
el.style.display = 'inline-block';
|
||
}
|
||
}
|
||
|
||
/* ================================
|
||
AIRTABLE API
|
||
================================= */
|
||
async function fetchAirtable(path){
|
||
const url = `https://api.airtable.com/v0/${CONFIG.AIRTABLE.BASE_ID}/${encodeURIComponent(path)}`;
|
||
const res = await fetch(url, { headers: asHeadersAirtable });
|
||
if(!res.ok) throw new Error('Airtable error '+res.status);
|
||
return res.json();
|
||
}
|
||
|
||
async function getBatchByBatchId(batchId){
|
||
const formula = `({batch_id}='${escapeAirtable(batchId)}')`;
|
||
const url = `https://api.airtable.com/v0/${CONFIG.AIRTABLE.BASE_ID}/${encodeURIComponent(CONFIG.AIRTABLE.TABLE_BATCHES)}?maxRecords=1&filterByFormula=${encodeURIComponent(formula)}`;
|
||
const res = await fetch(url, { headers: asHeadersAirtable });
|
||
if(!res.ok) throw new Error('Airtable batch error '+res.status);
|
||
const data = await res.json();
|
||
return data.records?.[0] || null;
|
||
}
|
||
|
||
async function getContratsForBatch(batchId, linkedIds=null){
|
||
// Méthode A (champ batch_id sur Contrats)
|
||
const formulaA = `({batch_id}='${escapeAirtable(batchId)}')`;
|
||
// Méthode B (à partir de la liste liée 'contrats' sur le batch)
|
||
const filterIds = (linkedIds||[]).map(id => `RECORD_ID()='${id}'`).join(',');
|
||
const formulaB = filterIds ? `OR(${filterIds})` : null;
|
||
|
||
const formula = formulaB || formulaA;
|
||
const url = `https://api.airtable.com/v0/${CONFIG.AIRTABLE.BASE_ID}/${encodeURIComponent(CONFIG.AIRTABLE.TABLE_CONTRATS)}?filterByFormula=${encodeURIComponent(formula)}`;
|
||
const res = await fetch(url, { headers: asHeadersAirtable });
|
||
if(!res.ok) throw new Error('Airtable contrats error '+res.status);
|
||
const data = await res.json();
|
||
return data.records || [];
|
||
}
|
||
|
||
/* ================================
|
||
DOCUSEAL API
|
||
================================= */
|
||
async function dsFetch(path, opts={}){
|
||
const url = `${CONFIG.DOCUSEAL.API_URL}${path}`;
|
||
const res = await fetch(url, {
|
||
...opts,
|
||
headers: { 'X-Auth-Token': CONFIG.DOCUSEAL.TOKEN, ...(opts.headers||{}) }
|
||
});
|
||
if(!res.ok){
|
||
let msg = res.status+' '+res.statusText;
|
||
try{ const j = await res.json(); msg += ' - '+(j.error || JSON.stringify(j)); }catch{}
|
||
throw new Error('DocuSeal: '+msg);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
async function dsProxy(path, opts = {}) {
|
||
const base = CONFIG.PROXY?.DS_BASE || '';
|
||
const url = `${base}${path}`;
|
||
const res = await fetch(url, { ...opts });
|
||
if (!res.ok) {
|
||
let msg = res.status+' '+res.statusText;
|
||
try { const j = await res.json(); msg += ' - '+(j.error || JSON.stringify(j)); } catch {}
|
||
throw new Error('DocuSeal Proxy: ' + msg);
|
||
}
|
||
return res.json();
|
||
}
|
||
|
||
// Récupère la soumission pour un template, spécifique au rôle Employeur
|
||
async function getEmployerSubmissionByTemplate(templateId){
|
||
// Utilise directement /templates/:id/submissions (il n'y aura jamais plusieurs soumissions par template)
|
||
const resp = await dsProxy(`/templates/${templateId}/submissions`)
|
||
const sub = (resp?.data && Array.isArray(resp.data)) ? resp.data[0] : (Array.isArray(resp) ? resp[0] : resp);
|
||
if (!sub || !sub.id) return null;
|
||
|
||
// Récupère le détail pour obtenir l'URL d'embed ou le slug
|
||
const detail = await dsProxy(`/submissions/${sub.id}`);
|
||
const roles = detail.submitters || detail.roles || [];
|
||
const employer = roles.find(r => (r.role||r.name)==='Employeur') || {};
|
||
// Si un slug est présent pour le rôle Employeur, construit l'URL publique
|
||
let embed = null;
|
||
if (employer.slug) {
|
||
embed = `https://docuseal.eu/s/${employer.slug}`;
|
||
} else {
|
||
embed = employer.embed_src || employer.sign_src || detail.embed_src || null;
|
||
}
|
||
return { id: sub.id, embed_src: embed };
|
||
}
|
||
|
||
/* ================================
|
||
RENDER
|
||
================================= */
|
||
function renderBatchHeader(batch, nb, nbReady, nbDone, nbEmpTodo, nbFinished){
|
||
const structure = batch?.fields?.['Nom structure'] || batch?.fields?.['Structure'] || '—';
|
||
// Employer name logic
|
||
const employer = batch?.fields?.['employeur_api'] || batch?.fields?.['Employeur'] || batch?.fields?.['Client'] || null;
|
||
const managerEmail = 'paie@odentas.fr';
|
||
window.__managerEmail = managerEmail;
|
||
const empEl = qs('#employer-name');
|
||
if (empEl) {
|
||
if (employer) { empEl.textContent = employer; empEl.style.display='inline-block'; }
|
||
else { empEl.style.display='none'; }
|
||
}
|
||
const batchHtml = `Batch ${batch?.fields?.batch_id || '—'} · ${structure} <span id="sync-status" class="sync ok" style="display:none">Synchronisation activée</span>`;
|
||
qs('#batch-sub').innerHTML = batchHtml;
|
||
setSyncStatus('active');
|
||
qs('#stat-total').textContent = nb;
|
||
qs('#stat-ready').textContent = nbReady;
|
||
qs('#stat-done').textContent = nbDone;
|
||
qs('#stat-finished').textContent = nbFinished;
|
||
qs('#stat-emp-todo').textContent = nbEmpTodo;
|
||
}
|
||
|
||
function cardTemplate(c){
|
||
const f = c.fields || {};
|
||
const ref = f.Reference || '—';
|
||
const mat = f['Matricule API'] || f.Matricule || '—';
|
||
const nom = f['Nom salarié'] || f['Nom Salarié'] || f['Nom'] || f['Salarié'] || f['Nom complet'] || f['Prénom Nom'] || f['Prenom Nom'] || f['Nom et Prénom'] || '—';
|
||
const st = fmtStatusEmployeur(f['Contrat signé par employeur']);
|
||
const isSigned = String(f['Contrat signé par employeur']||'').toLowerCase() === 'oui';
|
||
const btnDisabled = !f._employer_embed || isSigned;
|
||
const salarieOK = String(f['Contrat signé']||'').toLowerCase()==='oui';
|
||
let stFinal = st;
|
||
if (isSigned && salarieOK) {
|
||
stFinal = { label: 'Signé par salarié', cls: 'ok' };
|
||
}
|
||
|
||
return `
|
||
<div class="card" data-id="${c.id}">
|
||
<div class="row">
|
||
<div>
|
||
<div class="title">Contrat</div>
|
||
<div class="ref">${ref}</div>
|
||
</div>
|
||
<div class="status ${stFinal.cls}">${stFinal.label}</div>
|
||
</div>
|
||
<div class="row meta" style="color:#aab6d6">
|
||
<div>Matricule : <strong class="emph-val">${mat}</strong></div>
|
||
<div>Template : <code class="muted-val">${f.docuseal_template_id || '—'}</code></div>
|
||
</div>
|
||
<div class="row meta" style="color:#aab6d6">
|
||
<div>Salarié : <strong class="emph-val">${nom}</strong></div>
|
||
<div></div>
|
||
</div>
|
||
<div class="row">
|
||
${btnDisabled ? `
|
||
<span class="tt" data-tip="Ce contrat a été envoyé à votre salarié, et sera disponible au téléchargement après réception de toutes les signatures" tabindex="0">
|
||
<button class="btn" data-open="${c.id}" disabled>Déjà signé</button>
|
||
</span>
|
||
` : `
|
||
<button class="btn" data-open="${c.id}">Signer (Employeur)</button>
|
||
`}
|
||
<button class="fab contact-fab" title="Contacter le gestionnaire" data-contact="${c.id}">
|
||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||
<path d="M2 5a3 3 0 0 1 3-3h14a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H9.5l-3.7 3.2A1 1 0 0 1 4 20.7V18H5a3 3 0 0 1-3-3V5Zm3-1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h2v1.6l2.6-2.3a1 1 0 0 1 .7-.3H19a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5Z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function bindCardActions(records){
|
||
const byId = Object.fromEntries(records.map(r=>[r.id, r]));
|
||
qsa('[data-open]').forEach(el=>{
|
||
el.addEventListener('click', e=>{
|
||
e.preventDefault();
|
||
const id = el.getAttribute('data-open');
|
||
const rec = byId[id];
|
||
const f = rec?.fields || {};
|
||
if(!f._employer_embed){
|
||
alert("Lien d'embed indisponible pour ce contrat.");
|
||
return;
|
||
}
|
||
const title = `Signature (Employeur) · ${f.Reference || id}`;
|
||
qs('#dlg-title').textContent = title;
|
||
const src = f._employer_embed;
|
||
// Web component DocuSeal (évite X-Frame-Options)
|
||
qs('#dlg-body').innerHTML = `
|
||
<docuseal-form
|
||
data-src="${src}"
|
||
data-language="fr"
|
||
data-with-title="false"
|
||
data-background-color="#fff"
|
||
data-custom-css="
|
||
/* Exemples : adapte selon tes besoins/branding */
|
||
/* Couleur du bouton Valider */
|
||
#submit_form_button { background-color: #6ee7b7 !important; color: #0b1220 !important; }
|
||
/* Barre d’en-tête interne du formulaire */
|
||
.ds-form-header { background:#fff !important; color:#0b1220 !important; }
|
||
.ds-form-header .title, .ds-form-header .subtitle { color:#0b1220 !important; }
|
||
/* Icônes & textes secondaires */
|
||
.ds-text-muted { color:#475569 !important; }
|
||
">
|
||
</docuseal-form>
|
||
`;
|
||
// Fallback si le composant n'est pas prêt
|
||
setTimeout(() => {
|
||
const ready = window.customElements && customElements.get('docuseal-form');
|
||
const present = qs('docuseal-form', qs('#dlg-body'));
|
||
if (!ready || !present) {
|
||
console.warn('[Hub] docuseal-form indisponible, ouverture d’un nouvel onglet');
|
||
window.open(src, '_blank', 'noopener');
|
||
}
|
||
}, 250);
|
||
openDlgSafely();
|
||
});
|
||
});
|
||
}
|
||
|
||
function openContactDialog(rec){
|
||
const f = rec?.fields || {};
|
||
qs('#contact-ref').textContent = f.Reference || rec.id;
|
||
qs('#contact-mat').textContent = f['Matricule API'] || f.Matricule || '—';
|
||
qs('#contact-to').textContent = 'Renaud BREVIERE-ABRAHAM';
|
||
qs('#contact-reason').value = 'Erreur';
|
||
qs('#contact-msg').value = '';
|
||
// Stocker le contexte du contrat à l'ouverture de la modale contact
|
||
window.__contactCtx = {
|
||
recordId: rec.id,
|
||
reference: f.Reference || rec.id,
|
||
matricule: f['Matricule API'] || f.Matricule || null,
|
||
templateId: f.docuseal_template_id || null
|
||
};
|
||
// ouvre la modale
|
||
const dlgc = qs('#dlg-contact');
|
||
if (dlgc && typeof dlgc.showModal === 'function') dlgc.showModal();
|
||
else if (dlgc) dlgc.setAttribute('open','');
|
||
}
|
||
|
||
function bindContactActions(records){
|
||
const byId = Object.fromEntries(records.map(r=>[r.id, r]));
|
||
// Boutons sur les cartes
|
||
qsa('[data-contact]').forEach(el=>{
|
||
if (el.__bound) return; el.__bound = true;
|
||
el.addEventListener('click', e=>{
|
||
e.preventDefault();
|
||
const id = el.getAttribute('data-contact');
|
||
const rec = byId[id];
|
||
if(!rec){ alert('Contrat introuvable.'); return; }
|
||
openContactDialog(rec);
|
||
});
|
||
});
|
||
|
||
// Soumission du formulaire
|
||
const form = qs('#contact-form');
|
||
if (form && !form.__bound) {
|
||
form.__bound = true;
|
||
form.addEventListener('submit', async (e)=>{
|
||
e.preventDefault();
|
||
const to = (window.__managerEmail || 'paie@odentas.fr');
|
||
const reason = qs('#contact-reason').value;
|
||
const msg = qs('#contact-msg').value || '';
|
||
const ref = qs('#contact-ref').textContent;
|
||
const mat = qs('#contact-mat').textContent;
|
||
const url = location.href;
|
||
|
||
// Option webhook (si tu me donnes une URL)
|
||
const webhook = (CONFIG && CONFIG.CONTACT && CONFIG.CONTACT.WEBHOOK_URL) ? CONFIG.CONTACT.WEBHOOK_URL : null;
|
||
console.log('[Hub] contact webhook URL:', webhook || '(none)');
|
||
try{
|
||
if (webhook) {
|
||
const ctx = window.__contactCtx || {};
|
||
const batch = window.__batchCache || {};
|
||
const bf = (batch.fields||{});
|
||
const body = {
|
||
source: 'hub_signature_batch',
|
||
event: 'contact_request',
|
||
timestamp: new Date().toISOString(),
|
||
to,
|
||
reason,
|
||
message: msg,
|
||
page: url,
|
||
contract: {
|
||
airtable_record_id: ctx.recordId || null,
|
||
reference: ctx.reference || ref,
|
||
matricule: ctx.matricule || mat,
|
||
template_id: ctx.templateId || null
|
||
},
|
||
batch: {
|
||
airtable_record_id: batch.id || null,
|
||
batch_id: bf.batch_id || null,
|
||
employeur: bf.employeur || bf.Employeur || bf.Client || null,
|
||
structure: bf['Nom structure'] || bf.Structure || null
|
||
}
|
||
};
|
||
const headers = { 'Content-Type':'application/json' };
|
||
if (CONFIG.CONTACT.SECRET) headers['Authorization'] = `Bearer ${CONFIG.CONTACT.SECRET}`;
|
||
const res = await fetch(webhook, { method:'POST', mode:'cors', headers, body: JSON.stringify(body) });
|
||
if(!res.ok) throw new Error('Webhook HTTP '+res.status);
|
||
alert('Votre demande a été envoyée au gestionnaire.');
|
||
} else {
|
||
// Fallback: mailto
|
||
const subject = encodeURIComponent(`[Contrat ${ref}] ${reason}`);
|
||
const body = encodeURIComponent(`Bonjour,\n\nJe souhaite ${reason.toLowerCase()} sur le contrat ${ref} (matricule ${mat}).\n\nDétails :\n${msg}\n\nLien : ${url}`);
|
||
window.location.href = `mailto:${encodeURIComponent(to)}?subject=${subject}&body=${body}`;
|
||
}
|
||
qs('#dlg-contact').close();
|
||
}catch(err){
|
||
console.error('contact submit error', err);
|
||
alert("Échec de l'envoi. Merci de réessayer ou d'envoyer un email à " + to);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ================================
|
||
MAIN
|
||
================================= */
|
||
|
||
// Nouvelle fonction loadAndRender pour charger et afficher les données du batch
|
||
async function loadAndRender(batchId){
|
||
// 1) Batch + contrats associés
|
||
const batch = await getBatchByBatchId(batchId);
|
||
if(!batch){ qs('#batch-sub').textContent = 'Batch introuvable'; qs('#cards').innerHTML=''; qs('#empty').style.display='block'; return; }
|
||
// expose batch globally for contact webhook
|
||
window.__batchCache = batch;
|
||
|
||
const linked = (batch.fields?.contrats || []).map(x=> (typeof x==='string'? x : x.id));
|
||
const contrats = await getContratsForBatch(batchId, linked);
|
||
|
||
// 2) Pour chaque contrat → récupérer la soumission employeur via template_id (si pas déjà en base)
|
||
for(const rec of contrats){
|
||
const f = rec.fields || {};
|
||
let embed = null;
|
||
|
||
if (f.embed_src_employeur) {
|
||
embed = f.embed_src_employeur;
|
||
} else if (f.docuseal_template_id) {
|
||
try {
|
||
const sub = await getEmployerSubmissionByTemplate(f.docuseal_template_id);
|
||
embed = sub?.embed_src || null;
|
||
} catch (e) {
|
||
console.warn('DocuSeal template->submission', e);
|
||
}
|
||
}
|
||
|
||
if (embed) {
|
||
console.log('[Hub] Embed URL for', f.Reference || rec.id, '=>', embed);
|
||
}
|
||
|
||
rec.fields._employer_embed = embed || null; // champ virtuel pour rendu
|
||
}
|
||
|
||
// 3) Stats + rendu
|
||
const total = contrats.length;
|
||
const employerSigned = (r) => String(r.fields?.['Contrat signé par employeur']||'').toLowerCase()==='oui';
|
||
const salarieSigned = (r) => String(r.fields?.['Contrat signé']||'').toLowerCase()==='oui';
|
||
|
||
const done = contrats.filter(employerSigned).length; // signés par employeur
|
||
const empTodo = contrats.filter(r => employerSigned(r) && !salarieSigned(r)).length; // en attente salarié (après signature employeur)
|
||
const finished = contrats.filter(r => employerSigned(r) && salarieSigned(r)).length; // tous signés
|
||
const ready = contrats.filter(r => {
|
||
const f = r.fields || {};
|
||
return !!f._employer_embed && !employerSigned(r); // à signer par l'employeur
|
||
}).length;
|
||
|
||
renderBatchHeader(batch, total, ready, done, empTodo, finished);
|
||
|
||
const cardsEl = qs('#cards');
|
||
if(!contrats.length){ qs('#empty').style.display='block'; cardsEl.innerHTML=''; window.__contratsCache = contrats; return; }
|
||
qs('#empty').style.display='none';
|
||
cardsEl.innerHTML = contrats.map(cardTemplate).join('');
|
||
bindCardActions(contrats);
|
||
bindContactActions(contrats);
|
||
|
||
// Met en cache pour le filtre
|
||
window.__contratsCache = contrats;
|
||
|
||
// Met en cache pour le filtre
|
||
window.__contratsCache = contrats;
|
||
}
|
||
|
||
// Helpers overlay et polling
|
||
function showPollOverlay(){
|
||
const el = qs('#poll-timeout-overlay');
|
||
if (el) el.style.display = 'block';
|
||
setSyncStatus('paused');
|
||
}
|
||
function hidePollOverlay(){
|
||
const el = qs('#poll-timeout-overlay');
|
||
if (el) el.style.display = 'none';
|
||
setSyncStatus('active');
|
||
}
|
||
function stopPolling(){
|
||
if (window.__pollTimer) { clearInterval(window.__pollTimer); window.__pollTimer = null; }
|
||
if (window.__pollStopTimer) { clearTimeout(window.__pollStopTimer); window.__pollStopTimer = null; }
|
||
window.__pollBusy = false;
|
||
}
|
||
|
||
function armPollStopTimer(){
|
||
if (!(CONFIG.POLL_MAX_MS && CONFIG.POLL_MAX_MS > 0)) return;
|
||
if (window.__pollStopTimer) clearTimeout(window.__pollStopTimer);
|
||
window.__pollStopTimer = setTimeout(()=>{ stopPolling(); showPollOverlay(); }, CONFIG.POLL_MAX_MS);
|
||
}
|
||
|
||
// Auto resume helper: hides overlay, refreshes once, restarts polling
|
||
async function autoResume(batchId){
|
||
if (!batchId) batchId = window.__batchId;
|
||
if (!batchId) return;
|
||
hidePollOverlay();
|
||
try { await refreshNow(batchId, 'Reconnexion en cours…'); } catch(e){ console.warn(e); }
|
||
startPolling(batchId);
|
||
}
|
||
|
||
// Polling starter qui évite les requêtes superposées, armement timer d'arrêt
|
||
function startPolling(batchId){
|
||
stopPolling(); // nettoie d'abord
|
||
window.__pollBusy = false;
|
||
window.__pollTimer = setInterval(async () => {
|
||
if (window.__pollBusy) return;
|
||
window.__pollBusy = true;
|
||
try { await loadAndRender(batchId); } catch(e){ console.warn('poll error', e); }
|
||
finally { window.__pollBusy = false; }
|
||
}, CONFIG.POLL_INTERVAL_MS || 10000);
|
||
|
||
// ⏳ arme/ réarme le timer d’arrêt
|
||
armPollStopTimer();
|
||
setSyncStatus('active');
|
||
}
|
||
|
||
async function main(){
|
||
initTheme();
|
||
const { batchId } = paramsFromUrl();
|
||
window.__batchId = batchId; // expose batchId for overlay click-to-resume
|
||
if(!batchId){ alert('Paramètre batchId manquant'); return; }
|
||
|
||
showPageLoader('Connexion aux serveurs Odentas…');
|
||
// Premier rendu
|
||
await loadAndRender(batchId);
|
||
hidePageLoader();
|
||
|
||
|
||
// Theme toggle binding (checkbox)
|
||
const tbtn = qs('#theme-toggle');
|
||
if (tbtn && !tbtn.__bound){
|
||
tbtn.__bound = true;
|
||
tbtn.addEventListener('change', ()=>{
|
||
const next = tbtn.checked ? 'light' : 'dark';
|
||
applyTheme(next);
|
||
localStorage.setItem('hub_theme', next);
|
||
});
|
||
}
|
||
|
||
// Overlay actions
|
||
const btnReload = qs('#btn-reload');
|
||
if (btnReload && !btnReload.__bound){
|
||
btnReload.__bound = true;
|
||
btnReload.addEventListener('click', ()=> location.reload());
|
||
}
|
||
const btnResume = qs('#btn-resume-poll');
|
||
if (btnResume && !btnResume.__bound){
|
||
btnResume.__bound = true;
|
||
btnResume.addEventListener('click', async ()=>{
|
||
hidePollOverlay();
|
||
try { await refreshNow(batchId, 'Reconnexion en cours…'); } catch(e){ console.warn(e); }
|
||
startPolling(batchId); // relance + réarmement du stop timer
|
||
});
|
||
}
|
||
|
||
// Clique n'importe où sur l'overlay (ou dans son contenu) pour réactiver la synchro
|
||
const ov = qs('#poll-timeout-overlay');
|
||
if (ov && !ov.__boundClickAnywhere){
|
||
ov.__boundClickAnywhere = true;
|
||
ov.addEventListener('click', async () => {
|
||
hidePollOverlay();
|
||
const bid = window.__batchId || batchId;
|
||
try { await refreshNow(bid, 'Reconnexion en cours…'); } catch(e){ console.warn(e); }
|
||
if (bid) startPolling(bid);
|
||
});
|
||
}
|
||
|
||
// Ajoute aussi une reprise au clavier (touche quelconque) tant que l’overlay est visible.
|
||
const resumeOnAnyKey = (e) => {
|
||
const el = qs('#poll-timeout-overlay');
|
||
if (!el || el.style.display === 'none') return;
|
||
hidePollOverlay();
|
||
if (window.__batchId) startPolling(window.__batchId);
|
||
};
|
||
if (!window.__resumeKeyBound){
|
||
window.__resumeKeyBound = true;
|
||
window.addEventListener('keydown', resumeOnAnyKey);
|
||
}
|
||
|
||
// 🔄 Réarme sur activité utilisateur (si l’overlay n’est pas affiché)
|
||
const resetOnActivity = () => {
|
||
const ov = qs('#poll-timeout-overlay');
|
||
if (!ov || ov.style.display === 'none') armPollStopTimer();
|
||
};
|
||
['click','keydown','mousemove','touchstart','focus'].forEach(evt => {
|
||
window.addEventListener(evt, resetOnActivity, { passive: true });
|
||
});
|
||
document.addEventListener('visibilitychange', async () => {
|
||
if (document.visibilityState === 'hidden') {
|
||
// Pause as soon as the tab is hidden
|
||
stopPolling();
|
||
showPollOverlay();
|
||
} else {
|
||
// Tab becomes visible again -> auto resume
|
||
await autoResume(window.__batchId || batchId);
|
||
}
|
||
});
|
||
window.addEventListener('blur', () => {
|
||
// If user switches window/app, pause to avoid useless traffic
|
||
stopPolling();
|
||
showPollOverlay();
|
||
});
|
||
window.addEventListener('focus', async () => {
|
||
// Coming back to the window -> auto resume
|
||
await autoResume(window.__batchId || batchId);
|
||
});
|
||
|
||
// Démarre le polling périodique
|
||
startPolling(batchId);
|
||
}
|
||
|
||
main().catch(err=>{
|
||
console.error(err);
|
||
hidePageLoader();
|
||
alert('Erreur: '+err.message);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |