Modification page "A valider" pour regrouper les créneaux des agents
This commit is contained in:
@@ -205,7 +205,7 @@ final class PageController extends Controller
|
|||||||
{
|
{
|
||||||
$this->access->requirePage('pta');
|
$this->access->requirePage('pta');
|
||||||
$this->renderPage('pta', 'PTA annuel par agent', 'pta', [
|
$this->renderPage('pta', 'PTA annuel par agent', 'pta', [
|
||||||
'pageSubtitle' => 'Le PTA détaillé d’un agent : souhaits de lieux, contraintes et répartition des heures.',
|
'pageSubtitle' => 'Le PTA détaillé d\'un agent : souhaits de lieux, contraintes et répartition des heures.',
|
||||||
'agents' => (new AgentModel($this->pdo))->allActive(),
|
'agents' => (new AgentModel($this->pdo))->allActive(),
|
||||||
'structures' => (new StructureModel($this->pdo))->allActive(),
|
'structures' => (new StructureModel($this->pdo))->allActive(),
|
||||||
'scripts' => ['pta.js'],
|
'scripts' => ['pta.js'],
|
||||||
@@ -303,8 +303,8 @@ final class PageController extends Controller
|
|||||||
public function manage_structures(): void
|
public function manage_structures(): void
|
||||||
{
|
{
|
||||||
$this->access->requirePage('manage_structures');
|
$this->access->requirePage('manage_structures');
|
||||||
$this->renderPage('manage_structures', 'Lieux d’affectation', 'manage_structures', [
|
$this->renderPage('manage_structures', 'Lieux d\'affectation', 'manage_structures', [
|
||||||
'pageSubtitle' => 'Créer un lieu d’affectation et définir son type : périscolaire ou extrascolaire.',
|
'pageSubtitle' => 'Créer un lieu d\'affectation et définir son type : périscolaire ou extrascolaire.',
|
||||||
'structures' => (new StructureModel($this->pdo))->allActive(),
|
'structures' => (new StructureModel($this->pdo))->allActive(),
|
||||||
'scripts' => ['administration.js'],
|
'scripts' => ['administration.js'],
|
||||||
'modules' => ['administration'],
|
'modules' => ['administration'],
|
||||||
@@ -323,7 +323,7 @@ final class PageController extends Controller
|
|||||||
{
|
{
|
||||||
$this->access->requirePage('dsi');
|
$this->access->requirePage('dsi');
|
||||||
$this->renderPage('dsi', 'Espace DSI', 'dsi', [
|
$this->renderPage('dsi', 'Espace DSI', 'dsi', [
|
||||||
'pageSubtitle' => 'Les comptes d’accès de l’application, leurs jetons, et le journal des connexions, des écritures et des refus.',
|
'pageSubtitle' => 'Les comptes d\'accès de l\'application, leurs jetons, et le journal des connexions, des écritures et des refus.',
|
||||||
'scripts' => ['dsi.js'],
|
'scripts' => ['dsi.js'],
|
||||||
'modules' => ['dsi'],
|
'modules' => ['dsi'],
|
||||||
'pageConfig' => [
|
'pageConfig' => [
|
||||||
@@ -343,7 +343,7 @@ final class PageController extends Controller
|
|||||||
public function faq(): void
|
public function faq(): void
|
||||||
{
|
{
|
||||||
$this->access->requirePage('faq');
|
$this->access->requirePage('faq');
|
||||||
$this->renderPage('faq', 'Guide d’utilisation', 'faq', [
|
$this->renderPage('faq', 'Guide d\'utilisation', 'faq', [
|
||||||
'pageSubtitle' => 'Le parcours complet, du paramétrage des lieux à la validation des plannings, et les réponses aux questions les plus fréquentes.',
|
'pageSubtitle' => 'Le parcours complet, du paramétrage des lieux à la validation des plannings, et les réponses aux questions les plus fréquentes.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ window.PTA_PAGE_CONFIG = <?= json_encode([
|
|||||||
'params' => $pageConfig,
|
'params' => $pageConfig,
|
||||||
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE) ?>;
|
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE) ?>;
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/core.js?v=7.23"></script>
|
<script src="assets/js/core.js?v=7.24"></script>
|
||||||
<?php foreach ($scripts as $script): ?>
|
<?php foreach ($scripts as $script): ?>
|
||||||
<script src="assets/js/<?= htmlspecialchars($script) ?>?v=7.23"></script>
|
<script src="assets/js/<?= htmlspecialchars($script) ?>?v=7.24"></script>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<script src="assets/js/page-bootstrap.js?v=7.23"></script>
|
<script src="assets/js/page-bootstrap.js?v=7.24"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ $showHelpTab = in_array('faq', $allowedPages, true);
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><?= htmlspecialchars($pageTitle) ?> — PTA</title>
|
<title><?= htmlspecialchars($pageTitle) ?> — PTA</title>
|
||||||
<link rel="stylesheet" href="assets/style.css?v=7.23">
|
<link rel="stylesheet" href="assets/style.css?v=7.24">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="app-shell">
|
<main class="app-shell">
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ $titre = $titres[$etape] ?? 'Connexion';
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><?= htmlspecialchars($titre) ?> — PTA</title>
|
<title><?= htmlspecialchars($titre) ?> — PTA</title>
|
||||||
<link rel="stylesheet" href="assets/style.css?v=7.23">
|
<link rel="stylesheet" href="assets/style.css?v=7.24">
|
||||||
</head>
|
</head>
|
||||||
<body class="role-page-body">
|
<body class="role-page-body">
|
||||||
<main class="role-page-shell">
|
<main class="role-page-shell">
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* File des plannings en attente de validation.
|
* File des plannings en attente de validation, REGROUPÉE PAR AGENT.
|
||||||
* Module JS : `pendingValidation`.
|
* Module JS : `pendingValidation`.
|
||||||
*
|
*
|
||||||
|
* Le tableau ne liste plus un planning par ligne mais un AGENT par ligne
|
||||||
|
* (nom, lieu d'affectation, nombre de semaines à valider) : la file dépassait
|
||||||
|
* la centaine de lignes en période de saisie. Le détail semaine par semaine
|
||||||
|
* reste accessible en dépliant la ligne de l'agent.
|
||||||
|
*
|
||||||
* Partiel de contenu inclus par la vue de page homonyme. Les identifiants HTML
|
* Partiel de contenu inclus par la vue de page homonyme. Les identifiants HTML
|
||||||
* posés ici sont les points d'ancrage du module JS : les renommer casse le
|
* posés ici sont les points d'ancrage du module JS : les renommer casse le
|
||||||
* module sans aucune erreur visible.
|
* module sans aucune erreur visible.
|
||||||
@@ -13,16 +18,16 @@
|
|||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>Plannings en attente de validation</h2>
|
<h2>Plannings en attente de validation</h2>
|
||||||
<p class="section-subtitle">Retrouvez tous les brouillons contenant des affectations. Ouvrez un planning pour le contrôler puis le valider directement dans l’écran de saisie.</p>
|
<p class="section-subtitle">Chaque ligne regroupe les brouillons d’un agent. Dépliez-la pour voir le détail des semaines, puis ouvrez celle à contrôler pour la valider dans l’écran de saisie.</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="refresh-pending-validation" class="button button-secondary" type="button">Actualiser</button>
|
<button id="refresh-pending-validation" class="button button-secondary" type="button">Actualiser</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pending-validation-summary" aria-live="polite">
|
<div class="pending-validation-summary" aria-live="polite">
|
||||||
<article class="pending-summary-card">
|
<article class="pending-summary-card">
|
||||||
<span>À valider</span>
|
<span>Agents concernés</span>
|
||||||
<strong id="pending-validation-count">0</strong>
|
<strong id="pending-validation-agents">0</strong>
|
||||||
<small>planning(s)</small>
|
<small><span id="pending-validation-count">0</span> planning(s) à valider</small>
|
||||||
</article>
|
</article>
|
||||||
<article class="pending-summary-card">
|
<article class="pending-summary-card">
|
||||||
<span>Volume en attente</span>
|
<span>Volume en attente</span>
|
||||||
@@ -47,8 +52,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Agent</th>
|
<th>Agent</th>
|
||||||
<th>Lieu d’affectation</th>
|
<th>Lieu d’affectation</th>
|
||||||
<th>Semaine</th>
|
<th>Semaines à valider</th>
|
||||||
<th>Créneaux</th>
|
|
||||||
<th>Volume</th>
|
<th>Volume</th>
|
||||||
<th>Dernière modification</th>
|
<th>Dernière modification</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* File des plannings à valider (`PTA.modules.pendingValidation`), écran pending.php.
|
* File des plannings à valider (`PTA.modules.pendingValidation`), écran pending.php.
|
||||||
*
|
*
|
||||||
* Liste les brouillons NON VIDES, du plus ancien au plus récent — l'ordre de
|
* L'API renvoie un brouillon NON VIDE par ligne, du plus ancien au plus récent
|
||||||
* traitement attendu, garanti par l'API. Cliquer sur une ligne ouvre l'écran de
|
* — l'ordre de traitement attendu. Le module REGROUPE ces lignes PAR AGENT :
|
||||||
* saisie positionné sur ce planning, défilé jusqu'aux boutons d'action.
|
* une ligne par agent (nom, lieu d'affectation, nombre de semaines), dépliable
|
||||||
|
* sur le détail semaine par semaine. C'est le seul niveau de regroupement :
|
||||||
|
* un agent planifié sur plusieurs lieux la même période reste une seule ligne,
|
||||||
|
* la colonne « lieu » énumère alors ses lieux.
|
||||||
|
*
|
||||||
|
* L'ordre des agents suit celui de l'API : le premier agent est celui qui porte
|
||||||
|
* le brouillon le plus ancien. Ouvrir une semaine ouvre l'écran de saisie
|
||||||
|
* positionné sur ce planning, défilé jusqu'aux boutons d'action.
|
||||||
*
|
*
|
||||||
* Ce module est aussi rafraîchi en mode silencieux par l'éditeur de planning
|
* Ce module est aussi rafraîchi en mode silencieux par l'éditeur de planning
|
||||||
* après chaque enregistrement ou validation, et alimente la pastille « À
|
* après chaque enregistrement ou validation, et alimente la pastille « À
|
||||||
* valider » de la barre de navigation.
|
* valider » de la barre de navigation. Les agents dépliés le restent d'un
|
||||||
|
* rafraîchissement à l'autre (`expandedAgents`), sans quoi une validation
|
||||||
|
* refermerait la liste sous les yeux de l'utilisateur.
|
||||||
*
|
*
|
||||||
* Deux totaux distincts sont affichés : les heures SAISIES et les heures
|
* Deux totaux distincts sont affichés : les heures SAISIES et les heures
|
||||||
* DÉCOMPTÉES du quota. Congés et absences non décomptées figurent au planning
|
* DÉCOMPTÉES du quota. Congés et absences non décomptées figurent au planning
|
||||||
@@ -25,6 +34,7 @@
|
|||||||
const badge = document.querySelector('#pending-validation-tab-badge');
|
const badge = document.querySelector('#pending-validation-tab-badge');
|
||||||
const refreshButton = document.querySelector('#refresh-pending-validation');
|
const refreshButton = document.querySelector('#refresh-pending-validation');
|
||||||
const countElement = document.querySelector('#pending-validation-count');
|
const countElement = document.querySelector('#pending-validation-count');
|
||||||
|
const agentsElement = document.querySelector('#pending-validation-agents');
|
||||||
const hoursElement = document.querySelector('#pending-validation-hours');
|
const hoursElement = document.querySelector('#pending-validation-hours');
|
||||||
const countedHoursElement = document.querySelector('#pending-validation-counted-hours');
|
const countedHoursElement = document.querySelector('#pending-validation-counted-hours');
|
||||||
const loadingElement = document.querySelector('#pending-validation-loading');
|
const loadingElement = document.querySelector('#pending-validation-loading');
|
||||||
@@ -34,7 +44,10 @@
|
|||||||
const messageElement = document.querySelector('#pending-validation-message');
|
const messageElement = document.querySelector('#pending-validation-message');
|
||||||
|
|
||||||
let items = [];
|
let items = [];
|
||||||
|
let groups = [];
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
/** Agents dépliés, conservés entre deux rendus. */
|
||||||
|
const expandedAgents = new Set();
|
||||||
|
|
||||||
/** Recompose « 2026-W08 » depuis l'année et le numéro de semaine. */
|
/** Recompose « 2026-W08 » depuis l'année et le numéro de semaine. */
|
||||||
function weekValue(item) {
|
function weekValue(item) {
|
||||||
@@ -76,14 +89,139 @@
|
|||||||
messageElement.textContent = text;
|
messageElement.textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regroupe les plannings par agent en conservant l'ordre d'arrivée : le
|
||||||
|
* premier groupe est donc celui du brouillon le plus ancien, et à
|
||||||
|
* l'intérieur d'un groupe les semaines restent chronologiques.
|
||||||
|
*/
|
||||||
|
function groupByAgent(list) {
|
||||||
|
const byAgent = new Map();
|
||||||
|
list.forEach((item) => {
|
||||||
|
const agentId = Number(item.id_agent);
|
||||||
|
let group = byAgent.get(agentId);
|
||||||
|
if (!group) {
|
||||||
|
group = {
|
||||||
|
id_agent: agentId,
|
||||||
|
agent_nom: item.agent_nom,
|
||||||
|
agent_prenom: item.agent_prenom,
|
||||||
|
matricule: item.matricule || '',
|
||||||
|
structures: new Map(),
|
||||||
|
plannings: [],
|
||||||
|
total_minutes: 0,
|
||||||
|
minutes_decomptees: 0,
|
||||||
|
nombre_creneaux: 0,
|
||||||
|
date_modification: null,
|
||||||
|
};
|
||||||
|
byAgent.set(agentId, group);
|
||||||
|
}
|
||||||
|
group.plannings.push(item);
|
||||||
|
group.total_minutes += Number(item.total_minutes || 0);
|
||||||
|
group.minutes_decomptees += Number(item.minutes_decomptees || 0);
|
||||||
|
group.nombre_creneaux += Number(item.nombre_creneaux || 0);
|
||||||
|
if (!group.structures.has(Number(item.id_structure))) {
|
||||||
|
group.structures.set(Number(item.id_structure), {
|
||||||
|
nom: item.structure_nom,
|
||||||
|
type: item.structure_type_affectation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!group.date_modification || String(item.date_modification) > String(group.date_modification)) {
|
||||||
|
group.date_modification = item.date_modification;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [...byAgent.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Colonne « lieu » : le lieu unique, ou l'énumération des lieux concernés. */
|
||||||
|
function structureCell(group) {
|
||||||
|
const structures = [...group.structures.values()];
|
||||||
|
if (structures.length === 1) {
|
||||||
|
return `<strong>${escapeHtml(structures[0].nom)}</strong>
|
||||||
|
<small>${escapeHtml(structureTypeLabel(structures[0].type))}</small>`;
|
||||||
|
}
|
||||||
|
return `<strong>${structures.length} lieux</strong>
|
||||||
|
<small>${escapeHtml(structures.map((structure) => structure.nom).join(' · '))}</small>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Une semaine du détail déplié. */
|
||||||
|
function weekRow(item) {
|
||||||
|
const week = weekValue(item);
|
||||||
|
const rowId = Number(item.id_planning);
|
||||||
|
return `
|
||||||
|
<li class="pending-week-item" data-planning-id="${rowId}">
|
||||||
|
<div class="pending-week-main">
|
||||||
|
<strong>Semaine ${Number(item.numero_semaine)}</strong>
|
||||||
|
<small>${formatDate(item.date_debut)} → ${formatDate(item.date_fin_ouvrable || item.date_fin)}</small>
|
||||||
|
</div>
|
||||||
|
<div class="pending-week-structure">
|
||||||
|
<strong>${escapeHtml(item.structure_nom)}</strong>
|
||||||
|
<small>${escapeHtml(structureTypeLabel(item.structure_type_affectation))}</small>
|
||||||
|
</div>
|
||||||
|
<div class="pending-week-volume">
|
||||||
|
<strong>${formatDuration(Number(item.total_minutes || 0))}</strong>
|
||||||
|
<small>${Number(item.nombre_creneaux)} créneau(x) · ${formatDuration(Number(item.minutes_decomptees || 0))} décomptées</small>
|
||||||
|
</div>
|
||||||
|
<div class="pending-week-modified">
|
||||||
|
<small>Modifié le ${formatDateTime(item.date_modification)}</small>
|
||||||
|
</div>
|
||||||
|
<div class="pending-week-action">
|
||||||
|
<button class="button button-success open-pending-planning" type="button" data-planning-id="${rowId}"
|
||||||
|
aria-label="Ouvrir le planning de ${escapeHtml(item.agent_prenom)} ${escapeHtml(item.agent_nom)} pour la semaine ${escapeHtml(week)}">Consulter et valider</button>
|
||||||
|
</div>
|
||||||
|
</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ligne d'agent + ligne de détail associée (masquée si l'agent est replié). */
|
||||||
|
function agentRows(group) {
|
||||||
|
const expanded = expandedAgents.has(group.id_agent);
|
||||||
|
const weeks = group.plannings.length;
|
||||||
|
const identite = `${escapeHtml(group.agent_prenom)} ${escapeHtml(group.agent_nom)}`;
|
||||||
|
return `
|
||||||
|
<tr class="pending-validation-row${expanded ? ' is-expanded' : ''}" data-agent-id="${group.id_agent}"
|
||||||
|
tabindex="0" role="button" aria-expanded="${expanded ? 'true' : 'false'}"
|
||||||
|
aria-label="${expanded ? 'Replier' : 'Déplier'} les ${weeks} semaine(s) à valider de ${identite}">
|
||||||
|
<td>
|
||||||
|
<strong>${identite}</strong>
|
||||||
|
<small>${escapeHtml(group.matricule)}</small>
|
||||||
|
</td>
|
||||||
|
<td>${structureCell(group)}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pending-count-chip">${weeks}</span>
|
||||||
|
<small>${group.nombre_creneaux} créneau(x)</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>${formatDuration(group.total_minutes)}</strong>
|
||||||
|
<small>${formatDuration(group.minutes_decomptees)} décomptées</small>
|
||||||
|
</td>
|
||||||
|
<td>${formatDateTime(group.date_modification)}</td>
|
||||||
|
<td class="pending-validation-action-cell">
|
||||||
|
<span class="pending-toggle-hint">${expanded ? 'Masquer le détail' : 'Voir le détail'}</span>
|
||||||
|
<span class="pending-toggle-chevron" aria-hidden="true">${expanded ? '▲' : '▼'}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="pending-validation-detail-row" data-agent-detail-id="${group.id_agent}"${expanded ? '' : ' hidden'}>
|
||||||
|
<td colspan="6">
|
||||||
|
<ul class="pending-week-list">${group.plannings.map(weekRow).join('')}</ul>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Rend le tableau de la file et ses compteurs de synthèse. */
|
/** Rend le tableau de la file et ses compteurs de synthèse. */
|
||||||
function render(data) {
|
function render(data) {
|
||||||
items = Array.isArray(data.plannings) ? data.plannings : [];
|
items = Array.isArray(data.plannings) ? data.plannings : [];
|
||||||
|
groups = groupByAgent(items);
|
||||||
const summary = data.summary || {};
|
const summary = data.summary || {};
|
||||||
const count = Number(summary.count ?? items.length);
|
const count = Number(summary.count ?? items.length);
|
||||||
|
|
||||||
|
// Un agent replié entre-temps ne doit pas rester dans le jeu d'agents
|
||||||
|
// dépliés : ses plannings ont pu être validés depuis.
|
||||||
|
const presents = new Set(groups.map((group) => group.id_agent));
|
||||||
|
[...expandedAgents].forEach((agentId) => {
|
||||||
|
if (!presents.has(agentId)) expandedAgents.delete(agentId);
|
||||||
|
});
|
||||||
|
|
||||||
setBadge(count);
|
setBadge(count);
|
||||||
if (countElement) countElement.textContent = String(count);
|
if (countElement) countElement.textContent = String(count);
|
||||||
|
if (agentsElement) agentsElement.textContent = String(groups.length);
|
||||||
if (hoursElement) hoursElement.textContent = formatDuration(Number(summary.total_minutes || 0));
|
if (hoursElement) hoursElement.textContent = formatDuration(Number(summary.total_minutes || 0));
|
||||||
if (countedHoursElement) countedHoursElement.textContent = formatDuration(Number(summary.counted_minutes || 0));
|
if (countedHoursElement) countedHoursElement.textContent = formatDuration(Number(summary.counted_minutes || 0));
|
||||||
|
|
||||||
@@ -92,34 +230,7 @@
|
|||||||
if (contentElement) contentElement.hidden = items.length === 0;
|
if (contentElement) contentElement.hidden = items.length === 0;
|
||||||
if (!bodyElement) return;
|
if (!bodyElement) return;
|
||||||
|
|
||||||
bodyElement.innerHTML = items.map((item) => {
|
bodyElement.innerHTML = groups.map(agentRows).join('');
|
||||||
const week = weekValue(item);
|
|
||||||
const rowId = Number(item.id_planning);
|
|
||||||
return `
|
|
||||||
<tr class="pending-validation-row" data-planning-id="${rowId}" tabindex="0" role="button" aria-label="Ouvrir le planning de ${escapeHtml(item.agent_prenom)} ${escapeHtml(item.agent_nom)} pour la semaine ${escapeHtml(week)}">
|
|
||||||
<td>
|
|
||||||
<strong>${escapeHtml(item.agent_prenom)} ${escapeHtml(item.agent_nom)}</strong>
|
|
||||||
<small>${escapeHtml(item.matricule || '')}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<strong>${escapeHtml(item.structure_nom)}</strong>
|
|
||||||
<small>${escapeHtml(structureTypeLabel(item.structure_type_affectation))}</small>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<strong>Semaine ${Number(item.numero_semaine)}</strong>
|
|
||||||
<small>${formatDate(item.date_debut)} → ${formatDate(item.date_fin_ouvrable || item.date_fin)}</small>
|
|
||||||
</td>
|
|
||||||
<td><span class="pending-count-chip">${Number(item.nombre_creneaux)}</span></td>
|
|
||||||
<td>
|
|
||||||
<strong>${formatDuration(Number(item.total_minutes || 0))}</strong>
|
|
||||||
<small>${formatDuration(Number(item.minutes_decomptees || 0))} décomptées</small>
|
|
||||||
</td>
|
|
||||||
<td>${formatDateTime(item.date_modification)}</td>
|
|
||||||
<td class="pending-validation-action-cell">
|
|
||||||
<button class="button button-success open-pending-planning" type="button" data-planning-id="${rowId}">Consulter et valider</button>
|
|
||||||
</td>
|
|
||||||
</tr>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,9 +280,29 @@
|
|||||||
window.location.href = `planning.php?${params.toString()}`;
|
window.location.href = `planning.php?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Déplie ou replie un agent, sans recharger la file. */
|
||||||
|
function toggleAgent(agentId, row) {
|
||||||
|
const expanded = expandedAgents.has(agentId);
|
||||||
|
if (expanded) expandedAgents.delete(agentId);
|
||||||
|
else expandedAgents.add(agentId);
|
||||||
|
const group = groups.find((candidate) => candidate.id_agent === agentId);
|
||||||
|
const identite = group ? `${group.agent_prenom} ${group.agent_nom}` : '';
|
||||||
|
const weeks = group ? group.plannings.length : 0;
|
||||||
|
|
||||||
|
row.classList.toggle('is-expanded', !expanded);
|
||||||
|
row.setAttribute('aria-expanded', expanded ? 'false' : 'true');
|
||||||
|
row.setAttribute('aria-label', `${expanded ? 'Déplier' : 'Replier'} les ${weeks} semaine(s) à valider de ${identite}`);
|
||||||
|
const hint = row.querySelector('.pending-toggle-hint');
|
||||||
|
if (hint) hint.textContent = expanded ? 'Voir le détail' : 'Masquer le détail';
|
||||||
|
const chevron = row.querySelector('.pending-toggle-chevron');
|
||||||
|
if (chevron) chevron.textContent = expanded ? '▼' : '▲';
|
||||||
|
const detail = bodyElement?.querySelector(`[data-agent-detail-id="${agentId}"]`);
|
||||||
|
if (detail) detail.hidden = expanded;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrouve le planning correspondant à l'élément cliqué, via le
|
* Retrouve le planning correspondant à l'élément cliqué, via le
|
||||||
* `data-planning-id` porté par la ligne. Support de la délégation
|
* `data-planning-id` porté par la semaine. Support de la délégation
|
||||||
* d'événements sur le corps du tableau.
|
* d'événements sur le corps du tableau.
|
||||||
*/
|
*/
|
||||||
function findItemFromEventTarget(target) {
|
function findItemFromEventTarget(target) {
|
||||||
@@ -185,7 +316,12 @@
|
|||||||
refreshButton?.addEventListener('click', () => refresh());
|
refreshButton?.addEventListener('click', () => refresh());
|
||||||
bodyElement?.addEventListener('click', (event) => {
|
bodyElement?.addEventListener('click', (event) => {
|
||||||
const item = findItemFromEventTarget(event.target);
|
const item = findItemFromEventTarget(event.target);
|
||||||
if (item) openPlanning(item);
|
if (item) {
|
||||||
|
openPlanning(item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = event.target.closest('.pending-validation-row');
|
||||||
|
if (row) toggleAgent(Number(row.dataset.agentId), row);
|
||||||
});
|
});
|
||||||
bodyElement?.addEventListener('keydown', (event) => {
|
bodyElement?.addEventListener('keydown', (event) => {
|
||||||
if (!['Enter', ' '].includes(event.key)) return;
|
if (!['Enter', ' '].includes(event.key)) return;
|
||||||
@@ -193,8 +329,7 @@
|
|||||||
const row = event.target.closest('.pending-validation-row');
|
const row = event.target.closest('.pending-validation-row');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const item = findItemFromEventTarget(row);
|
toggleAgent(Number(row.dataset.agentId), row);
|
||||||
if (item) openPlanning(item);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
refresh({ silent: true });
|
refresh({ silent: true });
|
||||||
|
|||||||
@@ -1420,7 +1420,7 @@ body.modal-open {
|
|||||||
|
|
||||||
.pending-validation-table {
|
.pending-validation-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1040px;
|
min-width: 900px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1482,6 +1482,73 @@ body.modal-open {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ligne d'agent dépliée : le détail des semaines lui est visuellement rattaché. */
|
||||||
|
.pending-validation-row.is-expanded {
|
||||||
|
background: #fffaf0;
|
||||||
|
box-shadow: inset 4px 0 0 #b06a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-toggle-hint {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-toggle-chevron {
|
||||||
|
color: #b06a00;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-validation-detail-row > td {
|
||||||
|
padding: 0 0.8rem 0.9rem 0.8rem;
|
||||||
|
background: #fcfdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-week-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-week-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.2fr) minmax(0, 1.2fr) minmax(0, 1.1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.7rem 0.85rem;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-week-item:hover {
|
||||||
|
border-color: #b06a00;
|
||||||
|
box-shadow: 0 1px 6px rgba(176, 106, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-week-item strong,
|
||||||
|
.pending-week-item small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-week-item small {
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-week-action {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-week-action .button {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.pending-validation-summary {
|
.pending-validation-summary {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user