307 lines
15 KiB
JavaScript
307 lines
15 KiB
JavaScript
(() => {
|
||
'use strict';
|
||
|
||
const PTA = window.PTA;
|
||
if (!PTA) return;
|
||
const { readJsonResponse, apiErrorMessage, escapeHtml, formatDate } = PTA.utils;
|
||
const el = (id) => document.getElementById(id);
|
||
const escapeAttr = (value) => String(value ?? '')
|
||
.replaceAll('&', '&')
|
||
.replaceAll('\"', '"')
|
||
.replaceAll("'", ''')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>');
|
||
let current = null;
|
||
|
||
function show(text, type = 'info') {
|
||
const box = el('annual-message');
|
||
if (!box) return;
|
||
box.className = `message message-${type}`;
|
||
box.textContent = text;
|
||
}
|
||
|
||
function formatGenerated(value) {
|
||
const date = new Date(String(value).replace(' ', 'T'));
|
||
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('fr-FR', { dateStyle: 'short', timeStyle: 'short' });
|
||
}
|
||
|
||
function formatContract(value) {
|
||
if (value === 'PERMANENT') return 'Permanent';
|
||
if (value === 'TEMPORAIRE') return 'Temporaire';
|
||
return value || 'Non renseigné';
|
||
}
|
||
|
||
function buildPlanningUrl(day) {
|
||
const agentId = Number(current?.agent?.id_agent || 0);
|
||
const structureId = Number(day.structure_id || current?.agent?.id_structure || 0);
|
||
const week = `${Number(day.week_year)}-W${String(day.week).padStart(2, '0')}`;
|
||
const params = new URLSearchParams({ agent_id: String(agentId), week, focus: 'annual' });
|
||
if (structureId) params.set('structure_id', String(structureId));
|
||
params.set('notice', `Ouverture depuis la vue annuelle — ${formatDate(day.date)}.`);
|
||
return `planning.php?${params.toString()}`;
|
||
}
|
||
|
||
function cellHtml(cell, day) {
|
||
const content = cell.display ? escapeHtml(cell.display) : ' ';
|
||
const title = cell.title ? ` title="${escapeAttr(cell.title)}"` : '';
|
||
const draftClass = day.has_draft && cell.minutes > 0 ? ' annual-cell-draft' : '';
|
||
return `<td class="annual-data-cell ${escapeHtml(cell.class || '')}${draftClass}" data-date="${escapeHtml(day.date)}"${title}>${content}</td>`;
|
||
}
|
||
|
||
function monthHtml(month) {
|
||
const rows = month.days.map((day) => {
|
||
const classes = [
|
||
day.is_weekend ? 'annual-weekend' : '',
|
||
day.is_vacation ? 'annual-vacation-row' : '',
|
||
day.has_entries ? 'annual-has-entry' : '',
|
||
].filter(Boolean).join(' ');
|
||
const vacationTitle = day.vacation_label ? ` title="${escapeAttr(day.vacation_label)}"` : '';
|
||
const weekText = day.show_week ? String(day.week).padStart(2, '0') : '';
|
||
return `
|
||
<tr class="${classes}" data-url="${escapeAttr(buildPlanningUrl(day))}"${vacationTitle}>
|
||
<th class="annual-day-cell"><span>${escapeHtml(day.weekday_short)}</span>${day.day}</th>
|
||
<td class="annual-week-cell">${weekText}</td>
|
||
${cellHtml(day.cells.AP, day)}
|
||
${cellHtml(day.cells.PP, day)}
|
||
${cellHtml(day.cells.AE, day)}
|
||
${cellHtml(day.cells.PE, day)}
|
||
</tr>`;
|
||
}).join('');
|
||
|
||
return `
|
||
<article class="annual-month" data-month="${month.number}">
|
||
<table>
|
||
<thead>
|
||
<tr><th colspan="6" class="annual-month-title">${escapeHtml(month.name)}</th></tr>
|
||
<tr class="annual-column-headings"><th>Jour</th><th>S</th><th>AP</th><th>PP</th><th>AE</th><th>PE</th></tr>
|
||
</thead>
|
||
<tbody>${rows}</tbody>
|
||
<tfoot>
|
||
<tr><th colspan="2">Total</th><td>${escapeHtml(month.totals.AP.compact || '0h')}</td><td>${escapeHtml(month.totals.PP.compact || '0h')}</td><td>${escapeHtml(month.totals.AE.compact || '0h')}</td><td>${escapeHtml(month.totals.PE.compact || '0h')}</td></tr>
|
||
</tfoot>
|
||
</table>
|
||
</article>`;
|
||
}
|
||
|
||
function quotaPeriodsHtml(periods) {
|
||
return periods.map((period) => `
|
||
<article class="annual-quota-chip">
|
||
<span>${escapeHtml(formatDate(period.date_debut_periode))} → ${escapeHtml(formatDate(period.date_fin_periode))}</span>
|
||
<strong>${escapeHtml(period.affectees?.libelle || '0 h 00')} affectées / ${escapeHtml(period.quota_cible?.libelle || '0 h 00')}</strong>
|
||
<small>Reste : ${escapeHtml(period.restantes?.libelle || '0 h 00')}</small>
|
||
</article>`).join('');
|
||
}
|
||
|
||
function render(data) {
|
||
current = data;
|
||
const agent = data.agent || {};
|
||
const periods = data.quota_periods || [];
|
||
const referencePeriod = periods[0] || {};
|
||
|
||
el('annual-content').hidden = false;
|
||
el('annual-title').textContent = `ANNUALISATION ${data.year}`;
|
||
el('annual-updated').textContent = `Mise à jour : ${formatGenerated(data.generated_at)}`;
|
||
el('annual-agent-name').textContent = `${agent.prenom || ''} ${agent.nom || ''}`.trim() || '—';
|
||
el('annual-agent-number').textContent = agent.matricule || '—';
|
||
el('annual-rate').textContent = referencePeriod.quotite_travail !== undefined ? `${Number(referencePeriod.quotite_travail).toLocaleString('fr-FR')} %` : '—';
|
||
el('annual-target').textContent = referencePeriod.quota_cible?.libelle || '—';
|
||
el('annual-contract').textContent = formatContract(agent.type_contrat);
|
||
el('annual-location').textContent = agent.structure_principale_nom || 'Sans lieu principal';
|
||
el('annual-post').textContent = agent.poste_libelle || 'Poste non renseigné';
|
||
el('annual-remaining').textContent = referencePeriod.restantes?.libelle || '—';
|
||
el('annual-quota-periods').innerHTML = quotaPeriodsHtml(periods);
|
||
el('annual-calendar-total').textContent = data.calendar_totals?.duration || '0 h 00';
|
||
el('annual-validated-total').textContent = data.calendar_totals?.validated || '0 h 00';
|
||
el('annual-draft-total').textContent = data.calendar_totals?.draft || '0 h 00';
|
||
el('annual-year-label').textContent = `01/01/${data.year} → 31/12/${data.year}`;
|
||
el('annual-months').innerHTML = (data.months || []).map(monthHtml).join('');
|
||
el('annual-legend').innerHTML = (data.legend || []).map((item) => `
|
||
<span class="annual-legend-item"><i class="${escapeHtml(item.class)}"></i><b>${escapeHtml(item.code)}</b> ${escapeHtml(item.label)}</span>
|
||
`).join('');
|
||
|
||
el('annual-open-pta').href = `pta.php?agent_id=${Number(agent.id_agent)}&date=${data.year}-07-01`;
|
||
el('annual-open-month').href = `agents.php?agent_id=${Number(agent.id_agent)}&month=${data.year}-01`;
|
||
el('annual-print').disabled = false;
|
||
el('annual-fullscreen').disabled = false;
|
||
if (el('annual-validate-year')) el('annual-validate-year').disabled = false;
|
||
}
|
||
|
||
async function load(options = {}) {
|
||
const agentId = Number(el('annual-agent')?.value || 0);
|
||
const year = Number(el('annual-year')?.value || 0);
|
||
if (!agentId || year < 2000 || year > 2100) {
|
||
show('Choisissez un agent et une année valide.', 'error');
|
||
return;
|
||
}
|
||
|
||
if (options.clearValidation !== false && el('annual-validation-result')) {
|
||
el('annual-validation-result').innerHTML = '';
|
||
}
|
||
show('Construction de la vue annuelle…');
|
||
el('annual-content').hidden = true;
|
||
try {
|
||
const response = await fetch(`api/annual_overview.php?agent_id=${agentId}&year=${year}`);
|
||
const data = await readJsonResponse(response);
|
||
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger la vue annuelle.'));
|
||
render(data);
|
||
show('Vue annuelle actualisée.', 'success');
|
||
} catch (error) {
|
||
show(error.message, 'error');
|
||
}
|
||
}
|
||
|
||
function validationPlanningUrl(planning) {
|
||
const agentId = Number(current?.agent?.id_agent || el('annual-agent')?.value || 0);
|
||
const week = `${Number(planning.annee)}-W${String(planning.numero_semaine).padStart(2, '0')}`;
|
||
const params = new URLSearchParams({
|
||
agent_id: String(agentId),
|
||
structure_id: String(Number(planning.id_structure)),
|
||
week,
|
||
focus: 'annual-validation',
|
||
notice: `Correction demandée depuis la vue annuelle — semaine ${String(planning.numero_semaine).padStart(2, '0')}.`,
|
||
});
|
||
return `planning.php?${params.toString()}`;
|
||
}
|
||
|
||
function renderValidationIssues(data) {
|
||
const container = el('annual-validation-result');
|
||
if (!container) return;
|
||
const invalid = (data.plannings || []).filter((planning) => planning.has_errors);
|
||
container.innerHTML = `
|
||
<div class="annual-validation-summary is-error">
|
||
<strong>Aucune semaine n’a été validée.</strong><br>
|
||
${escapeHtml(data.error || 'Des erreurs doivent être corrigées avant la validation annuelle.')}
|
||
<div>${invalid.length} planning(s) à corriger.</div>
|
||
</div>
|
||
<div class="annual-validation-issues">
|
||
${invalid.map((planning) => {
|
||
const errors = (planning.controls || []).filter((control) => control.niveau === 'ERREUR');
|
||
return `
|
||
<article class="annual-validation-issue">
|
||
<header>
|
||
<h4>Semaine ${String(planning.numero_semaine).padStart(2, '0')} — ${escapeHtml(planning.structure_nom || 'Lieu non renseigné')}</h4>
|
||
<a class="button button-secondary annual-validation-open-link"
|
||
href="${escapeAttr(validationPlanningUrl(planning))}"
|
||
target="_blank" rel="noopener">Corriger cette semaine</a>
|
||
</header>
|
||
<div>${escapeHtml(formatDate(planning.date_debut))} → ${escapeHtml(formatDate(planning.date_fin))}</div>
|
||
<ul>${errors.map((control) => `<li>${escapeHtml(control.message)}</li>`).join('')}</ul>
|
||
</article>`;
|
||
}).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
function renderValidationWarning(data) {
|
||
const container = el('annual-validation-result');
|
||
if (!container) return;
|
||
container.innerHTML = `
|
||
<div class="annual-validation-summary is-warning">
|
||
<strong>Avertissement avant validation.</strong><br>
|
||
${escapeHtml(data.message || 'Une confirmation est nécessaire.')}
|
||
</div>`;
|
||
}
|
||
|
||
function renderValidationSuccess(data) {
|
||
const container = el('annual-validation-result');
|
||
if (!container) return;
|
||
container.innerHTML = `
|
||
<div class="annual-validation-summary is-success">
|
||
<strong>Validation terminée.</strong><br>
|
||
${escapeHtml(data.message || 'Les plannings ont été validés.')}
|
||
</div>`;
|
||
}
|
||
|
||
async function validateWholeYear(forceQuota = false) {
|
||
const agentId = Number(el('annual-agent')?.value || 0);
|
||
const year = Number(el('annual-year')?.value || 0);
|
||
if (!agentId || !year || !current || Number(current.agent?.id_agent) !== agentId || Number(current.year) !== year) {
|
||
show('Affichez d’abord la vue annuelle de l’agent à contrôler.', 'error');
|
||
return;
|
||
}
|
||
|
||
const button = el('annual-validate-year');
|
||
if (button) button.disabled = true;
|
||
const container = el('annual-validation-result');
|
||
if (container) {
|
||
container.innerHTML = '<div class="annual-validation-summary">Contrôle de toutes les semaines en brouillon…</div>';
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('api/annual_validate.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
csrf_token: PTA.config.csrfToken,
|
||
agent_id: agentId,
|
||
year,
|
||
force_quota: Boolean(forceQuota),
|
||
}),
|
||
});
|
||
const data = await readJsonResponse(response);
|
||
|
||
if (response.status === 422 && data.status === 'errors') {
|
||
renderValidationIssues(data);
|
||
show('Des semaines doivent être corrigées avant la validation annuelle.', 'error');
|
||
return;
|
||
}
|
||
|
||
if (response.status === 409 && data.status === 'warning') {
|
||
renderValidationWarning(data);
|
||
const confirmed = window.confirm(`${data.message || 'Le quota annuel présente un avertissement.'}\n\nValider malgré cet avertissement ?`);
|
||
if (confirmed) await validateWholeYear(true);
|
||
return;
|
||
}
|
||
|
||
if (!response.ok) {
|
||
throw new Error(apiErrorMessage(data, 'Impossible de valider l’année.'));
|
||
}
|
||
|
||
renderValidationSuccess(data);
|
||
show(data.message || 'Validation annuelle terminée.', 'success');
|
||
await load({ clearValidation: false });
|
||
} catch (error) {
|
||
if (container) {
|
||
container.innerHTML = `<div class="annual-validation-summary is-error">${escapeHtml(error.message)}</div>`;
|
||
}
|
||
show(error.message, 'error');
|
||
} finally {
|
||
if (button) button.disabled = false;
|
||
}
|
||
}
|
||
|
||
function handleCalendarClick(event) {
|
||
const row = event.target.closest('tr[data-url]');
|
||
if (!row) return;
|
||
window.location.href = row.dataset.url;
|
||
}
|
||
|
||
async function toggleFullscreen() {
|
||
const target = el('annual-content');
|
||
if (!target) return;
|
||
try {
|
||
if (!document.fullscreenElement) {
|
||
await target.requestFullscreen?.();
|
||
} else {
|
||
await document.exitFullscreen?.();
|
||
}
|
||
} catch (error) {
|
||
show('Le mode plein écran n’est pas disponible dans ce navigateur.', 'error');
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
el('load-annual')?.addEventListener('click', load);
|
||
el('annual-print')?.addEventListener('click', () => window.print());
|
||
el('annual-fullscreen')?.addEventListener('click', toggleFullscreen);
|
||
el('annual-validate-year')?.addEventListener('click', () => validateWholeYear(false));
|
||
el('annual-months')?.addEventListener('click', handleCalendarClick);
|
||
|
||
const params = window.PTA_PAGE_CONFIG?.params || {};
|
||
if (params.agentId) el('annual-agent').value = String(params.agentId);
|
||
if (params.year) el('annual-year').value = String(params.year);
|
||
if (params.agentId) load();
|
||
}
|
||
|
||
PTA.modules.annualOverview = { init, load };
|
||
})();
|