V7.10 — Tableau de bord, guide, semaine type et enveloppe de formation

Reprise dans le répertoire de production du travail mené dans le worktree
ux-service-enfance :

- tableau de bord Service Enfance (dashboard.php) et navigation regroupée ;
- guide d'utilisation (faq.php) accessible depuis toute la navigation ;
- éditeur de semaine type mis en avant sur l'écran de saisie ;
- droit à l'enveloppe de formation par agent, en remplacement de la case
  « formation déjà prévue dans la répartition annuelle » ;
- correctif de « [object Object] » sur les cartes de quota du PTA ;
- motif de travail par défaut à la saisie, heures travaillées complètes
  dans les PDF.

Le docker-compose.yml de production est conservé : celui du worktree décrit
une pile de test (conteneurs et volume suffixés « test », ports 9080/9081),
qui aurait fait redémarrer l'application sur un volume MariaDB vide.

Les migrations database/migration_v7_9*.sql sont obligatoires ; le service
db-migrate les applique au déploiement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
PTA
2026-08-17 16:53:04 +02:00
parent 57bd8da5c8
commit 4201894e28
69 changed files with 5228 additions and 246 deletions

View File

@@ -110,8 +110,37 @@
updateAgentPdfButton();
}
/**
* Motif proposé par défaut sur une nouvelle plage : le temps de travail, très
* majoritaire à la saisie. On se rabat sur le premier motif décomptant le quota
* si le référentiel ne contient pas le code TRAVAIL.
*/
function defaultMotifId() {
const motifs = window.PTA_CONFIG?.motifs || [];
const motif = motifs.find((item) => String(item.code).toUpperCase() === 'TRAVAIL')
|| motifs.find((item) => item.quota)
|| motifs[0];
return motif ? Number(motif.id) : null;
}
/**
* Un agent sans droit à l'enveloppe de formation ne peut pas recevoir le motif
* « Formation ». Le droit vaut tant que l'agent n'est pas chargé : on ne retire
* jamais une possibilité sur une donnée absente.
*/
function agentHasTrainingRight() {
return Number(state.currentAgent?.droit_enveloppe_formation ?? 1) !== 0;
}
function isMotifAllowedForAgent(motif) {
return agentHasTrainingRight() || String(motif.code).toUpperCase() !== 'FORMATION';
}
function motifOptionsHtml(selectedMotifId = null) {
return (window.PTA_CONFIG?.motifs || [])
// Un motif interdit reste proposé s'il est déjà celui de la plage :
// le retirer changerait silencieusement le motif d'une saisie existante.
.filter((motif) => isMotifAllowedForAgent(motif) || Number(motif.id) === Number(selectedMotifId))
.map((motif) => {
const selected = Number(motif.id) === Number(selectedMotifId) ? ' selected' : '';
const quotaLabel = motif.quota ? ' · décompte quota' : ' · hors quota';
@@ -122,13 +151,16 @@
function timeRangeHtml(dayLabel, index = 0, selectedMotifId = null) {
const suffix = `${dayLabel} plage ${index + 1}`;
const fallbackMotifId = defaultMotifId();
const motifId = selectedMotifId ?? fallbackMotifId;
const placeholder = fallbackMotifId === null ? '<option value="">Choisir un motif</option>' : '';
return `
<div class="weekly-time-range">
<label class="weekly-range-motif-field">
<span>Motif</span>
<select class="weekly-range-motif" aria-label="Motif ${escapeHtml(suffix)}">
<option value="">Choisir un motif</option>
${motifOptionsHtml(selectedMotifId)}
${placeholder}
${motifOptionsHtml(motifId)}
</select>
</label>
<label>
@@ -649,6 +681,7 @@
state.entries = [];
state.otherStructureEntries = [];
state.currentStructure = null;
state.currentAgent = null;
state.planningId = null;
state.quota = null;
state.savedCurrentCountedMinutes = 0;
@@ -710,6 +743,7 @@
state.entries = [];
state.otherStructureEntries = [];
state.currentStructure = null;
state.currentAgent = null;
state.planningId = null;
state.savedCurrentCountedMinutes = 0;
setStatus(null);
@@ -728,6 +762,7 @@
state.quota = data.quota || null;
state.currentStructure = data.current_structure || null;
state.currentAgent = data.agent || null;
const calendarByDate = new Map((data.calendar_days || []).map((day) => [day.date, day]));
state.weekDays = state.weekDays.map((day) => ({ ...day, ...(calendarByDate.get(day.date) || {}) }));
state.otherStructureEntries = (data.other_entries || []).map((entry) => ({
@@ -819,6 +854,11 @@
return;
}
if (!isMotifAllowedForAgent(motif)) {
errors.push(`${day?.label || row.dataset.date} ${label} : le motif « ${motif.label} » n’est pas ouvert à cet agent, qui n’a pas droit à l’enveloppe de formation.`);
return;
}
if (!start || !end) {
errors.push(`${day?.label || row.dataset.date} ${label} : renseignez l’heure de début et l’heure de fin.`);
return;