/**
* Éditeur de semaine type (`PTA.modules.weekTemplateEditor`, V7.10).
*
* Permet de COMPOSER un modèle lundi→vendredi directement, sans partir d'une
* semaine réellement planifiée. À distinguer de `week-templates.js`, qui
* enregistre et applique les modèles.
*
* MODES EXCLUSIFS DE planning.php — ce module pilote la bascule entre l'éditeur
* de semaine type (ouvert par défaut) et la saisie semaine par semaine, via
* `setMode()`. `page-bootstrap.js` force le second dès que l'URL porte `week` ou
* `focus` : un utilisateur venant corriger un planning identifié doit atterrir
* sur la saisie, pas sur l'éditeur de modèles.
*
* ÉTAT LOCAL — `draft` est une Map indexée par jour (1 à 5), contenant les
* plages en cours d'édition. Elle est indépendante de `PTA.state`, qui décrit la
* semaine réellement planifiée : un modèle n'est pas un planning.
*
* Le DOM fait autorité entre deux rendus : `syncDraftFromDom()` est appelé avant
* toute opération sur `draft`, car l'utilisateur peut avoir tapé dans les champs
* sans qu'aucun événement n'ait été traité.
*/
(() => {
'use strict';
const PTA = window.PTA;
if (!PTA) throw new Error('PTA core doit être chargé avant week-template-editor.js');
const { state } = PTA;
const { structureSelect, agentSelect, weekInput } = PTA.elements;
const { escapeHtml, readJsonResponse, apiErrorMessage, formatDuration, timeToMinutes, motifInfo } = PTA.utils;
const panel = document.querySelector('#template-mode-panel');
const weekPanel = document.querySelector('#week-mode-panel');
const modeTemplateButton = document.querySelector('#editor-mode-template');
const modeWeekButton = document.querySelector('#editor-mode-week');
const templateSelect = document.querySelector('#template-editor-select');
const nameInput = document.querySelector('#template-editor-name');
const gridElement = document.querySelector('#template-editor-grid');
const emptyElement = document.querySelector('#template-editor-empty');
const messageElement = document.querySelector('#template-editor-message');
const totalElement = document.querySelector('#template-editor-total');
const totalDetailElement = document.querySelector('#template-editor-total-detail');
const saveButton = document.querySelector('#template-editor-save');
const deleteButton = document.querySelector('#template-editor-delete');
const applyPeriodButton = document.querySelector('#template-editor-apply-period');
const applyWeekButton = document.querySelector('#template-editor-apply-week');
const DAYS = [
{ weekday: 1, label: 'Lundi' },
{ weekday: 2, label: 'Mardi' },
{ weekday: 3, label: 'Mercredi' },
{ weekday: 4, label: 'Jeudi' },
{ weekday: 5, label: 'Vendredi' },
];
/** Plages de la semaine type en cours d'édition, indexées par jour. */
let draft = new Map();
let templates = [];
let currentTemplateId = null;
let loading = false;
/** Message propre à l'éditeur, distinct du bandeau global de la page. */
function setMessage(text = '', type = 'info') {
if (!messageElement) return;
messageElement.className = text ? `message message-${type}` : 'message';
messageElement.textContent = text;
}
/** Agent auquel le modèle appartient ; un modèle est toujours nominatif. */
function agentId() {
return Number(agentSelect?.value || 0) || null;
}
/**
* Lieu proposé par défaut sur une nouvelle plage : celui sélectionné dans
* l'écran, sinon le premier disponible. Une plage sans lieu est refusée.
*/
function defaultStructureId() {
const selected = Number(structureSelect?.value || 0);
if (selected) return selected;
const first = structureSelect?.querySelector('option[value]:not([value=""])');
return first ? Number(first.value) : null;
}
/**
* Un agent sans droit à l'enveloppe de formation ne peut pas recevoir le
* motif « Formation ». Même règle qu'à la saisie semaine par semaine : 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;
}
/** Motifs proposables à CET agent, « Formation » exclue s'il n'y a pas droit. */
function motifs() {
return window.PTA_CONFIG?.motifs || [];
}
function defaultMotifId() {
const list = motifs();
const motif = list.find((item) => String(item.code).toUpperCase() === 'TRAVAIL')
|| list.find((item) => item.quota)
|| list[0];
return motif ? Number(motif.id) : null;
}
/** Options du sélecteur de motif d'une plage. */
function motifOptionsHtml(selectedMotifId = null) {
return motifs()
// Un motif interdit reste proposé s'il est déjà celui de la plage :
// le retirer changerait silencieusement une saisie existante.
.filter((motif) => agentHasTrainingRight()
|| String(motif.code).toUpperCase() !== 'FORMATION'
|| Number(motif.id) === Number(selectedMotifId))
.map((motif) => {
const selected = Number(motif.id) === Number(selectedMotifId) ? ' selected' : '';
const quotaLabel = motif.quota ? ' · décompte quota' : ' · hors quota';
return ``;
})
.join('');
}
/**
* Options du sélecteur de lieu.
* Un modèle peut porter des plages sur PLUSIEURS lieux : c'est ce qui le
* réserve au Service Enfance.
*/
function structureOptionsHtml(selectedStructureId = null) {
return [...(structureSelect?.querySelectorAll('option[value]:not([value=""])') || [])]
.map((option) => {
const selected = Number(option.value) === Number(selectedStructureId) ? ' selected' : '';
return ``;
})
.join('');
}
/**
* Cibles de recopie d'un jour : chacun des autres jours, ou tous d'un coup.
* Un agent a souvent le même rythme du lundi au vendredi ; recopier évite de
* ressaisir cinq fois la même chose.
*/
function copyTargetOptionsHtml(weekday) {
const others = DAYS.filter((day) => day.weekday !== weekday);
return [
'',
...others.map((day) => ``),
'',
].join('');
}
/** Plages d'un jour du brouillon (tableau vide si le jour est libre). */
function rangesFor(weekday) {
return draft.get(weekday) || [];
}
/** Brouillon vierge : les cinq jours présents, tous sans plage. */
function emptyDraft() {
const next = new Map();
DAYS.forEach((day) => next.set(day.weekday, []));
return next;
}
/** Plage neuve, horaires vides mais motif et lieu déjà proposés. */
function newRange() {
return {
heure_debut: '',
heure_fin: '',
id_motif: defaultMotifId(),
id_structure: defaultStructureId(),
};
}
/** Durée d'une plage ; 0 tant qu'elle est incomplète, jamais négatif. */
function rangeMinutes(range) {
if (!range.heure_debut || !range.heure_fin) return 0;
return Math.max(0, timeToMinutes(range.heure_fin) - timeToMinutes(range.heure_debut));
}
/** Total des minutes d'un jour. */
function dayMinutes(weekday) {
return rangesFor(weekday).reduce((total, range) => total + rangeMinutes(range), 0);
}
/**
* Totaux du modèle : toutes les minutes, et celles qui s'imputeraient sur le
* quota. La distinction compte, un modèle pouvant contenir des congés.
*/
function totals() {
let all = 0;
let counted = 0;
DAYS.forEach((day) => {
rangesFor(day.weekday).forEach((range) => {
const minutes = rangeMinutes(range);
all += minutes;
if (motifInfo(range.id_motif)?.quota) counted += minutes;
});
});
return { all, counted };
}
/** Balisage d'une plage : motif, lieu, horaires, suppression. */
function rangeHtml(weekday, index, range) {
const label = `${DAYS.find((day) => day.weekday === weekday)?.label || 'Jour'} plage ${index + 1}`;
return `
→
`;
}
/**
* Redessine toute la grille depuis `draft`, puis met à jour les totaux.
* Rendu complet plutôt que ciblé : le nombre de plages par jour varie sans
* cesse, et une mise à jour fine serait plus fragile.
*/
function render() {
if (!gridElement) return;
const ready = Boolean(agentId());
if (emptyElement) emptyElement.hidden = ready;
gridElement.hidden = !ready;
if (ready) {
gridElement.innerHTML = DAYS.map((day) => {
const ranges = rangesFor(day.weekday);
const minutes = dayMinutes(day.weekday);
return `
${escapeHtml(day.label)}${minutes > 0 ? formatDuration(minutes) : '—'}