Files
pta/assets/js/administration.js
PTA 30cb50031d Droit à l'enveloppe de formation par agent, et motif « Formation » filtré à la saisie
Nouvelle colonne `agent.droit_enveloppe_formation`, cochée par défaut, saisie à
la création de l'agent et modifiable depuis sa fiche. Un agent sans ce droit :

- ne voit plus le motif « Formation » dans le sélecteur de motif de l'éditeur de
  planning ni dans le modal de la vue individuelle ;
- se voit refuser toute affectation de ce motif par le serveur, avec un message
  qui nomme le motif et l'agent (save_planning.php et update_agent_entry.php) ;
- a une enveloppe de formation de 0 min au PTA annuel, et une ALERTE
  FORMATION_SANS_DROIT si des heures de formation subsistent dans son planning.

À ne pas confondre avec `formation_repartition_annuelle`, qui existait déjà :
celle-ci dit que la formation est déjà comptée dans la répartition annuelle —
l'agent y a droit, l'enveloppe complémentaire n'est simplement pas ajoutée. Les
deux règles sont désormais rassemblées dans AgentModel::trainingEnvelopeMinutes(),
qui remplace les deux calculs « 840 ou 0 » jusque-là dupliqués.

Le motif interdit reste affiché lorsqu'il est déjà celui de la plage en cours
d'édition : le retirer de la liste changerait silencieusement le motif d'une
saisie existante. C'est le contrôle de saisie, puis le serveur, qui demandent
alors de corriger la plage.

La migration est nommée v7_9b et non v7_10 : les fichiers sont joués dans
l'ordre alphabétique, où « v7_10 » passerait avant « v7_8 ».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:30:43 +02:00

487 lines
24 KiB
JavaScript
Raw 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 administration.js');
const { structureSelect } = PTA.elements;
const {
escapeHtml,
readJsonResponse,
apiErrorMessage,
formatDuration,
} = PTA.utils;
const typeLabel = (type) => type === 'EXTRASCOLAIRE' ? 'Extrascolaire' : 'Périscolaire';
async function saveAgentStructure(button) {
const row = button.closest('tr[data-agent-id]');
const select = row?.querySelector('.agent-structure-select');
const message = document.querySelector('#assignment-message');
if (!row || !select || !message) return;
button.disabled = true;
message.className = 'message';
message.textContent = '';
try {
const response = await fetch('api/agent_assignments.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
agent_id: Number(row.dataset.agentId),
structure_id: select.value ? Number(select.value) : null,
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de modifier le lieu principal de l’agent.'));
message.className = 'message message-success';
message.textContent = data.message;
if (structureSelect?.value) await PTA.modules.planning?.loadAgents?.();
if (document.querySelector('#overview-structure')?.value) await PTA.modules.structureOverview?.load?.();
if (document.querySelector('#overview-agent')?.value) await PTA.modules.agentOverview?.load?.();
} catch (error) {
message.className = 'message message-error';
message.textContent = error.message;
} finally {
button.disabled = false;
}
}
async function saveStructureType(button) {
const row = button.closest('tr[data-structure-id]');
const select = row?.querySelector('.structure-type-select');
const message = document.querySelector('#structure-settings-message');
if (!row || !select || !message) return;
button.disabled = true;
message.className = 'message';
message.textContent = '';
try {
const response = await fetch('api/update_structure.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
structure_id: Number(row.dataset.structureId),
type_affectation: select.value,
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de modifier le type du lieu.'));
updateStructureLabels(Number(row.dataset.structureId), select.value);
message.className = 'message message-success';
message.textContent = data.message;
if (structureSelect?.value) await PTA.modules.planning?.loadAgents?.();
if (document.querySelector('#overview-structure')?.value === String(row.dataset.structureId)) {
await PTA.modules.structureOverview?.load?.();
}
if (document.querySelector('#overview-agent')?.value) await PTA.modules.agentOverview?.load?.();
} catch (error) {
message.className = 'message message-error';
message.textContent = error.message;
} finally {
button.disabled = false;
}
}
function addOptionIfMissing(select, value, label, selected = false) {
if (!select) return;
let option = Array.from(select.options).find((item) => item.value === String(value));
if (!option) {
option = document.createElement('option');
option.value = String(value);
select.appendChild(option);
}
option.textContent = label;
if (selected) option.selected = true;
}
function structureOptionLabel(structure) {
return `${structure.nom} — ${typeLabel(structure.type_affectation)}`;
}
function addStructureEverywhere(structure) {
const id = Number(structure.id_structure);
const label = structureOptionLabel(structure);
[
document.querySelector('#structure'),
document.querySelector('#overview-structure'),
document.querySelector('#new-agent-structure'),
document.querySelector('#edit-agent-entry-structure'),
document.querySelector('#edit-agent-profile-structure'),
document.querySelector('#coverage-structure-filter'),
document.querySelector('#coverage-rules-structure'),
].forEach((select) => addOptionIfMissing(select, id, label));
document.querySelectorAll('.agent-structure-select').forEach((select) => {
addOptionIfMissing(select, id, label);
});
}
function updateStructureLabels(structureId, type) {
const source = document.querySelector(`#structure-settings-body tr[data-structure-id="${Number(structureId)}"]`);
const structureName = source?.querySelector('td strong')?.textContent?.trim();
if (!structureName) return;
const label = `${structureName} — ${typeLabel(type)}`;
document.querySelectorAll('select').forEach((select) => {
const option = Array.from(select.options).find((item) => item.value === String(structureId));
if (option && (
select.matches('#structure, #overview-structure, #new-agent-structure, #edit-agent-entry-structure, #edit-agent-profile-structure, #coverage-structure-filter, #coverage-rules-structure, .agent-structure-select')
)) {
option.textContent = label;
}
});
}
function structureOptionsHtml(selectedStructureId = null) {
const source = document.querySelector('#new-agent-structure');
if (!source) return '<option value="">Sans lieu principal</option>';
const options = ['<option value="">Sans lieu principal</option>'];
Array.from(source.options).forEach((option) => {
if (!option.value) return;
const selected = Number(option.value) === Number(selectedStructureId) ? ' selected' : '';
options.push(`<option value="${Number(option.value)}"${selected}>${escapeHtml(option.textContent.trim())}</option>`);
});
return options.join('');
}
function addStructureSettingsRow(structure) {
const body = document.querySelector('#structure-settings-body');
if (!body || body.querySelector(`tr[data-structure-id="${Number(structure.id_structure)}"]`)) return;
const row = document.createElement('tr');
row.dataset.structureId = String(Number(structure.id_structure));
row.innerHTML = `
<td><strong>${escapeHtml(structure.nom)}</strong></td>
<td>${escapeHtml(structure.code)}</td>
<td>
<select class="structure-type-select" aria-label="Type d’affectation de ${escapeHtml(structure.nom)}">
<option value="PERISCOLAIRE" ${structure.type_affectation === 'PERISCOLAIRE' ? 'selected' : ''}>Périscolaire</option>
<option value="EXTRASCOLAIRE" ${structure.type_affectation === 'EXTRASCOLAIRE' ? 'selected' : ''}>Extrascolaire</option>
</select>
</td>
<td><button type="button" class="button button-secondary save-structure-type">Enregistrer</button></td>
`;
body.appendChild(row);
}
function qualificationLabel(agent) {
if (!Number(agent.est_diplome)) return 'Non diplômé';
return agent.diplome_libelle ? `Diplômé — ${agent.diplome_libelle}` : 'Diplômé';
}
function formatContractDate(value) {
if (!value) return 'Non renseignée';
const [year, month, day] = String(value).split('-');
return year && month && day ? `${day}/${month}/${year}` : value;
}
function agentRowHtml(agent) {
const structureLabel = agent.structure_nom || agent.structure_principale_nom || 'Sans lieu principal';
return `
<td><strong class="agent-row-name">${escapeHtml(agent.prenom)} ${escapeHtml(agent.nom)}</strong></td>
<td>${escapeHtml(agent.matricule)}</td>
<td class="agent-row-post">${escapeHtml(agent.poste_libelle || 'Non renseigné')}</td>
<td class="agent-row-qualification">${escapeHtml(qualificationLabel(agent))}</td>
<td class="agent-row-phone">${escapeHtml(agent.telephone || 'Non renseigné')}</td>
<td class="agent-row-contract-start">${escapeHtml(formatContractDate(agent.date_debut_contrat))}</td>
<td class="agent-row-structure">${escapeHtml(structureLabel)}</td>
<td><button type="button" class="button button-secondary edit-agent-profile">Modifier</button></td>
`;
}
function setAgentRowData(row, agent) {
row.dataset.agentId = String(Number(agent.id_agent));
row.dataset.email = agent.email || '';
row.dataset.telephone = agent.telephone || '';
row.dataset.adresse = agent.adresse || '';
row.dataset.estDiplome = Number(agent.est_diplome) ? '1' : '0';
row.dataset.diplomeLibelle = agent.diplome_libelle || '';
row.dataset.dateDebutContrat = agent.date_debut_contrat || '';
row.dataset.dateFinContrat = agent.date_fin_contrat || '';
row.dataset.idPoste = agent.id_poste ? String(Number(agent.id_poste)) : '';
row.dataset.typeContrat = agent.type_contrat || 'PERMANENT';
row.dataset.formationRepartitionAnnuelle = Number(agent.formation_repartition_annuelle) ? '1' : '0';
row.dataset.droitEnveloppeFormation = Number(agent.droit_enveloppe_formation ?? 1) ? '1' : '0';
row.dataset.commentairePta = agent.commentaire_pta || '';
row.dataset.structureId = agent.id_structure ? String(Number(agent.id_structure)) : '';
}
function addAgentEverywhere(agent) {
const overviewAgent = document.querySelector('#overview-agent');
addOptionIfMissing(
overviewAgent,
Number(agent.id_agent),
`${agent.prenom} ${agent.nom} (${agent.matricule})`
);
const body = document.querySelector('#agent-assignment-body');
if (body && !body.querySelector(`tr[data-agent-id="${Number(agent.id_agent)}"]`)) {
const row = document.createElement('tr');
setAgentRowData(row, agent);
row.innerHTML = agentRowHtml(agent);
body.appendChild(row);
}
}
function toggleDiplomaField(select, input) {
if (!select || !input) return;
const qualified = select.value === '1';
input.disabled = !qualified;
if (!qualified) input.value = '';
}
function openAgentProfileModal(row) {
const modal = document.querySelector('#agent-profile-modal');
if (!modal || !row) return;
document.querySelector('#edit-agent-profile-id').value = row.dataset.agentId || '';
document.querySelector('#agent-profile-modal-subtitle').textContent = row.querySelector('.agent-row-name')?.textContent?.trim() || '';
document.querySelector('#edit-agent-profile-email').value = row.dataset.email || '';
document.querySelector('#edit-agent-profile-phone').value = row.dataset.telephone || '';
document.querySelector('#edit-agent-profile-address').value = row.dataset.adresse || '';
document.querySelector('#edit-agent-profile-qualified').value = row.dataset.estDiplome === '1' ? '1' : '0';
document.querySelector('#edit-agent-profile-diploma').value = row.dataset.diplomeLibelle || '';
document.querySelector('#edit-agent-profile-contract-start').value = row.dataset.dateDebutContrat || '';
document.querySelector('#edit-agent-profile-contract-end').value = row.dataset.dateFinContrat || '';
document.querySelector('#edit-agent-profile-post').value = row.dataset.idPoste || '';
document.querySelector('#edit-agent-profile-contract-type').value = row.dataset.typeContrat || 'PERMANENT';
document.querySelector('#edit-agent-profile-training-planned').checked = row.dataset.formationRepartitionAnnuelle === '1';
document.querySelector('#edit-agent-profile-training-right').checked = row.dataset.droitEnveloppeFormation !== '0';
document.querySelector('#edit-agent-profile-pta-comment').value = row.dataset.commentairePta || '';
document.querySelector('#edit-agent-profile-structure').value = row.dataset.structureId || '';
toggleDiplomaField(document.querySelector('#edit-agent-profile-qualified'), document.querySelector('#edit-agent-profile-diploma'));
const message = document.querySelector('#agent-profile-message');
if (message) {
message.className = 'message';
message.textContent = '';
}
modal.hidden = false;
}
function closeAgentProfileModal() {
const modal = document.querySelector('#agent-profile-modal');
if (modal) modal.hidden = true;
}
async function saveAgentProfile(event) {
event.preventDefault();
const form = event.currentTarget;
const message = document.querySelector('#agent-profile-message');
const submit = form.querySelector('button[type="submit"]');
const agentId = Number(document.querySelector('#edit-agent-profile-id').value);
if (!agentId || !message || !submit) return;
submit.disabled = true;
message.className = 'message';
message.textContent = '';
try {
const structureValue = document.querySelector('#edit-agent-profile-structure').value;
const response = await fetch('api/update_agent.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
agent_id: agentId,
email: document.querySelector('#edit-agent-profile-email').value,
telephone: document.querySelector('#edit-agent-profile-phone').value,
adresse: document.querySelector('#edit-agent-profile-address').value,
est_diplome: document.querySelector('#edit-agent-profile-qualified').value === '1',
diplome_libelle: document.querySelector('#edit-agent-profile-diploma').value,
date_debut_contrat: document.querySelector('#edit-agent-profile-contract-start').value,
date_fin_contrat: document.querySelector('#edit-agent-profile-contract-end').value,
id_poste: Number(document.querySelector('#edit-agent-profile-post').value),
type_contrat: document.querySelector('#edit-agent-profile-contract-type').value,
formation_repartition_annuelle: document.querySelector('#edit-agent-profile-training-planned').checked,
droit_enveloppe_formation: document.querySelector('#edit-agent-profile-training-right').checked,
commentaire_pta: document.querySelector('#edit-agent-profile-pta-comment').value,
structure_id: structureValue ? Number(structureValue) : null,
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de modifier les informations de l’agent.'));
const row = document.querySelector(`#agent-assignment-body tr[data-agent-id="${agentId}"]`);
if (row) {
setAgentRowData(row, data.agent);
row.innerHTML = agentRowHtml({
...data.agent,
structure_nom: data.agent.structure_principale_nom,
});
}
message.className = 'message message-success';
message.textContent = data.message;
if (structureSelect?.value) await PTA.modules.planning?.loadAgents?.();
if (document.querySelector('#overview-structure')?.value) await PTA.modules.structureOverview?.load?.();
if (document.querySelector('#overview-agent')?.value === String(agentId)) await PTA.modules.agentOverview?.load?.();
setTimeout(closeAgentProfileModal, 500);
} catch (error) {
message.className = 'message message-error';
message.textContent = error.message;
} finally {
submit.disabled = false;
}
}
async function createStructure(event) {
event.preventDefault();
const form = event.currentTarget;
const message = document.querySelector('#create-structure-message');
const submit = form.querySelector('button[type="submit"]');
if (!message || !submit) return;
message.className = 'message';
message.textContent = '';
submit.disabled = true;
try {
const response = await fetch('api/create_structure.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
code: document.querySelector('#new-structure-code').value,
nom: document.querySelector('#new-structure-name').value,
adresse: document.querySelector('#new-structure-address').value,
type_affectation: document.querySelector('#new-structure-type').value,
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de créer le lieu d’affectation.'));
addStructureEverywhere(data.structure);
addStructureSettingsRow(data.structure);
form.reset();
document.querySelector('#new-structure-type').value = 'PERISCOLAIRE';
message.className = 'message message-success';
message.textContent = data.message;
PTA.modules.coverage?.refresh?.({ silent: true });
} catch (error) {
message.className = 'message message-error';
message.textContent = error.message;
} finally {
submit.disabled = false;
}
}
async function createAgent(event) {
event.preventDefault();
const form = event.currentTarget;
const message = document.querySelector('#create-agent-message');
const submit = form.querySelector('button[type="submit"]');
if (!message || !submit) return;
message.className = 'message';
message.textContent = '';
submit.disabled = true;
try {
const response = await fetch('api/create_agent.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
matricule: document.querySelector('#new-agent-matricule').value,
nom: document.querySelector('#new-agent-name').value,
prenom: document.querySelector('#new-agent-firstname').value,
email: document.querySelector('#new-agent-email').value,
telephone: document.querySelector('#new-agent-phone').value,
adresse: document.querySelector('#new-agent-address').value,
est_diplome: document.querySelector('#new-agent-qualified').value === '1',
diplome_libelle: document.querySelector('#new-agent-diploma').value,
structure_id: Number(document.querySelector('#new-agent-structure').value),
quotite_travail: Number(document.querySelector('#new-agent-rate').value),
date_debut_contrat: document.querySelector('#new-agent-contract-start').value,
date_fin_contrat: document.querySelector('#new-agent-contract-end').value,
id_poste: Number(document.querySelector('#new-agent-post').value),
type_contrat: document.querySelector('#new-agent-contract-type').value,
formation_repartition_annuelle: document.querySelector('#new-agent-training-planned').checked,
droit_enveloppe_formation: document.querySelector('#new-agent-training-right').checked,
commentaire_pta: document.querySelector('#new-agent-pta-comment').value,
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de créer l’agent.'));
addAgentEverywhere(data.agent);
const createdStructureId = String(data.agent.id_structure);
form.reset();
document.querySelector('#new-agent-rate').value = '100';
document.querySelector('#new-agent-contract-type').value = 'PERMANENT';
document.querySelector('#new-agent-training-planned').checked = false;
document.querySelector('#new-agent-qualified').value = '0';
toggleDiplomaField(document.querySelector('#new-agent-qualified'), document.querySelector('#new-agent-diploma'));
message.className = 'message message-success';
message.textContent = `${data.message} Quota annuel : ${formatDuration(Number(data.agent.quota_cible_minutes))}, suivi du 1er janvier au 31 décembre.`;
if (structureSelect?.value === createdStructureId) {
await PTA.modules.planning?.loadAgents?.();
}
if (document.querySelector('#overview-structure')?.value === createdStructureId) {
await PTA.modules.structureOverview?.load?.();
}
} catch (error) {
message.className = 'message message-error';
message.textContent = error.message;
} finally {
submit.disabled = false;
}
}
function init() {
document.querySelector('#agent-assignment-body')?.addEventListener('click', (event) => {
const button = event.target.closest('.edit-agent-profile');
if (button) openAgentProfileModal(button.closest('tr[data-agent-id]'));
});
document.querySelector('#structure-settings-body')?.addEventListener('click', (event) => {
const button = event.target.closest('.save-structure-type');
if (button) saveStructureType(button);
});
document.querySelector('#new-agent-qualified')?.addEventListener('change', (event) => {
toggleDiplomaField(event.currentTarget, document.querySelector('#new-agent-diploma'));
});
document.querySelector('#edit-agent-profile-qualified')?.addEventListener('change', (event) => {
toggleDiplomaField(event.currentTarget, document.querySelector('#edit-agent-profile-diploma'));
});
document.querySelector('#close-agent-profile-modal')?.addEventListener('click', closeAgentProfileModal);
document.querySelector('#cancel-agent-profile')?.addEventListener('click', closeAgentProfileModal);
document.querySelector('#agent-profile-modal')?.addEventListener('click', (event) => {
if (event.target.id === 'agent-profile-modal') closeAgentProfileModal();
});
document.querySelector('#agent-profile-form')?.addEventListener('submit', saveAgentProfile);
document.querySelector('#create-structure-form')?.addEventListener('submit', createStructure);
document.querySelector('#create-agent-form')?.addEventListener('submit', createAgent);
toggleDiplomaField(document.querySelector('#new-agent-qualified'), document.querySelector('#new-agent-diploma'));
}
PTA.modules.administration = {
init,
saveAgentStructure,
saveStructureType,
addStructureEverywhere,
addAgentEverywhere,
openAgentProfileModal,
};
})();