Files
pta/assets/js/teams.js
2026-08-27 11:38:59 +02:00

181 lines
10 KiB
JavaScript
Raw 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.
/**
* Équipes des mercredis et des vacances (`PTA.modules.teams`), écran teams.php.
*
* Une équipe rassemble des agents sur un lieu et une période, avec des règles
* d'encadrement propres à l'accueil collectif de mineurs (effectifs par tranche
* d'âge, taux d'encadrement, proportion d'agents diplômés). Le serveur calcule
* l'effectif requis et les contrôles ; ce module se contente de les afficher.
*
* Les contrôles suivent la forme commune {niveau, code, message}. AUCUN n'est
* bloquant : ils signalent, l'arbitrage reste humain.
*
* La page se recharge intégralement (`load()`) après chaque modification :
* ajouter ou retirer un membre change les contrôles de toute l'équipe.
*/
(() => {
'use strict';
const PTA = window.PTA;
// core.js absent : la page reste lisible, simplement non interactive.
if (!PTA) return;
const { apiJson, escapeHtml, formatDate } = PTA.utils;
const el = (id) => document.getElementById(id);
/** Message dans le bandeau de la page. */
function message(text, type = 'info') {
const box = el('team-list-message');
if (!box) return;
box.className = `message message-${type}`;
box.textContent = text;
}
/** Rend un contrôle, sa classe CSS dérivant de son niveau. */
function controlHtml(control) {
const level = String(control.niveau || 'INFO').toLowerCase();
return `<div class="control-item control-${escapeHtml(level)}"><strong>${escapeHtml(control.niveau)}</strong><span>${escapeHtml(control.message)}</span></div>`;
}
/** Carte complète d'une équipe : paramètres, membres, contrôles, actions. */
function teamHtml(team) {
const options = el('team-agent-options')?.innerHTML || '';
const members = (team.members || []).map((member) => `
<tr>
<td><strong>${escapeHtml(member.prenom)} ${escapeHtml(member.nom)}</strong><br><small>${escapeHtml(member.poste_libelle || 'Poste non renseigné')}</small></td>
<td>${member.est_diplome ? `Diplômé${member.diplome_libelle ? ` — ${escapeHtml(member.diplome_libelle)}` : ''}` : 'Non diplômé'}</td>
<td>${escapeHtml(member.fonction_equipe)}</td>
<td>${escapeHtml(member.structure_principale_nom || 'Sans lieu principal')}</td>
<td><button type="button" class="button button-danger remove-team-member" data-agent-id="${Number(member.id_agent)}">Retirer</button></td>
</tr>
`).join('') || '<tr><td colspan="5">Aucun agent affecté.</td></tr>';
return `
<article class="card team-card" data-team-id="${Number(team.id_equipe)}">
<div class="section-heading">
<div><h3>${escapeHtml(team.libelle)}</h3><p class="section-subtitle">${escapeHtml(team.structure_nom)} — ${formatDate(team.date_debut)} au ${formatDate(team.date_fin)} — ${escapeHtml(team.type_equipe)}</p></div>
<select class="team-status" aria-label="Statut"><option value="BROUILLON" ${team.statut === 'BROUILLON' ? 'selected' : ''}>Brouillon</option><option value="A_CONTROLER" ${team.statut === 'A_CONTROLER' ? 'selected' : ''}>À contrôler</option><option value="VALIDEE" ${team.statut === 'VALIDEE' ? 'selected' : ''}>Validée</option></select>
</div>
<div class="summary-grid compact-summary">
<article class="summary-card"><span>Agents requis</span><strong>${Number(team.agents_requis)}</strong></article>
<article class="summary-card"><span>Agents affectés</span><strong>${Number(team.agents_affectes)}</strong></article>
<article class="summary-card"><span>Diplômés</span><strong>${Number(team.agents_diplomes || 0)}</strong></article>
<article class="summary-card"><span>Enfants</span><strong>${Number(team.nombre_enfants_moins_6) + Number(team.nombre_enfants_6_plus)}</strong></article>
</div>
<div class="control-list">${(team.controls || []).map(controlHtml).join('')}</div>
<form class="team-member-form inline-form">
<label class="field"><span>Agent</span><select class="team-member-agent" required><option value="">Choisir un agent</option>${options}</select></label>
<label class="field"><span>Fonction dans l’équipe</span><select class="team-member-function"><option value="RESPONSABLE">Responsable</option><option value="ANIMATION">Animation</option><option value="RESTAURATION_ENTRETIEN">Restauration / entretien</option></select></label>
<label class="field"><span>Commentaire</span><input class="team-member-comment" type="text" maxlength="500"></label>
<button class="button button-primary" type="submit">Ajouter</button>
</form>
<div class="table-scroll"><table class="planning-table"><thead><tr><th>Agent</th><th>Qualification</th><th>Fonction</th><th>Lieu principal</th><th>Action</th></tr></thead><tbody>${members}</tbody></table></div>
</article>`;
}
/** Recharge toutes les équipes de la période filtrée. */
async function load() {
const start = el('team-filter-start')?.value || '';
const end = el('team-filter-end')?.value || '';
const query = new URLSearchParams();
if (start) query.set('date_debut', start);
if (end) query.set('date_fin', end);
message('Chargement des équipes…');
try {
const data = await apiJson(`api/teams.php?${query.toString()}`);
el('team-list').innerHTML = (data.teams || []).map(teamHtml).join('') || '<div class="empty-state">Aucune équipe sur cette période.</div>';
message(`${(data.teams || []).length} équipe(s) affichée(s).`, 'success');
} catch (error) { message(error.message, 'error'); }
}
/** Crée une équipe (toujours en BROUILLON), puis recharge la liste. */
async function create(event) {
event.preventDefault();
const box = el('team-create-message');
try {
const data = await apiJson('api/teams.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
structure_id: Number(el('team-structure').value),
type_equipe: el('team-type').value,
libelle: el('team-label').value,
date_debut: el('team-start').value,
date_fin: el('team-end').value,
nombre_enfants_moins_6: Number(el('team-under-six').value),
nombre_enfants_6_plus: Number(el('team-over-six').value),
ratio_moins_6: Number(el('team-ratio-under-six').value),
ratio_6_plus: Number(el('team-ratio-over-six').value),
minimum_agents: Number(el('team-min-agents').value),
pourcentage_diplomes_min: Number(el('team-qualified-rate').value),
commentaire: el('team-comment').value,
}),
});
box.className = 'message message-success'; box.textContent = data.message;
event.currentTarget.reset();
el('team-ratio-under-six').value = '8'; el('team-ratio-over-six').value = '12'; el('team-min-agents').value = '1'; el('team-qualified-rate').value = '50';
await load();
} catch (error) { box.className = 'message message-error'; box.textContent = error.message; }
}
/**
* Ajout et retrait de membres, par délégation sur la liste des équipes —
* elle est reconstruite à chaque chargement.
*/
async function handle(event) {
const card = event.target.closest('.team-card');
if (!card) return;
const teamId = Number(card.dataset.teamId);
if (event.type === 'submit' && event.target.matches('.team-member-form')) {
event.preventDefault();
try {
await apiJson('api/team_members.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken, team_id: teamId,
agent_id: Number(card.querySelector('.team-member-agent').value),
fonction_equipe: card.querySelector('.team-member-function').value,
commentaire: card.querySelector('.team-member-comment').value,
}) });
await load();
} catch (error) { message(error.message, 'error'); }
return;
}
const remove = event.target.closest('.remove-team-member');
if (remove) {
if (!window.confirm('Retirer cet agent de l’équipe ?')) return;
try {
await apiJson('api/team_members.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken, action: 'REMOVE', team_id: teamId, agent_id: Number(remove.dataset.agentId),
}) });
await load();
} catch (error) { message(error.message, 'error'); }
}
}
/**
* Fait avancer une équipe dans son cycle de vie
* (BROUILLON → A_CONTROLER → VALIDEE). Aucune transition n'est imposée : le
* suivi est indicatif, pas un verrou.
*/
async function status(event) {
const select = event.target.closest('.team-status');
if (!select) return;
const card = select.closest('.team-card');
try {
await apiJson('api/team_status.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken, team_id: Number(card.dataset.teamId), statut: select.value,
}) });
await load();
} catch (error) { message(error.message, 'error'); }
}
function init() {
const params = window.PTA_PAGE_CONFIG?.params || {};
if (params.dateDebut) el('team-filter-start').value = params.dateDebut;
if (params.dateFin) el('team-filter-end').value = params.dateFin;
el('team-create-form')?.addEventListener('submit', create);
el('load-teams')?.addEventListener('click', load);
el('team-list')?.addEventListener('submit', handle);
el('team-list')?.addEventListener('click', handle);
el('team-list')?.addEventListener('change', status);
load();
}
PTA.modules.teams = { init, load };
})();