369 lines
12 KiB
HTML
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>
|