Files
pta/assets/js/week-templates.js
PTA 4201894e28 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>
2026-08-17 16:53:04 +02:00

707 lines
33 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.
(() => {
'use strict';
const PTA = window.PTA;
if (!PTA) throw new Error('PTA core doit être chargé avant week-templates.js');
const { state } = PTA;
const { agentSelect, weekInput } = PTA.elements;
const { showMessage, readJsonResponse, apiErrorMessage, escapeHtml } = PTA.utils;
const saveButton = document.querySelector('#save-week-template');
const applyButton = document.querySelector('#apply-week-template');
const saveModal = document.querySelector('#save-week-template-modal');
const saveForm = document.querySelector('#save-week-template-form');
const saveNameInput = document.querySelector('#week-template-name');
const saveContext = document.querySelector('#save-week-template-context');
const saveMessage = document.querySelector('#save-week-template-message');
const applyModal = document.querySelector('#apply-week-template-modal');
const applyForm = document.querySelector('#apply-week-template-form');
const templateSelect = document.querySelector('#week-template-select');
const templateDetails = document.querySelector('#week-template-details');
const replaceWarning = document.querySelector('#week-template-replace-warning');
const deleteButton = document.querySelector('#delete-week-template');
const applyMessage = document.querySelector('#apply-week-template-message');
const periodButton = document.querySelector('#apply-week-template-period');
const periodModal = document.querySelector('#apply-week-template-period-modal');
const periodForm = document.querySelector('#apply-week-template-period-form');
const periodTemplateSelect = document.querySelector('#week-template-period-select');
const periodStartInput = document.querySelector('#week-template-period-start');
const periodEndInput = document.querySelector('#week-template-period-end');
const periodReplaceWarning = document.querySelector('#week-template-period-replace-warning');
const periodPreviewButton = document.querySelector('#preview-week-template-period');
const periodPreviewPanel = document.querySelector('#week-template-period-preview');
const periodConfirmButton = document.querySelector('#confirm-apply-week-template-period');
const periodMessage = document.querySelector('#apply-week-template-period-message');
let templates = [];
let lastPeriodPreview = null;
let lastPeriodPreviewSignature = '';
function contextReady() {
return Boolean(agentSelect?.value && weekInput?.value);
}
function globalEntryCount() {
return (state.entries?.length || 0) + (state.otherStructureEntries?.length || 0);
}
function refreshButtons() {
const ready = contextReady();
if (saveButton) saveButton.disabled = !ready;
if (applyButton) applyButton.disabled = !ready;
if (periodButton) periodButton.disabled = !ready;
}
function setInlineMessage(element, text = '', type = 'info') {
if (!element) return;
element.className = text ? `message message-${type}` : 'message';
element.textContent = text;
}
function updateBodyModalState() {
const anyOpen = Array.from(document.querySelectorAll('.modal-backdrop'))
.some((modal) => !modal.hidden);
document.body.classList.toggle('modal-open', anyOpen);
}
function closeModal(modal) {
if (!modal) return;
modal.hidden = true;
updateBodyModalState();
}
function openModal(modal) {
if (!modal) return;
modal.hidden = false;
document.body.classList.add('modal-open');
}
function agentLabel() {
return agentSelect?.options[agentSelect.selectedIndex]?.textContent?.trim() || 'Agent sélectionné';
}
async function persistCurrentEditorIfNeeded() {
const planning = PTA.modules.planning;
if (!planning || state.planningStatus === 'VALIDE') return true;
if (!planning.isPlanningContextReady?.()) return true;
const planningId = await planning.saveDraft?.({ silent: true });
return planningId !== null && planningId !== undefined;
}
async function openSaveModal() {
if (!contextReady()) {
showMessage('Sélectionnez un agent et une semaine avant d’enregistrer une semaine type.', 'warning');
return;
}
if (state.planningStatus !== 'VALIDE') {
const synced = PTA.modules.planning?.syncEntriesFromWeeklyForm?.({ silent: true });
if (synced === false) return;
}
if (globalEntryCount() === 0) {
showMessage('La semaine ne contient aucune affectation à enregistrer comme modèle.', 'warning');
return;
}
if (saveNameInput) saveNameInput.value = '';
if (saveContext) {
saveContext.innerHTML = `
<strong>${escapeHtml(agentLabel())}</strong>
<span>Semaine ${escapeHtml(weekInput.value)} · ${globalEntryCount()} créneau${globalEntryCount() > 1 ? 'x' : ''} actuellement affiché${globalEntryCount() > 1 ? 's' : ''}</span>
`;
}
setInlineMessage(saveMessage);
openModal(saveModal);
setTimeout(() => saveNameInput?.focus(), 0);
}
async function saveTemplate(replace = false) {
const name = saveNameInput?.value?.trim() || '';
if (!name) {
setInlineMessage(saveMessage, 'Saisissez un nom pour la semaine type.', 'warning');
saveNameInput?.focus();
return;
}
const submitButton = saveForm?.querySelector('button[type="submit"]');
if (submitButton) submitButton.disabled = true;
setInlineMessage(saveMessage, 'Enregistrement du modèle...', 'info');
try {
const persisted = await persistCurrentEditorIfNeeded();
if (!persisted) {
throw new Error('Le planning courant n’a pas pu être enregistré avant la création du modèle.');
}
const response = await fetch('api/save_week_template.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
agent_id: Number(agentSelect.value),
week: weekInput.value,
name,
replace,
}),
});
const data = await readJsonResponse(response);
if (response.status === 409 && data.code === 'TEMPLATE_EXISTS' && !replace) {
const confirmed = window.confirm(`Une semaine type nommée « ${name} » existe déjà pour cet agent. Voulez-vous la remplacer par la semaine actuelle ?`);
if (confirmed) {
await saveTemplate(true);
} else {
setInlineMessage(saveMessage, 'Enregistrement annulé.', 'info');
}
return;
}
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’enregistrer la semaine type.'));
setInlineMessage(saveMessage, data.message, 'success');
showMessage(data.message, 'success');
setTimeout(() => closeModal(saveModal), 450);
} catch (error) {
setInlineMessage(saveMessage, error.message, 'error');
} finally {
if (submitButton) submitButton.disabled = false;
}
}
async function loadTemplates() {
if (!agentSelect?.value) return [];
const response = await fetch(`api/week_templates.php?agent_id=${encodeURIComponent(agentSelect.value)}`);
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 : [];
return templates;
}
function renderTemplateSelect() {
if (!templateSelect) return;
if (!templates.length) {
templateSelect.innerHTML = '<option value="">Aucune semaine type enregistrée</option>';
templateSelect.disabled = true;
if (deleteButton) deleteButton.disabled = true;
if (templateDetails) templateDetails.innerHTML = '<p class="hint">Créez d’abord une semaine type depuis une semaine déjà planifiée.</p>';
return;
}
templateSelect.disabled = false;
templateSelect.innerHTML = '<option value="">Choisir une semaine type</option>' + templates.map((template) => (
`<option value="${Number(template.id)}">${escapeHtml(template.name)}</option>`
)).join('');
renderSelectedTemplateDetails();
}
function renderSelectedTemplateDetails() {
const selectedId = Number(templateSelect?.value || 0);
const template = templates.find((item) => Number(item.id) === selectedId);
if (deleteButton) deleteButton.disabled = !template;
if (!templateDetails) return;
if (!template) {
templateDetails.innerHTML = '<p class="hint">Sélectionnez un modèle pour l’appliquer à la semaine affichée.</p>';
return;
}
templateDetails.innerHTML = `
<div class="week-template-detail-card">
<strong>${escapeHtml(template.name)}</strong>
<span>${Number(template.entry_count)} créneau${Number(template.entry_count) > 1 ? 'x' : ''} · ${Number(template.location_count)} lieu${Number(template.location_count) > 1 ? 'x' : ''}</span>
</div>
`;
}
/**
* `preselectedId` permet à l'éditeur de semaine type d'ouvrir la modale
* directement sur le modèle en cours d'édition, sans le rechercher.
*/
async function openApplyModal(preselectedId = null) {
if (!contextReady()) {
showMessage('Sélectionnez un agent et la semaine cible avant d’importer une semaine type.', 'warning');
return;
}
setInlineMessage(applyMessage, 'Chargement des modèles...', 'info');
if (templateSelect) {
templateSelect.disabled = true;
templateSelect.innerHTML = '<option value="">Chargement...</option>';
}
openModal(applyModal);
try {
await loadTemplates();
renderTemplateSelect();
if (preselectedId && templateSelect) {
templateSelect.value = String(Number(preselectedId));
renderSelectedTemplateDetails();
}
setInlineMessage(applyMessage);
} catch (error) {
setInlineMessage(applyMessage, error.message, 'error');
}
}
async function applyTemplate() {
const templateId = Number(templateSelect?.value || 0);
const mode = applyForm?.querySelector('input[name="week-template-mode"]:checked')?.value || 'merge';
const template = templates.find((item) => Number(item.id) === templateId);
if (!templateId || !template) {
setInlineMessage(applyMessage, 'Sélectionnez une semaine type.', 'warning');
return;
}
if (mode === 'replace') {
const confirmed = window.confirm(
`Le mode « Remplacer » va supprimer toutes les affectations actuelles de ${agentLabel()} pour ${weekInput.value}, tous lieux confondus, puis appliquer « ${template.name} ».\n\nContinuer ?`
);
if (!confirmed) return;
}
const submitButton = applyForm?.querySelector('button[type="submit"]');
if (submitButton) submitButton.disabled = true;
if (deleteButton) deleteButton.disabled = true;
setInlineMessage(applyMessage, 'Import de la semaine type...', 'info');
try {
// En mode compléter, persister d'abord les éventuelles modifications locales non encore enregistrées.
if (mode === 'merge') {
const persisted = await persistCurrentEditorIfNeeded();
if (!persisted) {
throw new Error('Le planning courant n’a pas pu être enregistré avant l’import.');
}
}
const response = await fetch('api/apply_week_template.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
template_id: templateId,
agent_id: Number(agentSelect.value),
week: weekInput.value,
mode,
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’importer la semaine type.'));
if (data.quota) state.quota = data.quota;
await PTA.modules.planning?.loadPlanning?.();
setInlineMessage(applyMessage, data.message, 'success');
showMessage(data.message, 'success');
setTimeout(() => closeModal(applyModal), 550);
} catch (error) {
setInlineMessage(applyMessage, error.message, 'error');
} finally {
if (submitButton) submitButton.disabled = false;
renderSelectedTemplateDetails();
}
}
function periodMode() {
return periodForm?.querySelector('input[name="week-template-period-mode"]:checked')?.value || 'merge';
}
function periodFilter() {
return periodForm?.querySelector('input[name="week-template-period-filter"]:checked')?.value || 'all';
}
function weekMonday(weekValue) {
const match = /^(\d{4})-W(\d{2})$/.exec(weekValue || '');
if (!match) return null;
const year = Number(match[1]);
const week = Number(match[2]);
const jan4 = new Date(Date.UTC(year, 0, 4));
const jan4Day = jan4.getUTCDay() || 7;
const monday = new Date(jan4);
monday.setUTCDate(jan4.getUTCDate() - jan4Day + 1 + (week - 1) * 7);
return monday;
}
function dateInputValue(date) {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
const year = date.getUTCFullYear();
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const day = String(date.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatDateFr(value) {
if (!value) return '';
const date = new Date(`${value}T12:00:00`);
return new Intl.DateTimeFormat('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date);
}
function invalidatePeriodPreview() {
lastPeriodPreview = null;
lastPeriodPreviewSignature = '';
if (periodConfirmButton) periodConfirmButton.disabled = true;
if (periodPreviewPanel) {
periodPreviewPanel.hidden = true;
periodPreviewPanel.innerHTML = '';
}
setInlineMessage(periodMessage);
updatePeriodReplaceWarning();
}
function currentPeriodSignature() {
return JSON.stringify({
template_id: Number(periodTemplateSelect?.value || 0),
start_date: periodStartInput?.value || '',
end_date: periodEndInput?.value || '',
mode: periodMode(),
period_filter: periodFilter(),
});
}
function renderPeriodTemplateSelect() {
if (!periodTemplateSelect) return;
if (!templates.length) {
periodTemplateSelect.innerHTML = '<option value="">Aucune semaine type enregistrée</option>';
periodTemplateSelect.disabled = true;
return;
}
periodTemplateSelect.disabled = false;
periodTemplateSelect.innerHTML = '<option value="">Choisir une semaine type</option>' + templates.map((template) => (
`<option value="${Number(template.id)}">${escapeHtml(template.name)}</option>`
)).join('');
}
function setDefaultPeriodDates() {
const monday = weekMonday(weekInput?.value);
if (!monday) return;
if (periodStartInput && !periodStartInput.value) periodStartInput.value = dateInputValue(monday);
if (periodEndInput && !periodEndInput.value) {
const friday = new Date(monday);
friday.setUTCDate(monday.getUTCDate() + 4);
periodEndInput.value = dateInputValue(friday);
}
}
async function openPeriodModal(preselectedId = null) {
if (!contextReady()) {
showMessage('Sélectionnez un agent et une semaine avant d’appliquer une semaine type sur une période.', 'warning');
return;
}
invalidatePeriodPreview();
setDefaultPeriodDates();
if (periodTemplateSelect) {
periodTemplateSelect.disabled = true;
periodTemplateSelect.innerHTML = '<option value="">Chargement...</option>';
}
openModal(periodModal);
setInlineMessage(periodMessage, 'Chargement des modèles...', 'info');
try {
await loadTemplates();
renderPeriodTemplateSelect();
if (preselectedId && periodTemplateSelect) {
periodTemplateSelect.value = String(Number(preselectedId));
invalidatePeriodPreview();
}
setInlineMessage(periodMessage);
} catch (error) {
setInlineMessage(periodMessage, error.message, 'error');
}
}
function updatePeriodReplaceWarning() {
if (periodReplaceWarning) periodReplaceWarning.hidden = periodMode() !== 'replace';
}
function renderPeriodPreview(preview) {
if (!periodPreviewPanel) return;
const summary = preview.summary || {};
const conflicts = Array.isArray(preview.conflicts) ? preview.conflicts : [];
const days = Array.isArray(preview.days) ? preview.days : [];
const vacations = Array.isArray(preview.vacation_periods) ? preview.vacation_periods : [];
const mode = preview.mode || periodMode();
const filter = preview.period_filter || periodFilter();
const filterLabel = filter === 'school'
? 'Périodes scolaires uniquement'
: filter === 'extra'
? 'Périodes extrascolaires uniquement'
: 'Toutes les semaines';
const vacationHtml = vacations.length
? `<div class="week-template-period-vacations"><strong>Vacances prises en compte</strong><ul>${vacations.map((item) => `<li>${escapeHtml(item.label)} : ${formatDateFr(item.start_date)} → ${formatDateFr(item.end_date)}</li>`).join('')}</ul></div>`
: filter === 'extra'
? '<div class="week-template-period-alert is-warning"><strong>Aucune période de vacances enregistrée sur cette plage.</strong> Aucun jour ne pourra être considéré comme extrascolaire tant que le calendrier n’est pas renseigné.</div>'
: '<div class="week-template-period-alert is-warning"><strong>Aucune période de vacances enregistrée sur cette plage.</strong> Les dates seront considérées comme scolaires.</div>';
const relevantDays = days.filter((day) => Number(day.eligible_count) > 0 || Number(day.skipped_count) > 0 || Number(day.conflict_count) > 0);
const dayRows = relevantDays.slice(0, 80).map((day) => {
const periodLabel = day.period_type === 'EXTRASCOLAIRE'
? `Extrascolaire${day.vacation_label ? ` · ${escapeHtml(day.vacation_label)}` : ''}`
: 'Périscolaire';
return `<tr>
<td>${formatDateFr(day.date)}</td>
<td><span class="weekly-period-badge ${day.period_type === 'EXTRASCOLAIRE' ? 'is-extra' : 'is-peri'}">${periodLabel}</span></td>
<td>${Number(day.eligible_count)}</td>
<td>${Number(day.skipped_count)}</td>
<td>${Number(day.conflict_count) || '—'}</td>
</tr>`;
}).join('');
const conflictHtml = conflicts.length
? `<div class="week-template-period-alert is-error"><strong>${conflicts.length} conflit${conflicts.length > 1 ? 's' : ''} détecté${conflicts.length > 1 ? 's' : ''}.</strong> ${mode === 'merge' ? 'Le mode « Compléter » ne peut pas être appliqué tant que ces conflits existent. Vous pouvez choisir « Remplacer les jours concernés » après vérification.' : 'Ces créneaux existants seront remplacés sur les jours concernés.'}</div>`
: '<div class="week-template-period-alert is-success"><strong>Aucun chevauchement détecté.</strong></div>';
periodPreviewPanel.innerHTML = `
<div class="week-template-period-summary">
<article><span>Périmètre</span><strong>${escapeHtml(filterLabel)}</strong></article>
<article><span>Créneaux à créer</span><strong>${Number(summary.entry_count || 0)}</strong></article>
<article><span>Jours concernés</span><strong>${Number(summary.target_day_count || 0)}</strong></article>
<article><span>Semaines touchées</span><strong>${Number(summary.week_count || 0)}</strong></article>
<article><span>Ignorés (vacances)</span><strong>${Number(summary.skipped_vacation_count || 0)}</strong></article>
<article><span>Ignorés (hors vacances)</span><strong>${Number(summary.skipped_school_count || 0)}</strong></article>
<article><span>${mode === 'replace' ? 'Créneaux existants remplacés' : 'Conflits'}</span><strong>${mode === 'replace' ? Number(summary.existing_entry_count_on_target_dates || 0) : Number(summary.conflict_count || 0)}</strong></article>
</div>
${vacationHtml}
${conflictHtml}
<div class="table-scroll week-template-period-table-wrap">
<table class="data-table week-template-period-table">
<thead><tr><th>Date</th><th>Contexte</th><th>Créés</th><th>Ignorés</th><th>Conflits</th></tr></thead>
<tbody>${dayRows || '<tr><td colspan="5" class="empty-cell">Aucune journée du modèle ne correspond à cette période.</td></tr>'}</tbody>
</table>
</div>
${relevantDays.length > 80 ? `<p class="hint">Aperçu limité aux 80 premières journées concernées sur ${relevantDays.length}.</p>` : ''}
`;
periodPreviewPanel.hidden = false;
const canApply = Number(summary.entry_count || 0) > 0
&& (mode !== 'merge' || Number(summary.conflict_count || 0) === 0);
if (periodConfirmButton) periodConfirmButton.disabled = !canApply;
}
async function previewPeriodApplication() {
const templateId = Number(periodTemplateSelect?.value || 0);
const startDate = periodStartInput?.value || '';
const endDate = periodEndInput?.value || '';
if (!templateId || !startDate || !endDate) {
setInlineMessage(periodMessage, 'Sélectionnez une semaine type et renseignez les deux dates.', 'warning');
return;
}
if (endDate < startDate) {
setInlineMessage(periodMessage, 'La date de fin doit être postérieure ou égale à la date de début.', 'warning');
return;
}
if (periodPreviewButton) periodPreviewButton.disabled = true;
if (periodConfirmButton) periodConfirmButton.disabled = true;
setInlineMessage(periodMessage, 'Analyse de la période et des vacances...', 'info');
try {
const response = await fetch('api/preview_week_template_period.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
template_id: templateId,
agent_id: Number(agentSelect.value),
start_date: startDate,
end_date: endDate,
mode: periodMode(),
period_filter: periodFilter(),
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de prévisualiser l’application sur la période.'));
lastPeriodPreview = data.preview;
lastPeriodPreviewSignature = currentPeriodSignature();
renderPeriodPreview(lastPeriodPreview);
setInlineMessage(periodMessage, 'Aperçu calculé. Vérifiez les dates avant de confirmer.', 'success');
} catch (error) {
lastPeriodPreview = null;
lastPeriodPreviewSignature = '';
if (periodPreviewPanel) periodPreviewPanel.hidden = true;
setInlineMessage(periodMessage, error.message, 'error');
} finally {
if (periodPreviewButton) periodPreviewButton.disabled = false;
}
}
async function applyTemplatePeriod() {
if (!lastPeriodPreview || lastPeriodPreviewSignature !== currentPeriodSignature()) {
setInlineMessage(periodMessage, 'La période a changé. Relancez la prévisualisation avant d’appliquer le modèle.', 'warning');
if (periodConfirmButton) periodConfirmButton.disabled = true;
return;
}
const templateId = Number(periodTemplateSelect?.value || 0);
const template = templates.find((item) => Number(item.id) === templateId);
const mode = periodMode();
const summary = lastPeriodPreview.summary || {};
if (mode === 'replace' && Number(summary.existing_entry_count_on_target_dates || 0) > 0) {
const confirmed = window.confirm(
`${Number(summary.existing_entry_count_on_target_dates)} créneau(x) existant(s) seront supprimé(s) sur les jours réellement concernés avant d’appliquer « ${template?.name || 'la semaine type'} ».\n\nLes dates exclues par le filtre de calendrier ne seront pas modifiées. Continuer ?`
);
if (!confirmed) return;
}
if (periodConfirmButton) periodConfirmButton.disabled = true;
if (periodPreviewButton) periodPreviewButton.disabled = true;
setInlineMessage(periodMessage, 'Application de la semaine type sur la période...', 'info');
try {
const response = await fetch('api/apply_week_template_period.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
template_id: templateId,
agent_id: Number(agentSelect.value),
start_date: periodStartInput.value,
end_date: periodEndInput.value,
mode,
period_filter: periodFilter(),
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’appliquer la semaine type sur la période.'));
await PTA.modules.planning?.loadPlanning?.();
PTA.modules.pendingValidation?.refresh?.({ silent: true });
PTA.modules.coverage?.refresh?.({ silent: true });
showMessage(data.message, 'success');
setInlineMessage(periodMessage, data.message, 'success');
lastPeriodPreview = null;
lastPeriodPreviewSignature = '';
setTimeout(() => closeModal(periodModal), 700);
} catch (error) {
setInlineMessage(periodMessage, error.message, 'error');
// Le planning peut avoir changé depuis l'aperçu : imposer une nouvelle prévisualisation.
lastPeriodPreview = null;
lastPeriodPreviewSignature = '';
if (periodConfirmButton) periodConfirmButton.disabled = true;
} finally {
if (periodPreviewButton) periodPreviewButton.disabled = false;
}
}
async function deleteSelectedTemplate() {
const templateId = Number(templateSelect?.value || 0);
const template = templates.find((item) => Number(item.id) === templateId);
if (!template) return;
const confirmed = window.confirm(`Supprimer définitivement la semaine type « ${template.name} » ?`);
if (!confirmed) return;
if (deleteButton) deleteButton.disabled = true;
try {
const response = await fetch('api/delete_week_template.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
template_id: templateId,
agent_id: Number(agentSelect.value),
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de supprimer la semaine type.'));
setInlineMessage(applyMessage, data.message, 'success');
await loadTemplates();
renderTemplateSelect();
} catch (error) {
setInlineMessage(applyMessage, error.message, 'error');
renderSelectedTemplateDetails();
}
}
function updateReplaceWarning() {
const mode = applyForm?.querySelector('input[name="week-template-mode"]:checked')?.value || 'merge';
if (replaceWarning) replaceWarning.hidden = mode !== 'replace';
}
function init() {
saveButton?.addEventListener('click', openSaveModal);
applyButton?.addEventListener('click', openApplyModal);
periodButton?.addEventListener('click', openPeriodModal);
saveForm?.addEventListener('submit', (event) => {
event.preventDefault();
saveTemplate(false);
});
applyForm?.addEventListener('submit', (event) => {
event.preventDefault();
applyTemplate();
});
periodForm?.addEventListener('submit', (event) => {
event.preventDefault();
applyTemplatePeriod();
});
periodPreviewButton?.addEventListener('click', previewPeriodApplication);
document.querySelector('#close-save-week-template-modal')?.addEventListener('click', () => closeModal(saveModal));
document.querySelector('#cancel-save-week-template')?.addEventListener('click', () => closeModal(saveModal));
document.querySelector('#close-apply-week-template-modal')?.addEventListener('click', () => closeModal(applyModal));
document.querySelector('#cancel-apply-week-template')?.addEventListener('click', () => closeModal(applyModal));
document.querySelector('#close-apply-week-template-period-modal')?.addEventListener('click', () => closeModal(periodModal));
document.querySelector('#cancel-apply-week-template-period')?.addEventListener('click', () => closeModal(periodModal));
saveModal?.addEventListener('click', (event) => {
if (event.target === saveModal) closeModal(saveModal);
});
applyModal?.addEventListener('click', (event) => {
if (event.target === applyModal) closeModal(applyModal);
});
periodModal?.addEventListener('click', (event) => {
if (event.target === periodModal) closeModal(periodModal);
});
templateSelect?.addEventListener('change', renderSelectedTemplateDetails);
applyForm?.querySelectorAll('input[name="week-template-mode"]').forEach((input) => {
input.addEventListener('change', updateReplaceWarning);
});
periodForm?.querySelectorAll('input[name="week-template-period-mode"]').forEach((input) => {
input.addEventListener('change', invalidatePeriodPreview);
});
periodForm?.querySelectorAll('input[name="week-template-period-filter"]').forEach((input) => {
input.addEventListener('change', invalidatePeriodPreview);
});
[periodTemplateSelect, periodStartInput, periodEndInput].forEach((input) => {
input?.addEventListener('change', invalidatePeriodPreview);
input?.addEventListener('input', invalidatePeriodPreview);
});
deleteButton?.addEventListener('click', deleteSelectedTemplate);
agentSelect?.addEventListener('change', refreshButtons);
weekInput?.addEventListener('input', refreshButtons);
weekInput?.addEventListener('change', refreshButtons);
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return;
if (saveModal && !saveModal.hidden) closeModal(saveModal);
if (applyModal && !applyModal.hidden) closeModal(applyModal);
if (periodModal && !periodModal.hidden) closeModal(periodModal);
});
updateReplaceWarning();
updatePeriodReplaceWarning();
refreshButtons();
}
PTA.modules.weekTemplates = {
init,
refreshButtons,
loadTemplates,
// Ouverts aussi depuis l'éditeur de semaine type.
openApplyModal,
openPeriodModal,
};
})();