Files
pta/assets/js/coverage.js
Loic Masi 5fbf76868f pour prod
2026-08-07 16:13:43 +02:00

285 lines
14 KiB
JavaScript
Raw Permalink 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 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 `
<tr class="coverage-alert-row" data-alert-index="${index}">
<td>
<strong>${escapeHtml(alert.structure_nom)}</strong>
<small>${escapeHtml(alert.structure_code || '')}</small>
</td>
<td>
<strong>${escapeHtml(alert.jour_libelle)}</strong>
<small>${formatDate(alert.date)}</small>
</td>
<td><strong>${escapeHtml(alert.heure_debut)}${escapeHtml(alert.heure_fin)}</strong></td>
<td>${Number(alert.minimum_agents)}</td>
<td>${Number(alert.agents_planifies)}</td>
<td><span class="coverage-missing-chip">${missing} agent${missing > 1 ? 's' : ''}</span></td>
<td class="coverage-action-cell"><button type="button" class="button button-primary open-coverage-gap" data-alert-index="${index}">Planifier un agent</button></td>
</tr>`;
}).join('');
}
if (unconfiguredElement) {
unconfiguredElement.hidden = unconfigured.length === 0;
unconfiguredElement.innerHTML = unconfigured.length ? `
<strong>Couverture non configurée pour ${unconfigured.length} lieu${unconfigured.length > 1 ? 'x' : ''}</strong>
<p>${unconfigured.map((item) => escapeHtml(item.nom)).join(', ')}. Configurez les plages attendues dans la page Administration pour pouvoir détecter les créneaux vides.</p>
` : '';
}
}
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 danalyser 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 `
<div class="coverage-rule-row">
<label class="field compact-field">
<span>Début</span>
<input class="coverage-rule-start" type="time" step="900" value="${escapeHtml(rule.heure_debut || '')}" required>
</label>
<label class="field compact-field">
<span>Fin</span>
<input class="coverage-rule-end" type="time" step="900" value="${escapeHtml(rule.heure_fin || '')}" required>
</label>
<label class="field compact-field coverage-rule-minimum-field">
<span>Agents min.</span>
<input class="coverage-rule-minimum" type="number" min="1" max="50" step="1" value="${Number(rule.minimum_agents || 1)}" required>
</label>
<button type="button" class="button button-danger remove-coverage-rule" aria-label="Supprimer cette plage">Supprimer</button>
</div>`;
}
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]) => `
<section class="coverage-rule-day" data-day="${day}">
<div class="coverage-rule-day-header">
<strong>${dayNames[day]}</strong>
<button type="button" class="button button-secondary add-coverage-rule">+ Ajouter une plage</button>
</div>
<div class="coverage-rule-day-ranges">
${dayRules.length ? dayRules.map(coverageRuleRow).join('') : '<p class="coverage-rule-empty">Aucune couverture obligatoire configurée.</p>'}
</div>
</section>`).join('');
if (saveRulesButton) saveRulesButton.disabled = false;
}
async function loadRules() {
const structureId = ruleStructureSelect?.value;
if (!structureId || !ruleEditor) {
if (ruleEditor) ruleEditor.innerHTML = '<div class="empty-state">Choisissez un lieu daffectation.</div>';
if (saveRulesButton) saveRulesButton.disabled = true;
return;
}
if (ruleMessage) {
ruleMessage.className = 'message';
ruleMessage.textContent = '';
}
ruleEditor.innerHTML = '<div class="empty-state">Chargement des règles de couverture…</div>';
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 denregistrer 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 = '<p class="coverage-rule-empty">Aucune couverture obligatoire configurée.</p>';
}
}
});
refresh({ silent: true });
}
PTA.modules.coverage = { init, refresh, loadRules };
})();