692 lines
32 KiB
JavaScript
692 lines
32 KiB
JavaScript
(() => {
|
||
'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="${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>
|
||
`;
|
||
}
|
||
|
||
async function openApplyModal() {
|
||
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();
|
||
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="${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() {
|
||
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();
|
||
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,
|
||
};
|
||
})();
|