pour prod
This commit is contained in:
691
assets/js/week-templates.js
Normal file
691
assets/js/week-templates.js
Normal file
@@ -0,0 +1,691 @@
|
||||
(() => {
|
||||
'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,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user