Files
Presencia/frontend/public/index.html
Claude 3df521ac30 Revue complète : sécurité, optimisations et UI/UX mobile
Sécurité
- JWT : un JWT_SECRET d'exemple ou trop court est ignoré au profit d'un
  secret aléatoire conservé en base (avant, la valeur publique de
  docker-compose.yml permettait de forger un jeton admin).
- Le compte est relu en base à chaque requête : désactivation, changement
  de rôle et réinitialisation du mot de passe prennent effet immédiatement.
- Connexion : 10 échecs max par e-mail / 15 min, temps constant que l'e-mail
  existe ou non, mot de passe admin retiré des logs.
- Validation des entrées (ids, dates, mois, longueurs, e-mail) : 400 au
  lieu de 500. Un admin ne peut plus se désactiver ni se rétrograder.
- Mots de passe : 8 caractères minimum ; changement en libre-service.
- Export : nom de fichier conforme RFC 5987 (un nom de société avec « — »
  faisait planter l'export).
- esc() échappe aussi les guillemets (injection d'attributs HTML).
- En-têtes CSP / X-Frame-Options / nosniff (Nginx + API), API et Postgres
  publiés sur 127.0.0.1 seulement, image backend non-root via npm ci.

Optimisations
- Export : une requête pour tout le mois au lieu d'une par cadre.
- Saisie groupée : un seul INSERT (unnest), doublons dédupliqués.
- Dates renvoyées en chaînes (plus de décalage d'un jour selon le TZ).
- Planning : chaque clic met à jour l'affichage localement au lieu de
  recharger le mois ; les réponses de mois périmées sont ignorées.
- Recherche Utilisateurs / Plannings filtrée localement (plus une requête
  par touche) ; statuts des sociétés chargés en parallèle.

UI/UX
- Mobile : toutes les destinations dans la barre basse (Sociétés et Ma
  saisie étaient inaccessibles à l'admin) ; tableaux affichés en cartes
  (les boutons d'action étaient hors écran).
- Boutons désactivés pendant l'envoi, focus et Échap dans les dialogues,
  retour à l'écran de connexion quand la session est révoquée, erreurs
  d'export affichées au lieu d'un fichier JSON téléchargé, actions
  destructives signalées, confirmation avant désactivation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5Bdayziw6tybgqETZoNSt
2026-10-09 07:17:54 +00:00

296 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Presencia — Gestion des présences</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='22' fill='%233f6b54'/%3E%3C/svg%3E" />
<link rel="stylesheet" href="css/inter.css" />
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/presencia.css" />
<link rel="stylesheet" href="css/phosphor.css" />
<link rel="stylesheet" href="css/app.css" />
</head>
<body>
<!-- ══════════════ LOGIN ══════════════ -->
<section id="view-login" class="login-split">
<div class="login-pitch">
<div class="brand-row">
<span class="brand-mark"></span>
<span class="brand-name">Presencia</span>
</div>
<div class="login-pitch-copy">
<h1>Les présences,<br />sans le tableur.</h1>
<p class="text-muted">Saisie par demi-journée, validation en deux temps, export mensuel par société. Chaque cadre remplit son mois en moins de deux minutes.</p>
</div>
<div class="login-pitch-feet">
<span>Validation cadre puis administrateur</span>
<span>Export PDF &amp; Excel</span>
</div>
</div>
<div class="login-formside">
<form id="login-form" class="login-form" autocomplete="on">
<div>
<h3>Connexion</h3>
<p class="login-lede">Accès réservé — les comptes sont créés par un administrateur.</p>
</div>
<div class="field">
<label for="login-email">Adresse e-mail</label>
<input class="input" id="login-email" type="email" required autocomplete="username" placeholder="vous@societe.com" />
</div>
<div class="field">
<label for="login-password">Mot de passe</label>
<input class="input" id="login-password" type="password" required autocomplete="current-password" placeholder="••••••••" />
</div>
<div id="login-error" class="login-error" hidden></div>
<button type="submit" class="btn btn-primary btn-block">Se connecter</button>
</form>
</div>
</section>
<!-- ══════════════ APP ══════════════ -->
<div id="app" class="shell" hidden>
<aside class="side">
<div class="side-brand">
<span class="brand-mark small"></span>
<span class="brand-name small">Presencia</span>
</div>
<nav id="side-nav"></nav>
<div class="side-foot">
<div class="side-user">
<span id="user-initials" class="avatar"></span>
<span class="side-user-text">
<span id="user-name" class="side-user-name"></span>
<span id="user-meta" class="side-user-meta"></span>
</span>
</div>
<button id="my-password-btn" class="navitem"><i class="ni-ico ph ph-lock-simple"></i><span>Mon mot de passe</span></button>
<button id="logout-btn" class="navitem"><i class="ni-ico ph ph-sign-out"></i><span>Déconnexion</span></button>
</div>
</aside>
<main class="content">
<div class="mobile-only mobile-head">
<span class="brand-mark tiny"></span>
<span id="screen-title" class="mobile-title"></span>
<button id="my-password-btn-mobile" class="chipbtn chipbtn-icon" aria-label="Changer mon mot de passe" title="Changer mon mot de passe"><i class="ph ph-lock-simple"></i></button>
<button id="logout-btn-mobile" class="chipbtn" aria-label="Se déconnecter"><i class="ph ph-sign-out"></i>Quitter</button>
</div>
<!-- ─── DASHBOARD ─── -->
<section id="view-dashboard" class="view stack-20">
<div>
<h3 id="dash-greeting"></h3>
<p id="dash-subtitle" class="lede"></p>
</div>
<div id="dash-tiles" class="tile-grid"></div>
<div class="stack-8">
<h6>À faire maintenant</h6>
<div id="dash-actions" class="stack-6"></div>
</div>
<div id="dash-company-block" class="stack-8" hidden>
<h6>État du mois par société</h6>
<div class="card table-card">
<table class="table">
<thead><tr><th>Société</th><th>Cadres validés</th><th>Statut</th><th></th></tr></thead>
<tbody id="dash-company-rows"></tbody>
</table>
</div>
</div>
</section>
<!-- ─── PLANNING ─── -->
<section id="view-planning" class="view stack-14" hidden>
<div class="planning-head">
<div class="planning-head-title">
<button id="back-to-plannings" class="btn btn-ghost back-link" hidden>← Tous les plannings</button>
<h3 id="planning-title"></h3>
<p id="planning-subtitle" class="lede"></p>
</div>
<div class="month-nav">
<button id="prev-month" class="btn btn-secondary btn-icon" aria-label="Mois précédent"><i class="ph ph-caret-left"></i></button>
<span id="month-label" class="month-label"></span>
<button id="next-month" class="btn btn-secondary btn-icon" aria-label="Mois suivant"><i class="ph ph-caret-right"></i></button>
</div>
</div>
<div class="lockbar">
<i id="lock-icon" class="ph ph-pencil-simple-line lockbar-icon"></i>
<span class="lockbar-text">
<span id="lock-title" class="lockbar-title"></span>
<span id="lock-detail" class="lockbar-detail"></span>
</span>
<span class="lockbar-actions">
<button id="reopen-month-btn" class="btn btn-secondary" hidden>Réouvrir le mois</button>
<button id="validate-month-btn" class="btn btn-primary" hidden>Valider mon mois</button>
</span>
</div>
<div class="toolrow">
<span class="toolrow-label">Saisie rapide</span>
<div id="brush-options" class="toolrow-items"></div>
</div>
<div class="toolrow">
<div id="view-options" class="toolrow-items"></div>
<span class="toolrow-right">
<button id="fill-workdays-btn" class="btn btn-secondary">Remplir les jours ouvrés</button>
<button id="clear-month-btn" class="btn btn-secondary btn-danger-ghost">Tout effacer</button>
</span>
</div>
<div id="brush-hint" class="brush-hint"></div>
<div id="planning-view" class="pv-grid">
<div class="cal-head">
<span>Lun</span><span>Mar</span><span>Mer</span><span>Jeu</span><span>Ven</span><span>Sam</span><span>Dim</span>
</div>
<div id="cal-grid" class="cal-grid"></div>
</div>
<div id="planning-summary" class="summary-row"></div>
</section>
<!-- ─── PLANNINGS (admin) ─── -->
<section id="view-plannings" class="view stack-14" hidden>
<div>
<h3>Plannings des cadres</h3>
<p id="plannings-subtitle" class="lede"></p>
</div>
<div class="filter-bar">
<span class="field">
<label for="pl-co">Société</label>
<select class="input" id="pl-co"><option value="all">Toutes les sociétés</option></select>
</span>
<span class="field">
<label for="pl-q">Rechercher un cadre</label>
<input class="input" id="pl-q" placeholder="Nom ou e-mail" />
</span>
</div>
<div id="cadre-rows" class="stack-6"></div>
</section>
<!-- ─── VALIDATION & EXPORT (admin) ─── -->
<section id="view-validation" class="view stack-14" hidden>
<div>
<h3>Validation &amp; export</h3>
<p class="lede">Une société se valide quand tous ses cadres ont validé leur mois.</p>
</div>
<div class="filter-bar">
<span class="field">
<label for="va-co">Société</label>
<select class="input" id="va-co"></select>
</span>
<span class="field">
<label for="va-mo">Mois</label>
<select class="input" id="va-mo"></select>
</span>
</div>
<div class="card validation-card">
<div class="validation-head">
<span class="validation-head-text">
<span id="validation-company-name" class="validation-company"></span>
<span id="validation-status-text" class="validation-status"></span>
</span>
<span class="validation-head-actions">
<button id="export-pdf-btn" class="btn btn-secondary"><i class="ph ph-file-pdf"></i>PDF</button>
<button id="export-excel-btn" class="btn btn-secondary"><i class="ph ph-file-xls"></i>Excel</button>
<button id="validate-company-btn" class="btn btn-primary">Valider la société</button>
</span>
</div>
<div class="progress"><span id="validation-progress" class="progress-bar"></span></div>
<table class="table">
<thead><tr><th>Cadre</th><th>Demi-journées</th><th>Statut</th><th></th></tr></thead>
<tbody id="validation-rows"></tbody>
</table>
</div>
</section>
<!-- ─── COMPANIES (admin) ─── -->
<section id="view-companies" class="view stack-14" hidden>
<div class="page-head">
<div>
<h3>Sociétés</h3>
<p id="companies-count" class="lede"></p>
</div>
<button id="add-company-btn" class="btn btn-primary"><i class="ph ph-plus"></i>Nouvelle société</button>
</div>
<div class="card table-card">
<table class="table">
<thead><tr><th>Nom</th><th>Cadres actifs</th><th>Créée le</th><th></th></tr></thead>
<tbody id="companies-rows"></tbody>
</table>
</div>
</section>
<!-- ─── USERS (admin) ─── -->
<section id="view-users" class="view stack-14" hidden>
<div class="page-head">
<div>
<h3>Utilisateurs</h3>
<p class="lede">Aucune inscription publique — les comptes sont créés ici.</p>
</div>
<button id="add-user-btn" class="btn btn-primary"><i class="ph ph-plus"></i>Nouvel utilisateur</button>
</div>
<div class="filter-bar">
<span class="field">
<label for="us-co">Société</label>
<select class="input" id="us-co"><option value="all">Toutes les sociétés</option></select>
</span>
<span class="field">
<label for="us-q">Rechercher</label>
<input class="input" id="us-q" placeholder="Nom ou e-mail" />
</span>
</div>
<div class="card table-card">
<table class="table">
<thead><tr><th>Nom</th><th>E-mail</th><th>Rôle</th><th>Société</th><th>Statut</th><th></th></tr></thead>
<tbody id="users-rows"></tbody>
</table>
</div>
</section>
<!-- ─── HISTORY ─── -->
<section id="view-history" class="view stack-14" hidden>
<div>
<h3>Historique</h3>
<p class="lede">Vos mois saisis et leurs totaux.</p>
</div>
<div class="card table-card">
<table class="table">
<thead><tr><th>Mois</th><th>Présent</th><th>Non-présent</th><th>Congé</th><th>RTT</th><th>Statut</th><th></th></tr></thead>
<tbody id="history-rows"></tbody>
</table>
</div>
</section>
</main>
<nav id="mobile-nav" class="mobile-only mobile-nav"></nav>
</div>
<!-- ══════════════ POPOVER ══════════════ -->
<div id="pop-backdrop" class="pop-backdrop" hidden></div>
<div id="pop" class="pop" hidden>
<div class="pop-head">
<span id="pop-title" class="pop-title"></span>
<span class="pop-hint">1–4 au clavier</span>
</div>
<div id="pop-options"></div>
<button id="pop-clear" class="pop-opt pop-opt-muted"><i class="dot st-none"></i>Effacer<span class="kbd">⌫</span></button>
<button id="pop-whole-day" class="pop-opt"><i class="ph ph-arrows-out-line-horizontal pop-ico"></i>Appliquer à la journée<span class="kbd">↵</span></button>
</div>
<!-- ══════════════ DIALOG ══════════════ -->
<div id="dialog-backdrop" class="dialog-backdrop" hidden>
<div id="dialog" class="dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title"></div>
</div>
<!-- ══════════════ TOAST ══════════════ -->
<div id="toast" class="toast" aria-live="polite" hidden></div>
<script src="js/app.js"></script>
</body>
</html>