(() => {
'use strict';
const PTA = window.PTA;
if (!PTA) throw new Error('PTA core doit être chargé avant coverage.js');
const { elements, utils } = PTA;
const { readJsonResponse, apiErrorMessage, escapeHtml, formatDuration, formatDate } = utils;
const badge = document.querySelector('#coverage-tab-badge');
const weekInput = document.querySelector('#coverage-week');
const structureFilter = document.querySelector('#coverage-structure-filter');
const refreshButton = document.querySelector('#refresh-coverage-alerts');
const loadingElement = document.querySelector('#coverage-loading');
const emptyElement = document.querySelector('#coverage-empty');
const contentElement = document.querySelector('#coverage-content');
const bodyElement = document.querySelector('#coverage-alerts-body');
const messageElement = document.querySelector('#coverage-message');
const unconfiguredElement = document.querySelector('#coverage-unconfigured');
const ruleStructureSelect = document.querySelector('#coverage-rules-structure');
const ruleEditor = document.querySelector('#coverage-rules-editor');
const ruleMessage = document.querySelector('#coverage-rules-message');
const loadRulesButton = document.querySelector('#load-coverage-rules');
const saveRulesButton = document.querySelector('#save-coverage-rules');
const dayNames = {
1: 'Lundi',
2: 'Mardi',
3: 'Mercredi',
4: 'Jeudi',
5: 'Vendredi',
};
let alerts = [];
let loading = false;
function setBadge(count) {
if (!badge) return;
badge.textContent = String(count);
badge.hidden = count === 0;
badge.setAttribute('aria-label', `${count} plage${count > 1 ? 's' : ''} avec couverture insuffisante`);
}
function setMessage(text = '', type = 'info') {
if (!messageElement) return;
messageElement.className = text ? `message message-${type}` : 'message';
messageElement.textContent = text;
}
function renderAlerts(data) {
alerts = Array.isArray(data.alerts) ? data.alerts : [];
const summary = data.summary || {};
const unconfigured = Array.isArray(data.unconfigured_structures) ? data.unconfigured_structures : [];
setBadge(alerts.length);
document.querySelector('#coverage-gap-count').textContent = String(Number(summary.gap_count || 0));
document.querySelector('#coverage-structure-count').textContent = String(Number(summary.affected_structure_count || 0));
document.querySelector('#coverage-gap-hours').textContent = formatDuration(Number(summary.gap_minutes || 0));
document.querySelector('#coverage-unconfigured-count').textContent = String(Number(summary.unconfigured_structure_count || 0));
if (loadingElement) loadingElement.hidden = true;
if (emptyElement) emptyElement.hidden = alerts.length !== 0;
if (contentElement) contentElement.hidden = alerts.length === 0;
if (bodyElement) {
bodyElement.innerHTML = alerts.map((alert, index) => {
const missing = Number(alert.agents_manquants || 0);
return `
${escapeHtml(alert.structure_nom)}
${escapeHtml(alert.structure_code || '')}
${escapeHtml(alert.jour_libelle)}
${formatDate(alert.date)}
${escapeHtml(alert.heure_debut)}–${escapeHtml(alert.heure_fin)}
${Number(alert.minimum_agents)}
${Number(alert.agents_planifies)}
${missing} agent${missing > 1 ? 's' : ''}
Planifier un agent
`;
}).join('');
}
if (unconfiguredElement) {
unconfiguredElement.hidden = unconfigured.length === 0;
unconfiguredElement.innerHTML = unconfigured.length ? `
Couverture non configurée pour ${unconfigured.length} lieu${unconfigured.length > 1 ? 'x' : ''}
${unconfigured.map((item) => escapeHtml(item.nom)).join(', ')}. Configurez les plages attendues dans la page Administration pour pouvoir détecter les créneaux vides.
` : '';
}
}
async function refresh(options = {}) {
if (loading) return;
const week = weekInput?.value;
if (!week) return;
loading = true;
if (!options.silent) setMessage('');
if (loadingElement) loadingElement.hidden = false;
if (refreshButton) refreshButton.disabled = true;
try {
const params = new URLSearchParams({ week });
if (structureFilter?.value) params.set('structure_id', structureFilter.value);
const response = await fetch(`api/coverage_alerts.php?${params.toString()}`, { headers: { Accept: 'application/json' } });
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’analyser la couverture.'));
renderAlerts(data);
} catch (error) {
if (loadingElement) loadingElement.hidden = true;
setMessage(error.message, 'error');
} finally {
loading = false;
if (refreshButton) refreshButton.disabled = false;
}
}
function openGap(alert) {
if (!alert) return;
const params = new URLSearchParams({
structure_id: String(alert.id_structure),
week: weekInput?.value || '',
notice: `Couverture insuffisante le ${alert.jour_libelle.toLowerCase()} ${formatDate(alert.date)} de ${alert.heure_debut} à ${alert.heure_fin} sur ${alert.structure_nom}. Choisissez un agent à affecter.`,
});
window.location.href = `planning.php?${params.toString()}`;
}
function coverageRuleRow(rule = {}) {
return `
Début
Fin
Agents min.
Supprimer
`;
}
function renderRuleEditor(rules = []) {
if (!ruleEditor) return;
const grouped = new Map();
for (let day = 1; day <= 5; day += 1) grouped.set(day, []);
rules.forEach((rule) => grouped.get(Number(rule.jour_semaine))?.push(rule));
ruleEditor.innerHTML = Array.from(grouped.entries()).map(([day, dayRules]) => `
${dayRules.length ? dayRules.map(coverageRuleRow).join('') : '
Aucune couverture obligatoire configurée.
'}
`).join('');
if (saveRulesButton) saveRulesButton.disabled = false;
}
async function loadRules() {
const structureId = ruleStructureSelect?.value;
if (!structureId || !ruleEditor) {
if (ruleEditor) ruleEditor.innerHTML = 'Choisissez un lieu d’affectation.
';
if (saveRulesButton) saveRulesButton.disabled = true;
return;
}
if (ruleMessage) {
ruleMessage.className = 'message';
ruleMessage.textContent = '';
}
ruleEditor.innerHTML = 'Chargement des règles de couverture…
';
try {
const response = await fetch(`api/structure_coverage_rules.php?structure_id=${encodeURIComponent(structureId)}`);
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger les règles de couverture.'));
renderRuleEditor(data.rules || []);
} catch (error) {
ruleEditor.innerHTML = '';
if (ruleMessage) {
ruleMessage.className = 'message message-error';
ruleMessage.textContent = error.message;
}
}
}
function collectRules() {
const rules = [];
ruleEditor?.querySelectorAll('.coverage-rule-day').forEach((daySection) => {
const day = Number(daySection.dataset.day);
daySection.querySelectorAll('.coverage-rule-row').forEach((row) => {
const start = row.querySelector('.coverage-rule-start')?.value || '';
const end = row.querySelector('.coverage-rule-end')?.value || '';
const minimum = Number(row.querySelector('.coverage-rule-minimum')?.value || 1);
if (!start && !end) return;
rules.push({ jour_semaine: day, heure_debut: start, heure_fin: end, minimum_agents: minimum });
});
});
return rules;
}
async function saveRules() {
const structureId = ruleStructureSelect?.value;
if (!structureId || !saveRulesButton || !ruleMessage) return;
saveRulesButton.disabled = true;
ruleMessage.className = 'message';
ruleMessage.textContent = '';
try {
const response = await fetch('api/save_structure_coverage_rules.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
structure_id: Number(structureId),
rules: collectRules(),
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’enregistrer les règles de couverture.'));
renderRuleEditor(data.rules || []);
ruleMessage.className = 'message message-success';
ruleMessage.textContent = data.message;
await refresh({ silent: true });
if (document.querySelector('#overview-structure')?.value === String(structureId)) {
await PTA.modules.structureOverview?.load?.();
}
} catch (error) {
ruleMessage.className = 'message message-error';
ruleMessage.textContent = error.message;
} finally {
saveRulesButton.disabled = false;
}
}
function init() {
refreshButton?.addEventListener('click', () => refresh());
weekInput?.addEventListener('change', () => refresh());
structureFilter?.addEventListener('change', () => refresh());
bodyElement?.addEventListener('click', (event) => {
const button = event.target.closest('.open-coverage-gap');
if (!button) return;
openGap(alerts[Number(button.dataset.alertIndex)]);
});
ruleStructureSelect?.addEventListener('change', loadRules);
loadRulesButton?.addEventListener('click', loadRules);
saveRulesButton?.addEventListener('click', saveRules);
ruleEditor?.addEventListener('click', (event) => {
const addButton = event.target.closest('.add-coverage-rule');
if (addButton) {
const day = addButton.closest('.coverage-rule-day');
const ranges = day?.querySelector('.coverage-rule-day-ranges');
if (!ranges) return;
ranges.querySelector('.coverage-rule-empty')?.remove();
ranges.insertAdjacentHTML('beforeend', coverageRuleRow());
return;
}
const removeButton = event.target.closest('.remove-coverage-rule');
if (removeButton) {
const ranges = removeButton.closest('.coverage-rule-day-ranges');
removeButton.closest('.coverage-rule-row')?.remove();
if (ranges && !ranges.querySelector('.coverage-rule-row')) {
ranges.innerHTML = 'Aucune couverture obligatoire configurée.
';
}
}
});
refresh({ silent: true });
}
PTA.modules.coverage = { init, refresh, loadRules };
})();