/**
* Vue mensuelle d'un agent (`PTA.modules.agentOverview`), écran agents.php.
*
* Affiche le planning d'un agent mois par mois, TOUS LIEUX CONFONDUS, et permet
* de modifier ses créneaux sans passer par l'écran de saisie.
*
* DIFFÉRENCE ESSENTIELLE AVEC L'ÉDITEUR DE PLANNING : ici, toute modification
* part IMMÉDIATEMENT au serveur. Il n'y a pas de brouillon local, pas de bouton
* « Enregistrer » : la page ne travaille pas sur une semaine mais sur des
* créneaux déjà en base, éventuellement répartis sur plusieurs lieux et
* plusieurs plannings. Chaque geste est donc suivi d'un rechargement complet.
*
* Trois niveaux de droits se combinent : `can_edit_draft` pour modifier,
* `can_edit_cross_structure` pour toucher un créneau d'un autre lieu que le
* sien, `can_copy_full_week` pour dupliquer une semaine entière. L'interface
* masque en conséquence ; le serveur retranche de toute façon.
*/
(() => {
'use strict';
const PTA = window.PTA;
if (!PTA) throw new Error('PTA core doit être chargé avant agent-overview.js');
const permissions = window.PTA_CONFIG?.permissions || {};
const accessProfile = window.PTA_CONFIG?.access || {};
const canEditDraft = Boolean(permissions.can_edit_draft);
const canEditCrossStructure = Boolean(permissions.can_edit_cross_structure);
const canCopyFullWeek = Boolean(permissions.can_copy_full_week);
const { shared } = PTA;
const {
escapeHtml,
readJsonResponse,
apiErrorMessage,
formatDate,
timeToMinutes,
formatDuration,
} = PTA.utils;
let draggedEntry = null;
let suppressClickUntil = 0;
let activeSourceWeek = '';
const typeLabel = (type) => type === 'EXTRASCOLAIRE' ? 'Extrascolaire' : 'Périscolaire';
/** Message dans un conteneur donné : la page en a plusieurs (bandeau, modales). */
function setMessage(element, text = '', type = 'info') {
if (!element) return;
element.className = text ? `message message-${type}` : 'message';
element.textContent = text;
}
/**
* Un créneau n'est modifiable que si l'utilisateur peut éditer les
* brouillons ET que le créneau relève de son périmètre : hors
* `can_edit_cross_structure`, seul son propre lieu.
*/
function canEditEntry(entry) {
if (!canEditDraft) return false;
if (canEditCrossStructure) return true;
return Number(entry.id_structure) === Number(accessProfile.structure_id || 0);
}
/**
* Balisage d'un créneau dans la grille mensuelle.
*
* Un créneau non modifiable est rendu SANS attribut de glisser-déposer et
* porte la mention « Lecture seule » : le droit se voit, plutôt que de se
* découvrir par un refus au clic.
*
* Toutes les valeurs issues de la base passent par escapeHtml(), y compris
* `motif_code` qui alimente un nom de classe CSS.
*/
function entryBadge(entry) {
const editable = canEditEntry(entry);
const interactionAttributes = editable
? `draggable="true" tabindex="0" role="button" data-editable="1" title="Glissez vers un autre jour pour déplacer. Ctrl/⌘ + glisser pour dupliquer."`
: 'data-editable="0"';
return `
${editable ? '⋮⋮' : ''}
`;
}
/** Total des minutes s'imputant sur le quota, motifs hors quota exclus. */
function countedMinutes(entries) {
return entries.reduce((sum, entry) => {
if (!Number(entry.compte_dans_quota)) return sum;
return sum + (timeToMinutes(entry.heure_fin) - timeToMinutes(entry.heure_debut));
}, 0);
}
/**
* Rend la grille mensuelle : une carte par jour, groupée par semaine ISO.
*
* La grille déborde du mois pour rester alignée sur les semaines complètes
* (voir AgentController::monthOverview) ; les jours hors mois sont grisés
* mais bien comptés dans les totaux hebdomadaires.
*/
function render(data) {
const container = document.querySelector('#agent-overview-content');
const empty = document.querySelector('#agent-overview-empty');
if (!container || !empty) return;
shared.lastAgentOverviewData = data;
empty.hidden = true;
container.hidden = false;
const monthMinutes = countedMinutes(data.entries || []);
const weekBlocks = (data.weeks || []).map((week) => {
const days = week.days.map((day) => {
const entries = (data.entries || []).filter((entry) => entry.date_jour === day.date);
return `
${escapeHtml(day.label)}${escapeHtml(day.display)}
${escapeHtml(data.agent.matricule)}${data.agent.structure_principale_nom ? ` · lieu principal : ${escapeHtml(data.agent.structure_principale_nom)} (${typeLabel(data.agent.structure_principale_type_affectation)})` : ''} · ${escapeHtml(data.month.label)}
${Number(data.agent.est_diplome) ? `🎓 Diplômé${data.agent.diplome_libelle ? ` — ${escapeHtml(data.agent.diplome_libelle)}` : ''}` : 'Non diplômé'}${data.agent.telephone ? ` · 📞 ${escapeHtml(data.agent.telephone)}` : ''}${data.agent.adresse ? ` · 📍 ${escapeHtml(data.agent.adresse)}` : ''}${data.agent.date_debut_contrat ? ` · Contrat depuis le ${escapeHtml(formatDate(data.agent.date_debut_contrat))}` : ' · ⚠ Date de début de contrat non renseignée'}
Quotité${Number(data.quota.quotite_travail).toLocaleString('fr-FR')} %Quota de la période${escapeHtml(data.quota.quota_cible.libelle)}${escapeHtml(data.quota.periode_libelle || '')}Décompté sur le mois affiché${escapeHtml(formatDuration(monthMinutes))}Travail + motifs comptant dans le quotaReste à affecter${escapeHtml(data.quota.restantes.libelle)}Calcul sur l’année civile du 1er janvier au 31 décembre.
${canEditDraft
? (canCopyFullWeek
? 'Glissez un créneau modifiable vers un autre jour pour le déplacer. Maintenez Ctrl (ou ⌘ sur Mac) pour le dupliquer. La duplication complète d’une semaine est réservée au Service Enfance.'
: 'Vous pouvez modifier les créneaux de votre lieu. Les créneaux saisis sur un autre lieu restent en lecture seule et toute modification est enregistrée en brouillon.')
: 'Cette vue est en lecture seule. Vous pouvez consulter votre planning et télécharger les documents disponibles.'}
${weekBlocks}
`;
bindMonthInteractions();
}
/**
* Retrouve un créneau dans les données du dernier chargement.
* `shared.lastAgentOverviewData` sert de cache : le glisser-déposer a besoin
* du créneau complet à partir du seul identifiant porté par le DOM.
*/
function findEntry(entryId) {
return shared.lastAgentOverviewData?.entries?.find((item) => Number(item.id_creneau) === Number(entryId)) || null;
}
/**
* Envoie un déplacement ou une duplication au serveur.
* Laisse remonter l'exception : l'appelant décide de l'affichage.
*/
async function persistEntryMove(entryId, targetDate, duplicate) {
const response = await fetch('api/move_or_duplicate_entry.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
entry_id: Number(entryId),
target_date: targetDate,
action: duplicate ? 'duplicate' : 'move',
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de déplacer ce créneau.'));
return data;
}
/** Retire les marques visuelles de dépôt sur toutes les cartes de jour. */
function clearDropTargets() {
document.querySelectorAll('#agent-overview-content .agent-month-day-card').forEach((card) => {
card.classList.remove('is-drop-target', 'is-copy-target');
});
}
/**
* Attache le glisser-déposer aux créneaux et aux jours après chaque rendu.
*
* À rappeler à CHAQUE `render()` : la grille est reconstruite entièrement et
* les écouteurs précédents disparaissent avec les anciens nœuds.
*
* Ctrl/⌘ enfoncé pendant le glisser transforme le déplacement en
* duplication, convention reprise des gestionnaires de fichiers.
*/
function bindMonthInteractions() {
const container = document.querySelector('#agent-overview-content');
if (!container) return;
container.querySelectorAll('.agent-month-entry[data-editable="1"]').forEach((item) => {
item.addEventListener('dragstart', (event) => {
draggedEntry = findEntry(Number(item.dataset.entryId));
item.classList.add('is-dragging');
event.dataTransfer.effectAllowed = 'copyMove';
event.dataTransfer.setData('text/plain', item.dataset.entryId || '');
});
item.addEventListener('dragend', () => {
item.classList.remove('is-dragging');
draggedEntry = null;
suppressClickUntil = Date.now() + 250;
clearDropTargets();
});
item.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
openModal(Number(item.dataset.entryId));
}
});
item.addEventListener('click', (event) => {
if (Date.now() < suppressClickUntil || event.target.closest('button')) return;
openModal(Number(item.dataset.entryId));
});
});
if (!canEditDraft) return;
container.querySelectorAll('.agent-month-day-card').forEach((card) => {
card.addEventListener('dragover', (event) => {
if (!draggedEntry) return;
event.preventDefault();
const duplicate = event.ctrlKey || event.metaKey;
event.dataTransfer.dropEffect = duplicate ? 'copy' : 'move';
clearDropTargets();
card.classList.add('is-drop-target');
if (duplicate) card.classList.add('is-copy-target');
});
card.addEventListener('dragleave', (event) => {
if (!card.contains(event.relatedTarget)) card.classList.remove('is-drop-target', 'is-copy-target');
});
card.addEventListener('drop', async (event) => {
if (!draggedEntry) return;
event.preventDefault();
const entry = draggedEntry;
const targetDate = card.dataset.date || '';
const duplicate = event.ctrlKey || event.metaKey;
clearDropTargets();
try {
const result = await persistEntryMove(entry.id_creneau, targetDate, duplicate);
await load();
setMessage(document.querySelector('#agent-overview-message'), result.message || (duplicate ? 'Créneau dupliqué.' : 'Créneau déplacé.'), 'success');
} catch (error) {
setMessage(document.querySelector('#agent-overview-message'), error.message, 'error');
}
});
});
}
/** Ouvre le contrat horaire annuel de l'agent dans un nouvel onglet. */
function openHoursContractPdf() {
const data = shared.lastAgentOverviewData;
const agentId = data?.agent?.id_agent || document.querySelector('#overview-agent')?.value;
const date = data?.month?.date_debut;
if (!agentId || !date) return;
const params = new URLSearchParams({ agent_id: String(agentId), date: String(date) });
window.open(`api/agent_hours_contract_pdf.php?${params.toString()}`, '_blank', 'noopener');
}
/** Ouvre le bilan d'heures annuel de l'agent dans un nouvel onglet. */
function openTimeSummaryPdf() {
const data = shared.lastAgentOverviewData;
const agentId = data?.agent?.id_agent || document.querySelector('#overview-agent')?.value;
const date = data?.month?.date_debut;
if (!agentId || !date) return;
const params = new URLSearchParams({ agent_id: String(agentId), date: String(date) });
window.open(`api/agent_time_summary_pdf.php?${params.toString()}`, '_blank', 'noopener');
}
function closeModal() {
const modal = document.querySelector('#agent-entry-modal');
const form = document.querySelector('#agent-entry-edit-form');
const message = document.querySelector('#agent-entry-edit-message');
if (!modal) return;
modal.hidden = true;
document.body.classList.remove('modal-open');
form?.reset();
setMessage(message);
}
/**
* Le sélecteur de motif est rendu côté serveur avec tous les motifs actifs :
* on le reconstruit à l'ouverture pour retirer « Formation » aux agents sans
* droit à l'enveloppe de formation. Le motif déjà porté par le créneau reste
* proposé, sans quoi le modal changerait silencieusement de motif.
*/
/** Options du sélecteur de motif de la modale d'édition. */
function renderMotifOptions(selectedMotifId) {
const select = document.querySelector('#edit-agent-entry-motif');
const motifs = window.PTA_CONFIG?.motifs || [];
if (!select || motifs.length === 0) return;
const agent = shared.lastAgentOverviewData?.agent;
const hasTrainingRight = Number(agent?.droit_enveloppe_formation ?? 1) !== 0;
select.innerHTML = motifs
.filter((motif) => hasTrainingRight
|| String(motif.code).toUpperCase() !== 'FORMATION'
|| Number(motif.id) === Number(selectedMotifId))
.map((motif) => ``)
.join('');
}
/** Ouvre la modale d'édition d'un créneau, préremplie de ses valeurs. */
function openModal(entryId) {
const entry = findEntry(entryId);
if (!entry || !canEditEntry(entry)) return;
const modal = document.querySelector('#agent-entry-modal');
if (!entry || !modal) return;
document.querySelector('#edit-agent-entry-id').value = String(Number(entry.id_creneau));
document.querySelector('#edit-agent-entry-date').value = entry.date_jour;
document.querySelector('#edit-agent-entry-structure').value = String(Number(entry.id_structure));
renderMotifOptions(Number(entry.id_motif));
document.querySelector('#edit-agent-entry-motif').value = String(Number(entry.id_motif));
document.querySelector('#edit-agent-entry-start').value = entry.heure_debut;
document.querySelector('#edit-agent-entry-end').value = entry.heure_fin;
setMessage(document.querySelector('#agent-entry-edit-message'));
modal.hidden = false;
document.body.classList.add('modal-open');
document.querySelector('#edit-agent-entry-motif')?.focus();
}
/**
* Supprime un créneau après confirmation, puis recharge la vue.
* Si le planning porteur était validé, le serveur le repasse en brouillon et
* le signale dans son message.
*/
async function deleteEntry(entryId) {
const entry = findEntry(entryId);
if (!entry || !canEditEntry(entry)) return;
if (!entry) return;
const details = `${entry.motif_libelle} · ${entry.heure_debut}–${entry.heure_fin} · ${entry.structure_nom}`;
if (!window.confirm(`Supprimer définitivement ce créneau ?\n\n${details}\n${formatDate(entry.date_jour)}`)) return;
const button = document.querySelector('#delete-agent-entry-edit');
const message = document.querySelector('#agent-entry-edit-message');
if (button) button.disabled = true;
setMessage(message, 'Suppression du créneau en cours…', 'info');
try {
const response = await fetch('api/delete_agent_entry.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
entry_id: Number(entryId),
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de supprimer ce créneau.'));
await load();
closeModal();
setMessage(document.querySelector('#agent-overview-message'), data.message || 'Créneau supprimé.', 'success');
// La suppression peut modifier la file de validation et créer un trou de couverture.
PTA.modules.pendingValidation?.refresh?.({ silent: true });
PTA.modules.coverage?.refresh?.({ silent: true });
} catch (error) {
setMessage(message, error.message, 'error');
} finally {
if (button) button.disabled = false;
}
}
/** Enregistre les modifications du créneau, puis recharge la vue. */
async function saveEntry(event) {
event.preventDefault();
if (!canEditDraft) return;
const submit = document.querySelector('#save-agent-entry-edit');
const message = document.querySelector('#agent-entry-edit-message');
if (!submit || !message) return;
submit.disabled = true;
setMessage(message);
try {
const response = await fetch('api/update_agent_entry.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
entry_id: Number(document.querySelector('#edit-agent-entry-id').value),
structure_id: Number(document.querySelector('#edit-agent-entry-structure').value),
motif_id: Number(document.querySelector('#edit-agent-entry-motif').value),
date: document.querySelector('#edit-agent-entry-date').value,
start: document.querySelector('#edit-agent-entry-start').value,
end: document.querySelector('#edit-agent-entry-end').value,
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de modifier ce créneau.'));
setMessage(message, data.message || 'Créneau modifié.', 'success');
await load();
setTimeout(closeModal, 450);
} catch (error) {
setMessage(message, error.message, 'error');
} finally {
submit.disabled = false;
}
}
/**
* Date → « 2026-W08 », numérotation ISO 8601.
*
* L'algorithme décale la date au JEUDI de sa semaine (`+ 4 - jour`), car
* c'est le jeudi qui détermine à quelle année appartient une semaine ISO.
* Sans ce décalage, une semaine à cheval sur deux années serait mal
* numérotée. Calcul en UTC pour éviter tout décalage de fuseau.
*/
function isoWeekValueFromDate(date) {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const day = d.getUTCDay() || 7;
d.setUTCDate(d.getUTCDate() + 4 - day);
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
}
/** « 2026-W08 » → la Date du lundi correspondant (semaine 1 = celle du 4 janvier). */
function weekMonday(weekValue) {
const match = /^(\d{4})-W(\d{2})$/.exec(weekValue);
if (!match) return null;
const year = Number(match[1]);
const week = Number(match[2]);
const jan4 = new Date(year, 0, 4);
const day = jan4.getDay() || 7;
const monday = new Date(jan4);
monday.setDate(jan4.getDate() - day + 1 + ((week - 1) * 7));
return monday;
}
/**
* Ouvre la modale de duplication d'une semaine entière.
* Réservée à `can_copy_full_week` : la copie porte sur TOUS les lieux de
* l'agent, elle déborde du périmètre d'un responsable de structure.
*/
function openWeekCopyModal(sourceWeek) {
if (!canCopyFullWeek) return;
const modal = document.querySelector('#agent-week-copy-modal');
const sourceInput = document.querySelector('#agent-week-copy-source');
const targetInput = document.querySelector('#agent-week-copy-target');
const summary = document.querySelector('#agent-week-copy-summary');
if (!modal || !sourceInput || !targetInput) return;
activeSourceWeek = sourceWeek;
sourceInput.value = sourceWeek;
const monday = weekMonday(sourceWeek);
if (monday) {
monday.setDate(monday.getDate() + 7);
targetInput.value = isoWeekValueFromDate(monday);
}
if (summary) summary.textContent = `La semaine ${sourceWeek.replace('-W', ' / S')} sera recopiée avec ses lieux, motifs et horaires.`;
setMessage(document.querySelector('#agent-week-copy-message'));
modal.hidden = false;
document.body.classList.add('modal-open');
targetInput.focus();
}
/** Ferme la modale de duplication de semaine. */
function closeWeekCopyModal() {
const modal = document.querySelector('#agent-week-copy-modal');
if (!modal) return;
modal.hidden = true;
document.body.classList.remove('modal-open');
activeSourceWeek = '';
document.querySelector('#agent-week-copy-form')?.reset();
setMessage(document.querySelector('#agent-week-copy-message'));
}
/**
* Duplique la semaine source vers la semaine cible.
* Deux modes : « merge » complète l'existant, « replace » l'écrase. En mode
* merge, le moindre chevauchement fait échouer toute l'opération côté
* serveur — il n'y a pas de copie partielle.
*/
async function copyWeek(event) {
event.preventDefault();
if (!canCopyFullWeek) return;
const agentId = Number(document.querySelector('#overview-agent')?.value || 0);
const sourceWeek = document.querySelector('#agent-week-copy-source')?.value || activeSourceWeek;
const targetWeek = document.querySelector('#agent-week-copy-target')?.value || '';
const mode = document.querySelector('input[name="agent-week-copy-mode"]:checked')?.value || 'merge';
const message = document.querySelector('#agent-week-copy-message');
const submit = document.querySelector('#confirm-agent-week-copy');
if (!agentId || !sourceWeek || !targetWeek) {
setMessage(message, 'Sélectionnez une semaine cible.', 'warning');
return;
}
if (sourceWeek === targetWeek) {
setMessage(message, 'La semaine cible doit être différente de la semaine source.', 'warning');
return;
}
if (mode === 'replace' && !window.confirm('Les créneaux existants de la semaine cible seront supprimés avant la copie. Continuer ?')) return;
if (submit) submit.disabled = true;
setMessage(message, 'Duplication de la semaine en cours...', 'info');
try {
const response = await fetch('api/copy_agent_week.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csrf_token: window.PTA_CONFIG.csrfToken,
agent_id: agentId,
source_week: sourceWeek,
target_week: targetWeek,
mode,
}),
});
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de dupliquer cette semaine.'));
setMessage(message, data.message || 'Semaine dupliquée.', 'success');
await load();
setMessage(document.querySelector('#agent-overview-message'), data.message || 'Semaine dupliquée.', 'success');
setTimeout(closeWeekCopyModal, 500);
} catch (error) {
setMessage(message, error.message, 'error');
} finally {
if (submit) submit.disabled = false;
}
}
/**
* Charge et rend la vue mensuelle de l'agent sélectionné.
* Sans agent ni mois, affiche une invite plutôt qu'une erreur : c'est
* l'état normal à l'ouverture de la page.
*/
async function load() {
const agentId = document.querySelector('#overview-agent')?.value;
const month = document.querySelector('#agent-overview-month')?.value;
const container = document.querySelector('#agent-overview-content');
const empty = document.querySelector('#agent-overview-empty');
if (!container || !empty) return;
if (!agentId || !month) {
empty.hidden = false;
empty.textContent = 'Choisissez un agent et un mois.';
container.hidden = true;
return;
}
setMessage(document.querySelector('#agent-overview-message'));
empty.hidden = false;
empty.textContent = 'Chargement du planning mensuel de l’agent…';
container.hidden = true;
try {
const params = new URLSearchParams({ agent_id: agentId, month });
const response = await fetch(`api/agent_month_overview.php?${params.toString()}`);
const data = await readJsonResponse(response);
if (!response.ok) throw new Error(apiErrorMessage(data, 'Impossible de charger la vue mensuelle de l’agent.'));
render(data);
} catch (error) {
empty.hidden = false;
empty.textContent = error.message;
}
}
/**
* Point d'entrée appelé par page-bootstrap.js.
* Les clics dans la grille sont traités par DÉLÉGATION sur le conteneur :
* elle est redessinée à chaque chargement, des écouteurs individuels
* devraient être reposés à chaque fois.
*/
function init() {
document.querySelector('#load-agent-overview')?.addEventListener('click', load);
document.querySelector('#overview-agent')?.addEventListener('change', load);
document.querySelector('#agent-overview-month')?.addEventListener('change', load);
document.querySelector('#agent-overview-content')?.addEventListener('click', (event) => {
if (event.target.closest('.agent-hours-contract-pdf')) {
openHoursContractPdf();
return;
}
if (event.target.closest('.agent-time-summary-pdf')) {
openTimeSummaryPdf();
return;
}
const copyButton = event.target.closest('.agent-copy-week');
if (copyButton && canCopyFullWeek) {
openWeekCopyModal(copyButton.dataset.sourceWeek || '');
return;
}
const editButton = event.target.closest('.overview-entry-edit');
if (editButton && canEditDraft) openModal(Number(editButton.dataset.entryId));
});
if (canEditDraft) {
document.querySelector('#agent-entry-edit-form')?.addEventListener('submit', saveEntry);
document.querySelector('#delete-agent-entry-edit')?.addEventListener('click', () => {
const entryId = Number(document.querySelector('#edit-agent-entry-id')?.value || 0);
if (entryId) deleteEntry(entryId);
});
document.querySelector('#close-agent-entry-modal')?.addEventListener('click', closeModal);
document.querySelector('#cancel-agent-entry-edit')?.addEventListener('click', closeModal);
document.querySelector('#agent-entry-modal')?.addEventListener('click', (event) => {
if (event.target.id === 'agent-entry-modal') closeModal();
});
}
if (canCopyFullWeek) {
document.querySelector('#agent-week-copy-form')?.addEventListener('submit', copyWeek);
document.querySelector('#close-agent-week-copy-modal')?.addEventListener('click', closeWeekCopyModal);
document.querySelector('#cancel-agent-week-copy')?.addEventListener('click', closeWeekCopyModal);
document.querySelector('#agent-week-copy-modal')?.addEventListener('click', (event) => {
if (event.target.id === 'agent-week-copy-modal') closeWeekCopyModal();
});
}
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape') return;
if (!document.querySelector('#agent-entry-modal')?.hidden) closeModal();
if (!document.querySelector('#agent-week-copy-modal')?.hidden) closeWeekCopyModal();
});
}
PTA.modules.agentOverview = { init, load };
})();