pour prod
This commit is contained in:
284
assets/js/coverage.js
Normal file
284
assets/js/coverage.js
Normal file
@@ -0,0 +1,284 @@
|
||||
(() => {
|
||||
'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 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 `
|
||||
<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 d’affectation.</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 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 = '<p class="coverage-rule-empty">Aucune couverture obligatoire configurée.</p>';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
refresh({ silent: true });
|
||||
}
|
||||
|
||||
PTA.modules.coverage = { init, refresh, loadRules };
|
||||
})();
|
||||
Reference in New Issue
Block a user