bonpoint/web/public/parent.html
jmartin c4ebb5b2d8 Initialise le dépôt Bonpoint avec docs et prototype web.
Structure docs/, web/ (app Node existante) et mobile/ (placeholder Flutter).
2026-06-12 11:39:51 +02:00

369 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#667eea">
<title>Espace Parents — Bons Points</title>
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Bons Points">
<meta name="mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="/css/style.css?v=40">
</head>
<body class="parent-page">
<div class="parent-container">
<div id="ecran-pin">
<a href="/" class="btn-retour">
<svg class="btn-retour-icone" width="18" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="15 18 9 12 15 6"/>
</svg>
Retour
</a>
<div class="ecran-pin-carte">
<div class="pin-entete">
<div class="avatar-cercle avatar-parent-pin" aria-hidden="true">
<img src="/photos/parents.png" alt="">
</div>
<h1>Espace Parents</h1>
<p class="pin-sous-titre">Entrez votre code PIN</p>
</div>
<div class="pin-zone">
<form id="form-pin" autocomplete="off" onsubmit="return false">
<input type="text" name="decoy-user" class="pin-leurre"
tabindex="-1" autocomplete="username" aria-hidden="true">
<input type="password" name="decoy-pass" class="pin-leurre"
tabindex="-1" autocomplete="current-password" aria-hidden="true">
<input type="text" id="pin-input" class="pin-input pin-masque"
name="bonpoint-code-parent"
inputmode="numeric" pattern="[0-9]*" maxlength="8"
placeholder="••••"
autocomplete="one-time-code"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
data-lpignore="true"
data-1p-ignore="true"
data-form-type="other"
readonly>
</form>
<button class="btn btn-principal btn-connexion-pin" id="btn-connexion">
Connexion
</button>
</div>
<p id="erreur-pin" class="erreur cacher"></p>
</div>
</div>
<div id="ecran-app" class="cacher">
<div class="parent-barre-haut">
<h1 class="parent-titre">
<div class="avatar-cercle avatar-parent-titre" aria-hidden="true">
<img src="/photos/parents.png" alt="">
</div>
Espace Parents
</h1>
<button class="btn btn-outline-blanc" id="btn-deconnexion">Déconnexion</button>
</div>
<div class="section section-enfant">
<h3>Enfant sélectionné</h3>
<div class="selecteur-enfant" id="selecteur-enfant"></div>
<div id="score-bandeau" class="score-bandeau"></div>
</div>
<div class="section">
<h3>Appliquer une règle</h3>
<div id="regles-par-famille"></div>
</div>
<div class="section">
<h3>Derniers mouvements</h3>
<div id="historique"></div>
</div>
</div>
</div>
<div id="modal-annuler" class="modal">
<div class="modal-contenu">
<p id="modal-annuler-texte"></p>
<div class="modal-boutons">
<button class="btn btn-principal" id="btn-confirmer-annuler">Oui, annuler</button>
<button class="btn btn-secondaire" id="btn-fermer-annuler">Non</button>
</div>
</div>
</div>
<div id="feedback" class="feedback"></div>
<script>
const PHOTOS = {
Ariana: '/photos/ariana.png',
Pablo: '/photos/pablo.png',
'Hélia': '/photos/helia.png',
};
let enfants = [];
let regles = [];
let enfantSelectionne = null;
const ecranPin = document.getElementById('ecran-pin');
const ecranApp = document.getElementById('ecran-app');
const pinInput = document.getElementById('pin-input');
const erreurPin = document.getElementById('erreur-pin');
const feedback = document.getElementById('feedback');
const modalAnnuler = document.getElementById('modal-annuler');
let mouvementAAnnuler = null;
function afficherFeedback(msg) {
feedback.textContent = msg;
feedback.classList.add('visible');
setTimeout(() => feedback.classList.remove('visible'), 2000);
}
async function verifierSession() {
try {
const res = await fetch('/api/parent/session');
if (res.ok) {
ecranPin.classList.add('cacher');
ecranApp.classList.remove('cacher');
await chargerDonnees();
}
} catch { /* pas connecté */ }
}
async function connexion() {
erreurPin.classList.add('cacher');
const pin = pinInput.value;
const res = await fetch('/api/parent/connexion', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin }),
});
if (!res.ok) {
const data = await res.json();
erreurPin.textContent = data.erreur || 'Erreur';
erreurPin.classList.remove('cacher');
return;
}
ecranPin.classList.add('cacher');
ecranApp.classList.remove('cacher');
pinInput.value = '';
await chargerDonnees();
}
async function deconnexion() {
await fetch('/api/parent/deconnexion', { method: 'POST' });
window.location.href = '/';
}
async function chargerDonnees() {
const res = await fetch('/api/parent/donnees');
const data = await res.json();
enfants = data.enfants;
regles = data.regles;
if (!enfantSelectionne && enfants.length > 0) {
enfantSelectionne = enfants[0].id;
}
afficherSelecteur();
afficherRegles();
afficherHistorique(data.mouvements);
}
function afficherSelecteur() {
const cont = document.getElementById('selecteur-enfant');
cont.innerHTML = enfants.map((e) => `
<button class="chip-enfant ${e.id === enfantSelectionne ? 'actif' : ''}"
data-id="${e.id}">
<div class="chip-vignette" style="border-color:${e.couleur}">
<img src="${PHOTOS[e.prenom]}" alt="${e.prenom}">
<span class="chip-badge" style="color:${e.couleur}">${e.score}</span>
</div>
</button>
`).join('');
cont.querySelectorAll('.chip-enfant').forEach((btn) => {
btn.addEventListener('click', () => {
enfantSelectionne = parseInt(btn.dataset.id, 10);
afficherSelecteur();
});
});
mettreAJourBandeau();
}
function mettreAJourBandeau() {
const e = enfants.find((x) => x.id === enfantSelectionne);
const bandeau = document.getElementById('score-bandeau');
if (!e) {
bandeau.innerHTML = '';
return;
}
bandeau.innerHTML = `
<span class="score-bandeau-nom">${e.prenom}</span>
<span class="score-bandeau-pts" style="color:${e.couleur}">
${e.score} points
</span>
`;
bandeau.style.borderColor = e.couleur;
}
function afficherRegles() {
const cont = document.getElementById('regles-par-famille');
const groupes = [];
let derniereFamille = null;
for (const r of regles) {
if (r.famille !== derniereFamille) {
groupes.push({ famille: r.famille, regles: [] });
derniereFamille = r.famille;
}
groupes[groupes.length - 1].regles.push(r);
}
cont.innerHTML = groupes.map((g) => `
<div class="famille-regles">
<h4 class="famille-titre">${g.famille}</h4>
<div class="grille-regles">
${g.regles.map((r) => `
<button class="btn-regle ${r.points >= 0 ? 'positif' : 'negatif'}"
data-id="${r.id}">
${r.icone} ${r.libelle}<br>
<strong>${r.points > 0 ? '+' : ''}${r.points}</strong>
</button>
`).join('')}
</div>
</div>
`).join('');
cont.querySelectorAll('.btn-regle').forEach((btn) => {
btn.addEventListener('click', () => appliquerRegle(
parseInt(btn.dataset.id, 10),
));
});
}
async function appliquerRegle(regleId) {
const res = await fetch('/api/parent/appliquer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enfant_id: enfantSelectionne,
regle_id: regleId,
}),
});
if (!res.ok) {
afficherFeedback('Erreur');
return;
}
const data = await res.json();
const idx = enfants.findIndex((e) => e.id === data.enfant.id);
if (idx >= 0) enfants[idx] = data.enfant;
afficherSelecteur();
await chargerDonnees();
afficherFeedback(`${data.enfant.prenom} : ${data.enfant.score} pts !`);
}
function afficherHistorique(mouvements) {
const cont = document.getElementById('historique');
if (!mouvements.length) {
cont.innerHTML = '<p class="historique-vide">Aucun mouvement</p>';
return;
}
cont.innerHTML = mouvements.map((m) => {
const date = new Date(m.cree_le + 'Z').toLocaleString('fr-FR', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
});
const cls = m.delta >= 0 ? 'delta-positif' : 'delta-negatif';
const signe = m.delta > 0 ? '+' : '';
const libelle = m.recompense || m.regle || m.note || '?';
const icone = m.icone_recompense || m.icone || '';
return `
<div class="historique-item">
<span class="historique-libelle">${icone} ${m.enfant}${libelle}</span>
<span class="historique-droite">
<span class="historique-points-col">
<span class="${cls}">${signe}${m.delta}</span>
<span class="historique-date">${date}</span>
</span>
<button class="btn-annuler-mouvement" data-id="${m.id}"
data-enfant="${m.enfant}" data-libelle="${libelle}"
data-delta="${m.delta}" title="Annuler ce mouvement"
aria-label="Annuler ce mouvement">✕</button>
</span>
</div>
`;
}).join('');
cont.querySelectorAll('.btn-annuler-mouvement').forEach((btn) => {
btn.addEventListener('click', () => ouvrirModalAnnuler(btn));
});
}
function ouvrirModalAnnuler(btn) {
const delta = parseInt(btn.dataset.delta, 10);
const signe = delta > 0 ? '+' : '';
mouvementAAnnuler = parseInt(btn.dataset.id, 10);
document.getElementById('modal-annuler-texte').textContent =
`Annuler « ${btn.dataset.libelle} » pour ${btn.dataset.enfant} (${signe}${delta}) ? Les points seront recalculés.`;
modalAnnuler.classList.add('ouvert');
}
function fermerModalAnnuler() {
modalAnnuler.classList.remove('ouvert');
mouvementAAnnuler = null;
}
async function confirmerAnnulerMouvement() {
if (!mouvementAAnnuler) return;
const res = await fetch('/api/parent/annuler-mouvement', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mouvement_id: mouvementAAnnuler }),
});
fermerModalAnnuler();
if (!res.ok) {
afficherFeedback('Impossible d\'annuler');
return;
}
await chargerDonnees();
afficherFeedback('Mouvement annulé');
}
document.getElementById('btn-connexion').addEventListener('click', connexion);
pinInput.addEventListener('focus', () => pinInput.removeAttribute('readonly'));
pinInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') connexion();
});
document.getElementById('btn-deconnexion').addEventListener('click', deconnexion);
document.getElementById('btn-confirmer-annuler')
.addEventListener('click', confirmerAnnulerMouvement);
document.getElementById('btn-fermer-annuler')
.addEventListener('click', fermerModalAnnuler);
modalAnnuler.addEventListener('click', (e) => {
if (e.target === modalAnnuler) fermerModalAnnuler();
});
verifierSession();
</script>
<script src="/js/pwa.js" defer></script>
</body>
</html>