676 lines
28 KiB
JavaScript
676 lines
28 KiB
JavaScript
/**
|
||
* É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 `<option value="${Number(motif.id)}"${selected}>${escapeHtml(motif.label)}${quotaLabel}</option>`;
|
||
})
|
||
.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 `<option value="${Number(option.value)}"${selected}>${escapeHtml(option.textContent.trim())}</option>`;
|
||
})
|
||
.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 [
|
||
'<option value="">Copier vers…</option>',
|
||
...others.map((day) => `<option value="${day.weekday}">${escapeHtml(day.label)}</option>`),
|
||
'<option value="all">Tous les autres jours</option>',
|
||
].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 `
|
||
<div class="template-range" data-weekday="${weekday}" data-index="${index}">
|
||
<label class="template-range-field template-range-field-motif">
|
||
<span>Motif</span>
|
||
<select class="template-range-motif" aria-label="Motif ${escapeHtml(label)}">
|
||
${motifOptionsHtml(range.id_motif)}
|
||
</select>
|
||
</label>
|
||
<label class="template-range-field">
|
||
<span>Début</span>
|
||
<input class="template-range-start" type="time" step="900" value="${escapeHtml(range.heure_debut)}" aria-label="Début ${escapeHtml(label)}">
|
||
</label>
|
||
<span class="template-range-separator" aria-hidden="true">→</span>
|
||
<label class="template-range-field">
|
||
<span>Fin</span>
|
||
<input class="template-range-end" type="time" step="900" value="${escapeHtml(range.heure_fin)}" aria-label="Fin ${escapeHtml(label)}">
|
||
</label>
|
||
<label class="template-range-field template-range-field-structure">
|
||
<span>Lieu</span>
|
||
<select class="template-range-structure" aria-label="Lieu ${escapeHtml(label)}">
|
||
${structureOptionsHtml(range.id_structure)}
|
||
</select>
|
||
</label>
|
||
<button type="button" class="button button-ghost template-range-remove" aria-label="Supprimer ${escapeHtml(label)}" title="Supprimer cette plage">×</button>
|
||
</div>`;
|
||
}
|
||
|
||
/**
|
||
* 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 `
|
||
<article class="template-day${ranges.length === 0 ? ' is-empty' : ''}" data-weekday="${day.weekday}">
|
||
<header class="template-day-header">
|
||
<strong>${escapeHtml(day.label)}</strong>
|
||
<span class="template-day-total">${minutes > 0 ? formatDuration(minutes) : '—'}</span>
|
||
</header>
|
||
<div class="template-day-ranges">
|
||
${ranges.length === 0
|
||
? '<p class="template-day-empty">Aucune plage — jour non travaillé.</p>'
|
||
: ranges.map((range, index) => rangeHtml(day.weekday, index, range)).join('')}
|
||
</div>
|
||
<footer class="template-day-actions">
|
||
<button type="button" class="button button-secondary template-day-add" data-weekday="${day.weekday}">+ Ajouter une plage</button>
|
||
<label class="template-day-copy-field">
|
||
<span class="visually-hidden">Copier ${escapeHtml(day.label)} vers un autre jour</span>
|
||
<select class="template-day-copy" data-weekday="${day.weekday}" aria-label="Copier ${escapeHtml(day.label)} vers un autre jour"${ranges.length === 0 ? ' disabled' : ''}>
|
||
${copyTargetOptionsHtml(day.weekday)}
|
||
</select>
|
||
</label>
|
||
</footer>
|
||
</article>`;
|
||
}).join('');
|
||
}
|
||
|
||
const { all, counted } = totals();
|
||
if (totalElement) totalElement.textContent = formatDuration(all);
|
||
if (totalDetailElement) totalDetailElement.textContent = `dont ${formatDuration(counted)} décomptées`;
|
||
|
||
refreshButtons();
|
||
}
|
||
|
||
/** Vrai si le brouillon contient au moins une plage. */
|
||
function hasAnyRange() {
|
||
return DAYS.some((day) => rangesFor(day.weekday).length > 0);
|
||
}
|
||
|
||
/**
|
||
* Active ou désactive les actions selon l'état : agent choisi, nom saisi,
|
||
* modèle non vide, absence d'appel réseau en cours.
|
||
*/
|
||
function refreshButtons() {
|
||
const ready = Boolean(agentId());
|
||
const named = Boolean(nameInput?.value.trim());
|
||
|
||
if (templateSelect) templateSelect.disabled = !ready;
|
||
if (nameInput) nameInput.disabled = !ready;
|
||
if (saveButton) saveButton.disabled = !ready || !named || !hasAnyRange() || loading;
|
||
if (deleteButton) deleteButton.disabled = !ready || currentTemplateId === null || loading;
|
||
// Dupliquer suppose un modèle déjà enregistré : on ne duplique jamais un
|
||
// brouillon d'écran, seulement ce qui est réellement en base.
|
||
if (applyPeriodButton) applyPeriodButton.disabled = !ready || currentTemplateId === null || loading;
|
||
if (applyWeekButton) applyWeekButton.disabled = !ready || currentTemplateId === null || loading;
|
||
}
|
||
|
||
/** Remplit le sélecteur des modèles existants de l'agent. */
|
||
function renderTemplateSelect() {
|
||
if (!templateSelect) return;
|
||
const options = ['<option value="">➕ Nouvelle semaine type</option>'];
|
||
templates.forEach((template) => {
|
||
const selected = Number(template.id) === Number(currentTemplateId) ? ' selected' : '';
|
||
const detail = `${template.entry_count} plage${template.entry_count > 1 ? 's' : ''}`;
|
||
options.push(`<option value="${Number(template.id)}"${selected}>${escapeHtml(template.name)} — ${detail}</option>`);
|
||
});
|
||
templateSelect.innerHTML = options.join('');
|
||
}
|
||
|
||
/** Recharge la liste des modèles de l'agent courant. */
|
||
async function loadTemplates() {
|
||
const agent = agentId();
|
||
if (!agent) {
|
||
templates = [];
|
||
renderTemplateSelect();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`api/week_templates.php?agent_id=${agent}`, {
|
||
headers: { Accept: 'application/json' },
|
||
});
|
||
const data = await readJsonResponse(response);
|
||
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger les semaines types.'));
|
||
templates = Array.isArray(data.templates) ? data.templates : [];
|
||
renderTemplateSelect();
|
||
} catch (error) {
|
||
setMessage(error.message, 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Charge un modèle existant dans le brouillon pour le modifier.
|
||
* Les plages sont réparties par jour dans la Map ; les horaires arrivent en
|
||
* HH:MM, format attendu par les champs `type="time"`.
|
||
*/
|
||
async function openTemplate(templateId) {
|
||
const agent = agentId();
|
||
if (!agent || !templateId) {
|
||
currentTemplateId = null;
|
||
draft = emptyDraft();
|
||
if (nameInput) nameInput.value = '';
|
||
render();
|
||
return;
|
||
}
|
||
|
||
loading = true;
|
||
refreshButtons();
|
||
try {
|
||
const response = await fetch(`api/week_template.php?agent_id=${agent}&template_id=${Number(templateId)}`, {
|
||
headers: { Accept: 'application/json' },
|
||
});
|
||
const data = await readJsonResponse(response);
|
||
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’ouvrir cette semaine type.'));
|
||
|
||
currentTemplateId = Number(data.template.id);
|
||
if (nameInput) nameInput.value = data.template.name;
|
||
|
||
draft = emptyDraft();
|
||
(data.entries || []).forEach((entry) => {
|
||
const weekday = Number(entry.jour_semaine);
|
||
if (!draft.has(weekday)) return;
|
||
draft.get(weekday).push({
|
||
heure_debut: entry.heure_debut,
|
||
heure_fin: entry.heure_fin,
|
||
id_motif: Number(entry.id_motif),
|
||
id_structure: Number(entry.id_structure),
|
||
});
|
||
});
|
||
setMessage('');
|
||
} catch (error) {
|
||
setMessage(error.message, 'error');
|
||
} finally {
|
||
loading = false;
|
||
render();
|
||
}
|
||
}
|
||
|
||
/** Lit les valeurs saisies à l'écran dans le brouillon en mémoire. */
|
||
function syncDraftFromDom() {
|
||
if (!gridElement || gridElement.hidden) return;
|
||
const next = emptyDraft();
|
||
gridElement.querySelectorAll('.template-range').forEach((element) => {
|
||
const weekday = Number(element.dataset.weekday);
|
||
if (!next.has(weekday)) return;
|
||
next.get(weekday).push({
|
||
heure_debut: element.querySelector('.template-range-start')?.value || '',
|
||
heure_fin: element.querySelector('.template-range-end')?.value || '',
|
||
id_motif: Number(element.querySelector('.template-range-motif')?.value || 0) || null,
|
||
id_structure: Number(element.querySelector('.template-range-structure')?.value || 0) || null,
|
||
});
|
||
});
|
||
draft = next;
|
||
}
|
||
|
||
/**
|
||
* Contrôles faits à l'écran pour éviter un aller-retour serveur inutile.
|
||
* Le serveur revalide tout : ceci n'est qu'une commodité.
|
||
*/
|
||
function localValidationError() {
|
||
for (const day of DAYS) {
|
||
const ranges = rangesFor(day.weekday);
|
||
for (const range of ranges) {
|
||
if (!range.heure_debut || !range.heure_fin) {
|
||
return `${day.label} : une plage n’a pas d’horaire de début ou de fin.`;
|
||
}
|
||
if (timeToMinutes(range.heure_fin) <= timeToMinutes(range.heure_debut)) {
|
||
return `${day.label} : la fin doit être postérieure au début (${range.heure_debut} → ${range.heure_fin}).`;
|
||
}
|
||
if (!range.id_motif || !range.id_structure) {
|
||
return `${day.label} : chaque plage doit avoir un motif et un lieu d’affectation.`;
|
||
}
|
||
}
|
||
const sorted = [...ranges].sort((a, b) => a.heure_debut.localeCompare(b.heure_debut));
|
||
for (let index = 1; index < sorted.length; index++) {
|
||
if (sorted[index].heure_debut < sorted[index - 1].heure_fin) {
|
||
return `${day.label} : deux plages se chevauchent (${sorted[index - 1].heure_debut}–${sorted[index - 1].heure_fin} et ${sorted[index].heure_debut}–${sorted[index].heure_fin}).`;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Met à plat la Map en la liste attendue par l'API : un objet par plage,
|
||
* portant son numéro de jour.
|
||
*/
|
||
function flatEntries() {
|
||
const entries = [];
|
||
DAYS.forEach((day) => {
|
||
rangesFor(day.weekday).forEach((range) => {
|
||
entries.push({
|
||
jour_semaine: day.weekday,
|
||
heure_debut: range.heure_debut,
|
||
heure_fin: range.heure_fin,
|
||
id_motif: range.id_motif,
|
||
id_structure: range.id_structure,
|
||
});
|
||
});
|
||
});
|
||
return entries;
|
||
}
|
||
|
||
/**
|
||
* Enregistre le modèle.
|
||
* Un 409 signifie qu'un modèle porte déjà ce nom : l'utilisateur se voit
|
||
* proposer le remplacement, ce qui rappelle cette fonction avec
|
||
* `replace = true`.
|
||
*/
|
||
async function save(replace = false) {
|
||
const agent = agentId();
|
||
const name = nameInput?.value.trim() || '';
|
||
if (!agent || !name) return;
|
||
|
||
syncDraftFromDom();
|
||
const error = localValidationError();
|
||
if (error) {
|
||
setMessage(error, 'error');
|
||
return;
|
||
}
|
||
|
||
loading = true;
|
||
refreshButtons();
|
||
try {
|
||
const response = await fetch('api/save_week_template_entries.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||
body: JSON.stringify({
|
||
csrf_token: window.PTA_CONFIG.csrfToken,
|
||
agent_id: agent,
|
||
name,
|
||
replace,
|
||
entries: flatEntries(),
|
||
}),
|
||
});
|
||
const data = await readJsonResponse(response);
|
||
|
||
if (response.status === 409 && data.code === 'TEMPLATE_EXISTS' && !replace) {
|
||
loading = false;
|
||
if (window.confirm(`Une semaine type « ${name} » existe déjà pour cet agent. La remplacer ?`)) {
|
||
await save(true);
|
||
return;
|
||
}
|
||
setMessage('Enregistrement annulé : le nom est déjà utilisé.', 'warning');
|
||
refreshButtons();
|
||
return;
|
||
}
|
||
|
||
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’enregistrer la semaine type.'));
|
||
|
||
currentTemplateId = Number(data.template.id);
|
||
setMessage(`${data.message} Elle peut maintenant être dupliquée sur une période.`, 'success');
|
||
await loadTemplates();
|
||
renderTemplateSelect();
|
||
} catch (err) {
|
||
setMessage(err.message, 'error');
|
||
} finally {
|
||
loading = false;
|
||
render();
|
||
}
|
||
}
|
||
|
||
/** Supprime le modèle ouvert, après confirmation. */
|
||
async function removeTemplate() {
|
||
const agent = agentId();
|
||
if (!agent || currentTemplateId === null) return;
|
||
const template = templates.find((item) => Number(item.id) === Number(currentTemplateId));
|
||
if (!window.confirm(`Supprimer définitivement la semaine type « ${template?.name || ''} » ? Les plannings déjà créés ne sont pas affectés.`)) {
|
||
return;
|
||
}
|
||
|
||
loading = true;
|
||
refreshButtons();
|
||
try {
|
||
const response = await fetch('api/delete_week_template.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||
body: JSON.stringify({
|
||
csrf_token: window.PTA_CONFIG.csrfToken,
|
||
agent_id: agent,
|
||
template_id: currentTemplateId,
|
||
}),
|
||
});
|
||
const data = await readJsonResponse(response);
|
||
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de supprimer la semaine type.'));
|
||
|
||
currentTemplateId = null;
|
||
draft = emptyDraft();
|
||
if (nameInput) nameInput.value = '';
|
||
setMessage(data.message || 'Semaine type supprimée.', 'success');
|
||
await loadTemplates();
|
||
} catch (error) {
|
||
setMessage(error.message, 'error');
|
||
} finally {
|
||
loading = false;
|
||
render();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Recopie les plages d'un jour vers un autre, ou vers tous les autres.
|
||
*
|
||
* Les plages sont CLONÉES (`{ ...range }`) : sans cette copie, les jours
|
||
* partageraient les mêmes objets et modifier l'un modifierait les autres.
|
||
*/
|
||
function copyDay(sourceWeekday, target) {
|
||
syncDraftFromDom();
|
||
const source = rangesFor(sourceWeekday);
|
||
if (source.length === 0) return;
|
||
|
||
const targets = target === 'all'
|
||
? DAYS.filter((day) => day.weekday !== sourceWeekday).map((day) => day.weekday)
|
||
: [Number(target)];
|
||
|
||
targets.forEach((weekday) => {
|
||
draft.set(weekday, source.map((range) => ({ ...range })));
|
||
});
|
||
|
||
const sourceLabel = DAYS.find((day) => day.weekday === sourceWeekday)?.label || 'Ce jour';
|
||
const targetLabel = target === 'all'
|
||
? 'tous les autres jours'
|
||
: (DAYS.find((day) => day.weekday === Number(target))?.label || 'le jour choisi').toLowerCase();
|
||
setMessage(`${sourceLabel} copié sur ${targetLabel}. Pensez à enregistrer la semaine type.`, 'info');
|
||
render();
|
||
}
|
||
|
||
/**
|
||
* Bascule entre les deux modes exclusifs de planning.php.
|
||
* Appelée aussi par `page-bootstrap.js`, qui force le mode « week » sur les
|
||
* liens profonds portant une semaine ou un focus.
|
||
*
|
||
* @param {'template'|'week'} mode
|
||
*/
|
||
function setMode(mode) {
|
||
const isTemplate = mode === 'template';
|
||
if (panel) panel.hidden = !isTemplate;
|
||
if (weekPanel) weekPanel.hidden = isTemplate;
|
||
modeTemplateButton?.classList.toggle('is-active', isTemplate);
|
||
modeWeekButton?.classList.toggle('is-active', !isTemplate);
|
||
modeTemplateButton?.setAttribute('aria-selected', String(isTemplate));
|
||
modeWeekButton?.setAttribute('aria-selected', String(!isTemplate));
|
||
}
|
||
|
||
/**
|
||
* Changement d'agent : le brouillon est vidé et les modèles rechargés.
|
||
* Un modèle appartient à un seul agent, celui de l'agent précédent n'a plus
|
||
* de sens.
|
||
*/
|
||
function onAgentChanged() {
|
||
currentTemplateId = null;
|
||
draft = emptyDraft();
|
||
if (nameInput) nameInput.value = '';
|
||
setMessage('');
|
||
loadTemplates().then(render);
|
||
}
|
||
|
||
/** Point d'entrée appelé par page-bootstrap.js. */
|
||
function init() {
|
||
draft = emptyDraft();
|
||
|
||
modeTemplateButton?.addEventListener('click', () => setMode('template'));
|
||
modeWeekButton?.addEventListener('click', () => setMode('week'));
|
||
|
||
agentSelect?.addEventListener('change', onAgentChanged);
|
||
structureSelect?.addEventListener('change', () => {
|
||
// Le lieu par défaut des nouvelles plages suit la sélection.
|
||
loadTemplates();
|
||
});
|
||
|
||
templateSelect?.addEventListener('change', (event) => openTemplate(event.target.value));
|
||
nameInput?.addEventListener('input', refreshButtons);
|
||
|
||
gridElement?.addEventListener('click', (event) => {
|
||
const addButton = event.target.closest('.template-day-add');
|
||
if (addButton) {
|
||
syncDraftFromDom();
|
||
const weekday = Number(addButton.dataset.weekday);
|
||
draft.get(weekday)?.push(newRange());
|
||
render();
|
||
return;
|
||
}
|
||
|
||
const removeButton = event.target.closest('.template-range-remove');
|
||
if (removeButton) {
|
||
const range = removeButton.closest('.template-range');
|
||
syncDraftFromDom();
|
||
const weekday = Number(range.dataset.weekday);
|
||
const index = Number(range.dataset.index);
|
||
draft.get(weekday)?.splice(index, 1);
|
||
render();
|
||
}
|
||
});
|
||
|
||
gridElement?.addEventListener('change', (event) => {
|
||
const copySelect = event.target.closest('.template-day-copy');
|
||
if (copySelect) {
|
||
const target = copySelect.value;
|
||
copySelect.value = '';
|
||
if (target) copyDay(Number(copySelect.dataset.weekday), target);
|
||
return;
|
||
}
|
||
// Toute autre modification met simplement les totaux à jour.
|
||
syncDraftFromDom();
|
||
render();
|
||
});
|
||
|
||
saveButton?.addEventListener('click', () => save(false));
|
||
deleteButton?.addEventListener('click', removeTemplate);
|
||
applyPeriodButton?.addEventListener('click', () => {
|
||
PTA.modules.weekTemplates?.openPeriodModal?.(currentTemplateId);
|
||
});
|
||
applyWeekButton?.addEventListener('click', () => {
|
||
if (!weekInput?.value) {
|
||
setMessage('Choisissez d’abord une semaine dans la sélection en haut de page.', 'warning');
|
||
return;
|
||
}
|
||
PTA.modules.weekTemplates?.openApplyModal?.(currentTemplateId);
|
||
});
|
||
|
||
render();
|
||
if (agentId()) loadTemplates().then(render);
|
||
}
|
||
|
||
PTA.modules.weekTemplateEditor = {
|
||
init,
|
||
setMode,
|
||
refresh: () => loadTemplates().then(render),
|
||
};
|
||
})();
|