V7.10 — Tableau de bord, guide, semaine type et enveloppe de formation
Reprise dans le répertoire de production du travail mené dans le worktree ux-service-enfance : - tableau de bord Service Enfance (dashboard.php) et navigation regroupée ; - guide d'utilisation (faq.php) accessible depuis toute la navigation ; - éditeur de semaine type mis en avant sur l'écran de saisie ; - droit à l'enveloppe de formation par agent, en remplacement de la case « formation déjà prévue dans la répartition annuelle » ; - correctif de « [object Object] » sur les cartes de quota du PTA ; - motif de travail par défaut à la saisie, heures travaillées complètes dans les PDF. Le docker-compose.yml de production est conservé : celui du worktree décrit une pile de test (conteneurs et volume suffixés « test », ports 9080/9081), qui aurait fait redémarrer l'application sur un volume MariaDB vide. Les migrations database/migration_v7_9*.sql sont obligatoires ; le service db-migrate les applique au déploiement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
579
assets/js/week-template-editor.js
Normal file
579
assets/js/week-template-editor.js
Normal file
@@ -0,0 +1,579 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const PTA = window.PTA;
|
||||
if (!PTA) throw new Error('PTA core doit être chargé avant week-template-editor.js');
|
||||
|
||||
const { state } = PTA;
|
||||
const { structureSelect, agentSelect, weekInput } = PTA.elements;
|
||||
const { escapeHtml, readJsonResponse, apiErrorMessage, formatDuration, timeToMinutes, motifInfo } = PTA.utils;
|
||||
|
||||
const panel = document.querySelector('#template-mode-panel');
|
||||
const weekPanel = document.querySelector('#week-mode-panel');
|
||||
const modeTemplateButton = document.querySelector('#editor-mode-template');
|
||||
const modeWeekButton = document.querySelector('#editor-mode-week');
|
||||
|
||||
const templateSelect = document.querySelector('#template-editor-select');
|
||||
const nameInput = document.querySelector('#template-editor-name');
|
||||
const gridElement = document.querySelector('#template-editor-grid');
|
||||
const emptyElement = document.querySelector('#template-editor-empty');
|
||||
const messageElement = document.querySelector('#template-editor-message');
|
||||
const totalElement = document.querySelector('#template-editor-total');
|
||||
const totalDetailElement = document.querySelector('#template-editor-total-detail');
|
||||
|
||||
const saveButton = document.querySelector('#template-editor-save');
|
||||
const deleteButton = document.querySelector('#template-editor-delete');
|
||||
const applyPeriodButton = document.querySelector('#template-editor-apply-period');
|
||||
const applyWeekButton = document.querySelector('#template-editor-apply-week');
|
||||
|
||||
const DAYS = [
|
||||
{ weekday: 1, label: 'Lundi' },
|
||||
{ weekday: 2, label: 'Mardi' },
|
||||
{ weekday: 3, label: 'Mercredi' },
|
||||
{ weekday: 4, label: 'Jeudi' },
|
||||
{ weekday: 5, label: 'Vendredi' },
|
||||
];
|
||||
|
||||
/** Plages de la semaine type en cours d'édition, indexées par jour. */
|
||||
let draft = new Map();
|
||||
let templates = [];
|
||||
let currentTemplateId = null;
|
||||
let loading = false;
|
||||
|
||||
function setMessage(text = '', type = 'info') {
|
||||
if (!messageElement) return;
|
||||
messageElement.className = text ? `message message-${type}` : 'message';
|
||||
messageElement.textContent = text;
|
||||
}
|
||||
|
||||
function agentId() {
|
||||
return Number(agentSelect?.value || 0) || null;
|
||||
}
|
||||
|
||||
function defaultStructureId() {
|
||||
const selected = Number(structureSelect?.value || 0);
|
||||
if (selected) return selected;
|
||||
const first = structureSelect?.querySelector('option[value]:not([value=""])');
|
||||
return first ? Number(first.value) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Un agent sans droit à l'enveloppe de formation ne peut pas recevoir le
|
||||
* motif « Formation ». Même règle qu'à la saisie semaine par semaine : le
|
||||
* droit vaut tant que l'agent n'est pas chargé, on ne retire jamais une
|
||||
* possibilité sur une donnée absente.
|
||||
*/
|
||||
function agentHasTrainingRight() {
|
||||
return Number(state.currentAgent?.droit_enveloppe_formation ?? 1) !== 0;
|
||||
}
|
||||
|
||||
function motifs() {
|
||||
return window.PTA_CONFIG?.motifs || [];
|
||||
}
|
||||
|
||||
function defaultMotifId() {
|
||||
const list = motifs();
|
||||
const motif = list.find((item) => String(item.code).toUpperCase() === 'TRAVAIL')
|
||||
|| list.find((item) => item.quota)
|
||||
|| list[0];
|
||||
return motif ? Number(motif.id) : null;
|
||||
}
|
||||
|
||||
function motifOptionsHtml(selectedMotifId = null) {
|
||||
return motifs()
|
||||
// Un motif interdit reste proposé s'il est déjà celui de la plage :
|
||||
// le retirer changerait silencieusement une saisie existante.
|
||||
.filter((motif) => agentHasTrainingRight()
|
||||
|| String(motif.code).toUpperCase() !== 'FORMATION'
|
||||
|| Number(motif.id) === Number(selectedMotifId))
|
||||
.map((motif) => {
|
||||
const selected = Number(motif.id) === Number(selectedMotifId) ? ' selected' : '';
|
||||
const quotaLabel = motif.quota ? ' · décompte quota' : ' · hors quota';
|
||||
return `<option value="${Number(motif.id)}"${selected}>${escapeHtml(motif.label)}${quotaLabel}</option>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function structureOptionsHtml(selectedStructureId = null) {
|
||||
return [...(structureSelect?.querySelectorAll('option[value]:not([value=""])') || [])]
|
||||
.map((option) => {
|
||||
const selected = Number(option.value) === Number(selectedStructureId) ? ' selected' : '';
|
||||
return `<option value="${Number(option.value)}"${selected}>${escapeHtml(option.textContent.trim())}</option>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function copyTargetOptionsHtml(weekday) {
|
||||
const others = DAYS.filter((day) => day.weekday !== weekday);
|
||||
return [
|
||||
'<option value="">Copier vers…</option>',
|
||||
...others.map((day) => `<option value="${day.weekday}">${escapeHtml(day.label)}</option>`),
|
||||
'<option value="all">Tous les autres jours</option>',
|
||||
].join('');
|
||||
}
|
||||
|
||||
function rangesFor(weekday) {
|
||||
return draft.get(weekday) || [];
|
||||
}
|
||||
|
||||
function emptyDraft() {
|
||||
const next = new Map();
|
||||
DAYS.forEach((day) => next.set(day.weekday, []));
|
||||
return next;
|
||||
}
|
||||
|
||||
function newRange() {
|
||||
return {
|
||||
heure_debut: '',
|
||||
heure_fin: '',
|
||||
id_motif: defaultMotifId(),
|
||||
id_structure: defaultStructureId(),
|
||||
};
|
||||
}
|
||||
|
||||
function rangeMinutes(range) {
|
||||
if (!range.heure_debut || !range.heure_fin) return 0;
|
||||
return Math.max(0, timeToMinutes(range.heure_fin) - timeToMinutes(range.heure_debut));
|
||||
}
|
||||
|
||||
function dayMinutes(weekday) {
|
||||
return rangesFor(weekday).reduce((total, range) => total + rangeMinutes(range), 0);
|
||||
}
|
||||
|
||||
function totals() {
|
||||
let all = 0;
|
||||
let counted = 0;
|
||||
DAYS.forEach((day) => {
|
||||
rangesFor(day.weekday).forEach((range) => {
|
||||
const minutes = rangeMinutes(range);
|
||||
all += minutes;
|
||||
if (motifInfo(range.id_motif)?.quota) counted += minutes;
|
||||
});
|
||||
});
|
||||
return { all, counted };
|
||||
}
|
||||
|
||||
function rangeHtml(weekday, index, range) {
|
||||
const label = `${DAYS.find((day) => day.weekday === weekday)?.label || 'Jour'} plage ${index + 1}`;
|
||||
return `
|
||||
<div class="template-range" data-weekday="${weekday}" data-index="${index}">
|
||||
<label class="template-range-field template-range-field-motif">
|
||||
<span>Motif</span>
|
||||
<select class="template-range-motif" aria-label="Motif ${escapeHtml(label)}">
|
||||
${motifOptionsHtml(range.id_motif)}
|
||||
</select>
|
||||
</label>
|
||||
<label class="template-range-field">
|
||||
<span>Début</span>
|
||||
<input class="template-range-start" type="time" step="900" value="${escapeHtml(range.heure_debut)}" aria-label="Début ${escapeHtml(label)}">
|
||||
</label>
|
||||
<span class="template-range-separator" aria-hidden="true">→</span>
|
||||
<label class="template-range-field">
|
||||
<span>Fin</span>
|
||||
<input class="template-range-end" type="time" step="900" value="${escapeHtml(range.heure_fin)}" aria-label="Fin ${escapeHtml(label)}">
|
||||
</label>
|
||||
<label class="template-range-field template-range-field-structure">
|
||||
<span>Lieu</span>
|
||||
<select class="template-range-structure" aria-label="Lieu ${escapeHtml(label)}">
|
||||
${structureOptionsHtml(range.id_structure)}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="button button-ghost template-range-remove" aria-label="Supprimer ${escapeHtml(label)}" title="Supprimer cette plage">×</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!gridElement) return;
|
||||
|
||||
const ready = Boolean(agentId());
|
||||
if (emptyElement) emptyElement.hidden = ready;
|
||||
gridElement.hidden = !ready;
|
||||
|
||||
if (ready) {
|
||||
gridElement.innerHTML = DAYS.map((day) => {
|
||||
const ranges = rangesFor(day.weekday);
|
||||
const minutes = dayMinutes(day.weekday);
|
||||
return `
|
||||
<article class="template-day${ranges.length === 0 ? ' is-empty' : ''}" data-weekday="${day.weekday}">
|
||||
<header class="template-day-header">
|
||||
<strong>${escapeHtml(day.label)}</strong>
|
||||
<span class="template-day-total">${minutes > 0 ? formatDuration(minutes) : '—'}</span>
|
||||
</header>
|
||||
<div class="template-day-ranges">
|
||||
${ranges.length === 0
|
||||
? '<p class="template-day-empty">Aucune plage — jour non travaillé.</p>'
|
||||
: ranges.map((range, index) => rangeHtml(day.weekday, index, range)).join('')}
|
||||
</div>
|
||||
<footer class="template-day-actions">
|
||||
<button type="button" class="button button-secondary template-day-add" data-weekday="${day.weekday}">+ Ajouter une plage</button>
|
||||
<label class="template-day-copy-field">
|
||||
<span class="visually-hidden">Copier ${escapeHtml(day.label)} vers un autre jour</span>
|
||||
<select class="template-day-copy" data-weekday="${day.weekday}" aria-label="Copier ${escapeHtml(day.label)} vers un autre jour"${ranges.length === 0 ? ' disabled' : ''}>
|
||||
${copyTargetOptionsHtml(day.weekday)}
|
||||
</select>
|
||||
</label>
|
||||
</footer>
|
||||
</article>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const { all, counted } = totals();
|
||||
if (totalElement) totalElement.textContent = formatDuration(all);
|
||||
if (totalDetailElement) totalDetailElement.textContent = `dont ${formatDuration(counted)} décomptées`;
|
||||
|
||||
refreshButtons();
|
||||
}
|
||||
|
||||
function hasAnyRange() {
|
||||
return DAYS.some((day) => rangesFor(day.weekday).length > 0);
|
||||
}
|
||||
|
||||
function refreshButtons() {
|
||||
const ready = Boolean(agentId());
|
||||
const named = Boolean(nameInput?.value.trim());
|
||||
|
||||
if (templateSelect) templateSelect.disabled = !ready;
|
||||
if (nameInput) nameInput.disabled = !ready;
|
||||
if (saveButton) saveButton.disabled = !ready || !named || !hasAnyRange() || loading;
|
||||
if (deleteButton) deleteButton.disabled = !ready || currentTemplateId === null || loading;
|
||||
// Dupliquer suppose un modèle déjà enregistré : on ne duplique jamais un
|
||||
// brouillon d'écran, seulement ce qui est réellement en base.
|
||||
if (applyPeriodButton) applyPeriodButton.disabled = !ready || currentTemplateId === null || loading;
|
||||
if (applyWeekButton) applyWeekButton.disabled = !ready || currentTemplateId === null || loading;
|
||||
}
|
||||
|
||||
function renderTemplateSelect() {
|
||||
if (!templateSelect) return;
|
||||
const options = ['<option value="">➕ Nouvelle semaine type</option>'];
|
||||
templates.forEach((template) => {
|
||||
const selected = Number(template.id) === Number(currentTemplateId) ? ' selected' : '';
|
||||
const detail = `${template.entry_count} plage${template.entry_count > 1 ? 's' : ''}`;
|
||||
options.push(`<option value="${Number(template.id)}"${selected}>${escapeHtml(template.name)} — ${detail}</option>`);
|
||||
});
|
||||
templateSelect.innerHTML = options.join('');
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
const agent = agentId();
|
||||
if (!agent) {
|
||||
templates = [];
|
||||
renderTemplateSelect();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`api/week_templates.php?agent_id=${agent}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const data = await readJsonResponse(response);
|
||||
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger les semaines types.'));
|
||||
templates = Array.isArray(data.templates) ? data.templates : [];
|
||||
renderTemplateSelect();
|
||||
} catch (error) {
|
||||
setMessage(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function openTemplate(templateId) {
|
||||
const agent = agentId();
|
||||
if (!agent || !templateId) {
|
||||
currentTemplateId = null;
|
||||
draft = emptyDraft();
|
||||
if (nameInput) nameInput.value = '';
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
refreshButtons();
|
||||
try {
|
||||
const response = await fetch(`api/week_template.php?agent_id=${agent}&template_id=${Number(templateId)}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const data = await readJsonResponse(response);
|
||||
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’ouvrir cette semaine type.'));
|
||||
|
||||
currentTemplateId = Number(data.template.id);
|
||||
if (nameInput) nameInput.value = data.template.name;
|
||||
|
||||
draft = emptyDraft();
|
||||
(data.entries || []).forEach((entry) => {
|
||||
const weekday = Number(entry.jour_semaine);
|
||||
if (!draft.has(weekday)) return;
|
||||
draft.get(weekday).push({
|
||||
heure_debut: entry.heure_debut,
|
||||
heure_fin: entry.heure_fin,
|
||||
id_motif: Number(entry.id_motif),
|
||||
id_structure: Number(entry.id_structure),
|
||||
});
|
||||
});
|
||||
setMessage('');
|
||||
} catch (error) {
|
||||
setMessage(error.message, 'error');
|
||||
} finally {
|
||||
loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
/** Lit les valeurs saisies à l'écran dans le brouillon en mémoire. */
|
||||
function syncDraftFromDom() {
|
||||
if (!gridElement || gridElement.hidden) return;
|
||||
const next = emptyDraft();
|
||||
gridElement.querySelectorAll('.template-range').forEach((element) => {
|
||||
const weekday = Number(element.dataset.weekday);
|
||||
if (!next.has(weekday)) return;
|
||||
next.get(weekday).push({
|
||||
heure_debut: element.querySelector('.template-range-start')?.value || '',
|
||||
heure_fin: element.querySelector('.template-range-end')?.value || '',
|
||||
id_motif: Number(element.querySelector('.template-range-motif')?.value || 0) || null,
|
||||
id_structure: Number(element.querySelector('.template-range-structure')?.value || 0) || null,
|
||||
});
|
||||
});
|
||||
draft = next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contrôles faits à l'écran pour éviter un aller-retour serveur inutile.
|
||||
* Le serveur revalide tout : ceci n'est qu'une commodité.
|
||||
*/
|
||||
function localValidationError() {
|
||||
for (const day of DAYS) {
|
||||
const ranges = rangesFor(day.weekday);
|
||||
for (const range of ranges) {
|
||||
if (!range.heure_debut || !range.heure_fin) {
|
||||
return `${day.label} : une plage n’a pas d’horaire de début ou de fin.`;
|
||||
}
|
||||
if (timeToMinutes(range.heure_fin) <= timeToMinutes(range.heure_debut)) {
|
||||
return `${day.label} : la fin doit être postérieure au début (${range.heure_debut} → ${range.heure_fin}).`;
|
||||
}
|
||||
if (!range.id_motif || !range.id_structure) {
|
||||
return `${day.label} : chaque plage doit avoir un motif et un lieu d’affectation.`;
|
||||
}
|
||||
}
|
||||
const sorted = [...ranges].sort((a, b) => a.heure_debut.localeCompare(b.heure_debut));
|
||||
for (let index = 1; index < sorted.length; index++) {
|
||||
if (sorted[index].heure_debut < sorted[index - 1].heure_fin) {
|
||||
return `${day.label} : deux plages se chevauchent (${sorted[index - 1].heure_debut}–${sorted[index - 1].heure_fin} et ${sorted[index].heure_debut}–${sorted[index].heure_fin}).`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function flatEntries() {
|
||||
const entries = [];
|
||||
DAYS.forEach((day) => {
|
||||
rangesFor(day.weekday).forEach((range) => {
|
||||
entries.push({
|
||||
jour_semaine: day.weekday,
|
||||
heure_debut: range.heure_debut,
|
||||
heure_fin: range.heure_fin,
|
||||
id_motif: range.id_motif,
|
||||
id_structure: range.id_structure,
|
||||
});
|
||||
});
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function save(replace = false) {
|
||||
const agent = agentId();
|
||||
const name = nameInput?.value.trim() || '';
|
||||
if (!agent || !name) return;
|
||||
|
||||
syncDraftFromDom();
|
||||
const error = localValidationError();
|
||||
if (error) {
|
||||
setMessage(error, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
refreshButtons();
|
||||
try {
|
||||
const response = await fetch('api/save_week_template_entries.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({
|
||||
csrf_token: window.PTA_CONFIG.csrfToken,
|
||||
agent_id: agent,
|
||||
name,
|
||||
replace,
|
||||
entries: flatEntries(),
|
||||
}),
|
||||
});
|
||||
const data = await readJsonResponse(response);
|
||||
|
||||
if (response.status === 409 && data.code === 'TEMPLATE_EXISTS' && !replace) {
|
||||
loading = false;
|
||||
if (window.confirm(`Une semaine type « ${name} » existe déjà pour cet agent. La remplacer ?`)) {
|
||||
await save(true);
|
||||
return;
|
||||
}
|
||||
setMessage('Enregistrement annulé : le nom est déjà utilisé.', 'warning');
|
||||
refreshButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible d’enregistrer la semaine type.'));
|
||||
|
||||
currentTemplateId = Number(data.template.id);
|
||||
setMessage(`${data.message} Elle peut maintenant être dupliquée sur une période.`, 'success');
|
||||
await loadTemplates();
|
||||
renderTemplateSelect();
|
||||
} catch (err) {
|
||||
setMessage(err.message, 'error');
|
||||
} finally {
|
||||
loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
async function removeTemplate() {
|
||||
const agent = agentId();
|
||||
if (!agent || currentTemplateId === null) return;
|
||||
const template = templates.find((item) => Number(item.id) === Number(currentTemplateId));
|
||||
if (!window.confirm(`Supprimer définitivement la semaine type « ${template?.name || ''} » ? Les plannings déjà créés ne sont pas affectés.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
refreshButtons();
|
||||
try {
|
||||
const response = await fetch('api/delete_week_template.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({
|
||||
csrf_token: window.PTA_CONFIG.csrfToken,
|
||||
agent_id: agent,
|
||||
template_id: currentTemplateId,
|
||||
}),
|
||||
});
|
||||
const data = await readJsonResponse(response);
|
||||
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de supprimer la semaine type.'));
|
||||
|
||||
currentTemplateId = null;
|
||||
draft = emptyDraft();
|
||||
if (nameInput) nameInput.value = '';
|
||||
setMessage(data.message || 'Semaine type supprimée.', 'success');
|
||||
await loadTemplates();
|
||||
} catch (error) {
|
||||
setMessage(error.message, 'error');
|
||||
} finally {
|
||||
loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function copyDay(sourceWeekday, target) {
|
||||
syncDraftFromDom();
|
||||
const source = rangesFor(sourceWeekday);
|
||||
if (source.length === 0) return;
|
||||
|
||||
const targets = target === 'all'
|
||||
? DAYS.filter((day) => day.weekday !== sourceWeekday).map((day) => day.weekday)
|
||||
: [Number(target)];
|
||||
|
||||
targets.forEach((weekday) => {
|
||||
draft.set(weekday, source.map((range) => ({ ...range })));
|
||||
});
|
||||
|
||||
const sourceLabel = DAYS.find((day) => day.weekday === sourceWeekday)?.label || 'Ce jour';
|
||||
const targetLabel = target === 'all'
|
||||
? 'tous les autres jours'
|
||||
: (DAYS.find((day) => day.weekday === Number(target))?.label || 'le jour choisi').toLowerCase();
|
||||
setMessage(`${sourceLabel} copié sur ${targetLabel}. Pensez à enregistrer la semaine type.`, 'info');
|
||||
render();
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
const isTemplate = mode === 'template';
|
||||
if (panel) panel.hidden = !isTemplate;
|
||||
if (weekPanel) weekPanel.hidden = isTemplate;
|
||||
modeTemplateButton?.classList.toggle('is-active', isTemplate);
|
||||
modeWeekButton?.classList.toggle('is-active', !isTemplate);
|
||||
modeTemplateButton?.setAttribute('aria-selected', String(isTemplate));
|
||||
modeWeekButton?.setAttribute('aria-selected', String(!isTemplate));
|
||||
}
|
||||
|
||||
function onAgentChanged() {
|
||||
currentTemplateId = null;
|
||||
draft = emptyDraft();
|
||||
if (nameInput) nameInput.value = '';
|
||||
setMessage('');
|
||||
loadTemplates().then(render);
|
||||
}
|
||||
|
||||
function init() {
|
||||
draft = emptyDraft();
|
||||
|
||||
modeTemplateButton?.addEventListener('click', () => setMode('template'));
|
||||
modeWeekButton?.addEventListener('click', () => setMode('week'));
|
||||
|
||||
agentSelect?.addEventListener('change', onAgentChanged);
|
||||
structureSelect?.addEventListener('change', () => {
|
||||
// Le lieu par défaut des nouvelles plages suit la sélection.
|
||||
loadTemplates();
|
||||
});
|
||||
|
||||
templateSelect?.addEventListener('change', (event) => openTemplate(event.target.value));
|
||||
nameInput?.addEventListener('input', refreshButtons);
|
||||
|
||||
gridElement?.addEventListener('click', (event) => {
|
||||
const addButton = event.target.closest('.template-day-add');
|
||||
if (addButton) {
|
||||
syncDraftFromDom();
|
||||
const weekday = Number(addButton.dataset.weekday);
|
||||
draft.get(weekday)?.push(newRange());
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
const removeButton = event.target.closest('.template-range-remove');
|
||||
if (removeButton) {
|
||||
const range = removeButton.closest('.template-range');
|
||||
syncDraftFromDom();
|
||||
const weekday = Number(range.dataset.weekday);
|
||||
const index = Number(range.dataset.index);
|
||||
draft.get(weekday)?.splice(index, 1);
|
||||
render();
|
||||
}
|
||||
});
|
||||
|
||||
gridElement?.addEventListener('change', (event) => {
|
||||
const copySelect = event.target.closest('.template-day-copy');
|
||||
if (copySelect) {
|
||||
const target = copySelect.value;
|
||||
copySelect.value = '';
|
||||
if (target) copyDay(Number(copySelect.dataset.weekday), target);
|
||||
return;
|
||||
}
|
||||
// Toute autre modification met simplement les totaux à jour.
|
||||
syncDraftFromDom();
|
||||
render();
|
||||
});
|
||||
|
||||
saveButton?.addEventListener('click', () => save(false));
|
||||
deleteButton?.addEventListener('click', removeTemplate);
|
||||
applyPeriodButton?.addEventListener('click', () => {
|
||||
PTA.modules.weekTemplates?.openPeriodModal?.(currentTemplateId);
|
||||
});
|
||||
applyWeekButton?.addEventListener('click', () => {
|
||||
if (!weekInput?.value) {
|
||||
setMessage('Choisissez d’abord une semaine dans la sélection en haut de page.', 'warning');
|
||||
return;
|
||||
}
|
||||
PTA.modules.weekTemplates?.openApplyModal?.(currentTemplateId);
|
||||
});
|
||||
|
||||
render();
|
||||
if (agentId()) loadTemplates().then(render);
|
||||
}
|
||||
|
||||
PTA.modules.weekTemplateEditor = {
|
||||
init,
|
||||
setMode,
|
||||
refresh: () => loadTemplates().then(render),
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user