Files
pta/assets/js/core.js
PTA 66cc8c8ade Base de travail V7.8 reprise du répertoire de travail
Commit technique : reproduit à l'identique l'état non commité de la copie
de travail (sécurisation V7.8, CSP à nonce, Session, migration des droits)
afin que le travail d'ergonomie qui suit soit isolable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:32:10 +02:00

187 lines
6.3 KiB
JavaScript

(() => {
'use strict';
const elements = {
structureSelect: document.querySelector('#structure'),
agentSelect: document.querySelector('#agent'),
weekInput: document.querySelector('#week'),
weeklyEntryBody: document.querySelector('#weekly-entry-body'),
addButton: document.querySelector('#add-entry'),
saveButton: document.querySelector('#save-draft'),
validateButton: document.querySelector('#validate-planning'),
modifyButton: document.querySelector('#modify-planning'),
planningBoard: document.querySelector('#planning-board'),
emptyState: document.querySelector('#empty-state'),
weekLabel: document.querySelector('#week-label'),
workTotal: document.querySelector('#work-total'),
statusBadge: document.querySelector('#planning-status'),
messageBox: document.querySelector('#message'),
openAgentPdfButton: document.querySelector('#open-agent-pdf'),
openStructurePdfButton: document.querySelector('#open-structure-pdf'),
quotaPanel: document.querySelector('#editor-quota'),
quotaRate: document.querySelector('#quota-rate'),
quotaTarget: document.querySelector('#quota-target'),
quotaPeriod: document.querySelector('#quota-period'),
quotaUsed: document.querySelector('#quota-used'),
quotaUsedDetail: document.querySelector('#quota-used-detail'),
quotaRemaining: document.querySelector('#quota-remaining'),
};
const state = {
entries: [],
otherStructureEntries: [],
currentStructure: null,
planningId: null,
planningStatus: null,
weekDays: [],
quota: null,
savedCurrentCountedMinutes: 0,
};
const shared = {
lastAgentOverviewData: null,
};
const dayNames = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi'];
/**
* Échappe une valeur pour l'insertion dans du HTML, texte ET attribut.
*
* La sérialisation via textContent/innerHTML n'échappe ni " ni ' : elle est
* sûre entre deux balises, mais permet de sortir d'un attribut value="…" ou
* title="…". PTA insère des libellés de vacances, des noms de lieux et des
* commentaires dans des attributs : les guillemets doivent être échappés.
*/
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function showMessage(text, type = 'info') {
if (!elements.messageBox) return;
elements.messageBox.className = `message message-${type}`;
elements.messageBox.textContent = text;
}
function clearMessage() {
if (!elements.messageBox) return;
elements.messageBox.className = 'message';
elements.messageBox.textContent = '';
}
async function readJsonResponse(response) {
const raw = await response.text();
if (!raw.trim()) {
throw new Error(`Le serveur a renvoyé une réponse vide (HTTP ${response.status}).`);
}
try {
return JSON.parse(raw);
} catch (error) {
const readable = raw
.replace(/<br\s*\/?\s*>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/&quot;/gi, '"')
.replace(/&#039;/gi, "'")
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&amp;/gi, '&')
.replace(/\s+/g, ' ')
.trim();
throw new Error(
`Le serveur PHP n'a pas renvoyé du JSON valide. ${readable.slice(0, 500) || 'Consultez les logs PHP.'}`
);
}
}
function apiErrorMessage(data, fallback) {
const base = data?.error || fallback;
return data?.details ? `${base} Détail : ${data.details}` : base;
}
function toIsoDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function parseIsoWeek(value) {
const match = /^(\d{4})-W(\d{2})$/.exec(value);
if (!match) return [];
const year = Number(match[1]);
const week = Number(match[2]);
const jan4 = new Date(year, 0, 4);
const jan4Day = jan4.getDay() || 7;
const monday = new Date(jan4);
monday.setDate(jan4.getDate() - jan4Day + 1 + (week - 1) * 7);
return Array.from({ length: 5 }, (_, index) => {
const date = new Date(monday);
date.setDate(monday.getDate() + index);
return {
date: toIsoDate(date),
label: dayNames[index],
display: date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }),
};
});
}
function timeToMinutes(time) {
const [hours, minutes] = String(time).split(':').map(Number);
return hours * 60 + minutes;
}
function formatDuration(minutes) {
const sign = minutes < 0 ? '-' : '';
const absolute = Math.abs(Math.round(minutes));
const hours = Math.floor(absolute / 60);
const mins = absolute % 60;
return `${sign}${hours} h ${String(mins).padStart(2, '0')}`;
}
function formatDate(value) {
if (!value) return '';
const [year, month, day] = String(value).split('-');
return `${day}/${month}/${year}`;
}
function motifInfo(id) {
const motif = (window.PTA_CONFIG?.motifs || []).find((item) => Number(item.id) === Number(id));
return motif ? {
id: Number(motif.id),
code: motif.code,
label: motif.label,
quota: Boolean(motif.quota),
} : null;
}
window.PTA = {
config: window.PTA_CONFIG || {},
elements,
state,
shared,
modules: {},
utils: {
escapeHtml,
showMessage,
clearMessage,
readJsonResponse,
apiErrorMessage,
parseIsoWeek,
timeToMinutes,
formatDuration,
formatDate,
motifInfo,
},
};
})();