<?php function getWorkingDays($startDate, $endDate, $holidays = []) { $start = strtotime($startDate); $end = strtotime($endDate); $workingDays = []; setlocale(LC_TIME, 'fr_FR.UTF-8'); for ($currentDate = $start; $currentDate <= $end; $currentDate = strtotime("+1 day", $currentDate)) { $dayOfWeek = date("N", $currentDate); if ($dayOfWeek < 6 && !in_array(date("Y-m-d", $currentDate), $holidays)) { $workingDays[] = [ 'day' => date("d", $currentDate), 'day_of_week_initial' => strtoupper(strftime("%A", $currentDate)[0]), 'week_number' => date("W", $currentDate), 'full_date' => date("Y-m-d", $currentDate) // Ajout de la date complète ]; } } return $workingDays; } // Liste des jours fériés et fêtes religieuses à exclure (exemple pour la France, à compléter avec d'autres fêtes) $holidays = [ "2025-01-01", // Jour de l'An "2025-04-14", // Lundi de Pâques "2025-05-01", // Fête du Travail "2025-05-08", // Victoire 1945 "2025-07-14", // Bastille "2025-08-15", // Assomption "2025-11-01", // Toussaint "2025-12-25", // Noël "2026-01-01", // Jour de l'An "2026-04-06", // Lundi de Pâques // Ajouter d'autres jours fériés ici... ]; // Définir la période de début et de fin $startDate = "2025-01-10"; $endDate = "2026-02-14"; // Appeler la fonction pour obtenir les jours ouvrés entre les deux dates $workingDays = getWorkingDays($startDate, $endDate, $holidays); ?> <!DOCTYPE html> <html lang="fr"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tableau Sticky - 3 Lignes Fixes</title> <style> .table-wrapper { width: 100%; max-height: 400px; overflow: auto; border: 1px solid #ccc; position: relative; } table { border-collapse: collapse; width: max-content; table-layout: fixed; } th, td { padding: 10px; text-align: center; height: 40px; border: 1px solid #ccc; white-space: nowrap; min-width: 120px; position: relative; background-color: white; box-sizing: border-box; } th { background-color: #f2f2f2; position: sticky; top: 0; z-index: 100; } th:first-child, td:first-child { position: sticky; left: 0; z-index: 101; background-color: white; } th:nth-child(2), td:nth-child(2) { position: sticky; left: 120px; z-index: 100; background-color: white; } th:nth-child(3), td:nth-child(3) { position: sticky; left: 240px; z-index: 99; background-color: white; } th:first-child::after, td:first-child::after, th:nth-child(2)::after, td:nth-child(2)::after, th:nth-child(3)::after, td:nth-child(3)::after { content: ""; position: absolute; right: 0; top: 0; width: 2px; height: 100%; background-color: #ccc; z-index: 102; } tbody tr:nth-child(odd) { background-color: #f9f9f9; } th:nth-child(n+4), td:nth-child(n+4) { min-width: 40px; max-width: 40px; width: 40px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .second-header th { background-color: #f2f2f2; position: sticky; top: 40px; z-index: 99; } .third-header th { background-color: #f2f2f2; position: sticky; top: 80px; z-index: 98; } .second-header th:first-child, .third-header th:first-child { left: 0; z-index: 101; background-color: white; } .second-header th:nth-child(2), .third-header th:nth-child(2) { left: 120px; z-index: 100; background-color: white; } .second-header th:nth-child(3), .third-header th:nth-child(3) { left: 240px; z-index: 99; background-color: white; } thead tr:first-child th:first-child, thead tr:first-child th:nth-child(2), thead tr:first-child th:nth-child(3) { z-index: 105; background-color: #ddd; } .second-header th:first-child, .second-header th:nth-child(2), .second-header th:nth-child(3) { z-index: 104; } .third-header th:first-child, .third-header th:nth-child(2), .third-header th:nth-child(3) { z-index: 103; } thead tr:first-child th, .second-header th, .third-header th { box-shadow: inset 0 -2px #aaa; background-clip: padding-box; padding-bottom: 2px; margin-bottom: -2px; z-index: 100; } .second-header th { top: calc(40px + 2px); margin-top: 2px; } .third-header th { top: calc(80px + 2px); margin-top: 2px; } .even-week { background-color: #ffcc99 !important; } .odd-week { background-color: #e6b3ff !important; } .fourth-header th { background-color: #f2f2f2; position: sticky; top: 120px; z-index: 97; } .fourth-header th:first-child { left: 0; z-index: 101; background-color: white; } .fourth-header th:nth-child(2) { left: 120px; z-index: 100; background-color: white; } .fourth-header th:nth-child(3) { left: 240px; z-index: 99; background-color: white; } .fourth-header th:first-child, .fourth-header th:nth-child(2), .fourth-header th:nth-child(3) { z-index: 102; } .fourth-header th { top: calc(120px + 2px); margin-top: 2px; } .fourth-header th { box-shadow: inset 0 -2px #aaa; background-clip: padding-box; padding-bottom: 2px; margin-bottom: -2px; z-index: 97; } .even-week { background-color: #ffcc99 !important; } .odd-week { background-color: #e6b3ff !important; } .month-even { background-color: #99ccff !important; } .month-odd { background-color: #ff9999 !important; } #addRowButton { margin-bottom: 20px; padding: 10px; background-color: #4CAF50; color: white; border: none; cursor: pointer; font-size: 16px; } #addRowButton:hover { background-color: #45a049; } #addSpecialRowButton { margin-bottom: 20px; padding: 10px; background-color: #2196F3; color: white; border: none; cursor: pointer; font-size: 16px; } #addSpecialRowButton:hover { background-color: #1e88e5; } .special-row { background-color: #f0f0f0; } #contextMenu { position: absolute; display: none; flex-direction: column; background: white; border: 1px solid #ccc; box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); border-radius: 5px; z-index: 1000; min-width: 180px; } #contextMenu .dropdown-item { display: block; width: 100%; padding: 8px 12px; text-align: left; color: #333; cursor: pointer; } #contextMenu .dropdown-item:hover { background: #f8f9fa; } #specialContextMenu { position: absolute; display: none; flex-direction: column; background: white; border: 1px solid #ccc; box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); border-radius: 5px; z-index: 1000; min-width: 180px; } #specialContextMenu .dropdown-item { display: block; width: 100%; padding: 8px 12px; text-align: left; color: #333; cursor: pointer; } #specialContextMenu .dropdown-item:hover { background: #f8f9fa; } .add-button { background-color: orange; color: white; border: none; padding: 5px 10px; cursor: pointer; margin-left: 5px; } .editable-input { width: 65px; box-sizing: border-box; } .collapsed { display: none; } </style> </head> <body> <div class="table-wrapper"> <table> <thead> <tr> <th></th> <th>plouf</th> <th>cretin</th> <?php foreach ($workingDays as $dateData): ?> <th> <?php echo $dateData['day']; ?> <br> </th> <?php endforeach; ?> </tr> <tr class="second-header"> <th></th> <th>Valeur 1</th> <th>Valeur 2</th> <?php $currentWeek = null; $weekClass = ""; foreach ($workingDays as $dateData): if ($currentWeek !== $dateData['week_number']) { $currentWeek = $dateData['week_number']; $weekClass = ($currentWeek % 2 === 0) ? 'even-week' : 'odd-week'; } ?> <th class="<?= $weekClass; ?>"><?php echo $dateData['day_of_week_initial']; ?></th> <?php endforeach; ?> </tr> <tr class="third-header"> <th></th> <th>Info 1</th> <th>Info 2</th> <?php $currentWeek = null; $colspan = 0; foreach ($workingDays as $index => $dateData): if ($currentWeek !== $dateData['week_number']) { if ($currentWeek !== null) { $weekClass = $currentWeek % 2 === 0 ? 'even-week' : 'odd-week'; echo '<th class="' . $weekClass . '" colspan="' . $colspan . '">' . $currentWeek . '</th>'; } $currentWeek = $dateData['week_number']; $colspan = 1; } else { $colspan++; } endforeach; if ($currentWeek !== null) { $weekClass = $currentWeek % 2 === 0 ? 'even-week' : 'odd-week'; echo '<th class="' . $weekClass . '" colspan="' . $colspan . '">' . $currentWeek . '</th>'; } ?> </tr> <tr class="fourth-header"> <th></th> <th>Label 1</th> <th>Label 2</th> <?php $currentMonthYear = null; $colspan = 0; $monthClass = ""; $colorIndex = 0; $colors = ['month-even', 'month-odd']; foreach ($workingDays as $index => $dateData): $monthYear = ucfirst(strftime("%B %Y", strtotime($dateData['full_date']))); if ($currentMonthYear !== $monthYear) { if ($currentMonthYear !== null) { echo '<th class="' . $monthClass . '" colspan="' . $colspan . '">' . $currentMonthYear . '</th>'; } $currentMonthYear = $monthYear; $colspan = 1; $monthClass = $colors[$colorIndex % 2]; $colorIndex++; } else { $colspan++; } endforeach; if ($currentMonthYear !== null) { echo '<th class="' . $monthClass . '" colspan="' . $colspan . '">' . $currentMonthYear . '</th>'; } ?> </tr> </thead> <tbody> </tbody> </table> </div> <button id="addRowButton">Ajouter une ligne</button> <button id="addSpecialRowButton">Ajouter une ligne spéciale</button> <script> document.addEventListener("DOMContentLoaded", function () { const tableBody = document.querySelector("tbody"); let specialRowCounter = 1; let rowCounter = 1; // Compteur pour les identifiants uniques des lignes const specialRowStates = {}; // Objet pour stocker l'état des lignes spéciales const associations = {}; // Objet pour stocker les associations entre lignes spéciales et standards const contextMenu = document.createElement("div"); contextMenu.id = "contextMenu"; contextMenu.classList.add("dropdown-menu"); contextMenu.style.display = "none"; document.body.appendChild(contextMenu); const specialContextMenu = document.createElement("div"); specialContextMenu.id = "specialContextMenu"; specialContextMenu.classList.add("dropdown-menu"); specialContextMenu.style.display = "none"; document.body.appendChild(specialContextMenu); function addRow(afterRow = null) { const rowNumber = tableBody.rows.length + 1; const newRow = document.createElement("tr"); newRow.setAttribute("draggable", "true"); newRow.dataset.rowId = `row-${rowCounter}`; // Attribuer un identifiant unique const firstCells = ["Row " + rowNumber, "Data " + rowNumber , "cretin " + rowNumber]; firstCells.forEach((text, index) => { const td = document.createElement("td"); if (index === 0) { const input = document.createElement("input"); input.type = "text"; input.classList.add("editable-input"); input.addEventListener("click", function (event) { event.stopPropagation(); input.focus(); displayRowId(newRow); // Afficher l'identifiant de la ligne }); input.addEventListener("blur", function () { input.classList.remove("editing"); }); input.addEventListener("keydown", function (event) { if (event.key === "Escape") { input.classList.remove("editing"); input.blur(); } }); td.appendChild(input); } else { td.textContent = text; } if (index === 0) { td.classList.add("row-header"); td.addEventListener("contextmenu", function (event) { event.preventDefault(); showContextMenu(event, newRow); }); } newRow.appendChild(td); }); const headerCells = document.querySelectorAll("thead tr:nth-child(2) th:not(.weekend):not(.holiday)"); const workingDaysCount = headerCells.length; for (let i = 0; i < workingDaysCount; i++) { const td = document.createElement("td"); td.textContent = "0"; td.style.backgroundColor = "white"; td.addEventListener("click", function () { td.textContent = td.textContent === "0" ? "1" : "0"; td.style.backgroundColor = td.textContent === "1" ? "green" : "white"; }); newRow.appendChild(td); } if (afterRow) { tableBody.insertBefore(newRow, afterRow.nextSibling); } else { tableBody.appendChild(newRow); } newRow.addEventListener("dragstart", dragStart); newRow.addEventListener("dragover", dragOver); newRow.addEventListener("drop", drop); rowCounter++; // Incrémenter le compteur pour le prochain identifiant } function addSpecialRow(afterRow = null) { const newRow = document.createElement("tr"); newRow.classList.add("special-row"); newRow.setAttribute("draggable", "true"); newRow.dataset.rowId = `special-row-${specialRowCounter}`; // Attribuer un identifiant unique const firstCell = document.createElement("td"); firstCell.classList.add("row-header"); const input = document.createElement("input"); input.type = "text"; input.classList.add("editable-input"); input.placeholder = `Ligne spéciale ${specialRowCounter}`; input.addEventListener("click", function (event) { event.stopPropagation(); input.focus(); displayRowId(newRow); // Afficher l'identifiant de la ligne }); input.addEventListener("blur", function () { input.classList.remove("editing"); }); input.addEventListener("keydown", function (event) { if (event.key === "Escape") { input.classList.remove("editing"); input.blur(); } }); const addButton = document.createElement("button"); addButton.classList.add("add-button"); addButton.textContent = "+"; addButton.addEventListener("click", function () { toggleCollapse(newRow); }); firstCell.appendChild(input); firstCell.appendChild(addButton); firstCell.addEventListener("contextmenu", function (event) { event.preventDefault(); showSpecialContextMenu(event, newRow); }); newRow.appendChild(firstCell); const otherCells = [`Spéciale ${specialRowCounter}-2`, `Spéciale ${specialRowCounter}-3`]; otherCells.forEach(text => { const td = document.createElement("td"); td.textContent = text; newRow.appendChild(td); }); const td = document.createElement("td"); td.textContent = `Ligne spéciale ${specialRowCounter} - Détails`; td.colSpan = document.querySelector("thead tr:nth-child(2)").cells.length - 3; newRow.appendChild(td); if (afterRow) { tableBody.insertBefore(newRow, afterRow.nextSibling); } else { tableBody.appendChild(newRow); } newRow.addEventListener("dragstart", dragStart); newRow.addEventListener("dragover", dragOver); newRow.addEventListener("drop", drop); specialRowCounter++; } function displayRowId(row) { const rowId = row.dataset.rowId; alert(`Identifiant de la ligne : ${rowId}`); } function toggleCollapse(specialRow) { const nextSpecialRow = getNextSpecialRow(specialRow); const rowsToToggle = []; let currentRow = specialRow.nextElementSibling; while (currentRow && currentRow !== nextSpecialRow) { if (!currentRow.classList.contains("special-row")) { rowsToToggle.push(currentRow); } currentRow = currentRow.nextElementSibling; } rowsToToggle.forEach(row => { row.classList.toggle("collapsed"); }); } function getNextSpecialRow(specialRow) { let currentRow = specialRow.nextElementSibling; while (currentRow) { if (currentRow.classList.contains("special-row")) { return currentRow; } currentRow = currentRow.nextElementSibling; } return null; } function showContextMenu(event, row) { contextMenu.innerHTML = ` <button class="dropdown-item" id="add-row">Ajouter une ligne</button> <button class="dropdown-item text-danger" id="delete-row">Supprimer cette ligne</button> `; contextMenu.style.top = `${event.clientY}px`; contextMenu.style.left = `${event.clientX}px`; contextMenu.style.display = "flex"; contextMenu.style.flexDirection = "column"; document.getElementById("add-row").addEventListener("click", function () { addRow(row); hideContextMenu(); }); document.getElementById("delete-row").addEventListener("click", function () { row.remove(); hideContextMenu(); }); } function showSpecialContextMenu(event, row) { specialContextMenu.innerHTML = ` <button class="dropdown-item" id="add-special-row">Ajouter une ligne spéciale</button> <button class="dropdown-item text-danger" id="delete-special-row">Supprimer cette ligne</button> <label class="dropdown-item"> <input type="checkbox" id="special-checkbox"> Option spéciale </label> `; specialContextMenu.style.top = `${event.clientY}px`; specialContextMenu.style.left = `${event.clientX}px`; specialContextMenu.style.display = "flex"; specialContextMenu.style.flexDirection = "column"; const checkbox = document.getElementById("special-checkbox"); const rowId = row.dataset.rowId; // Vérifie s'il y a des lignes standards immédiatement en dessous de la ligne spéciale let hasStandardRowsBelow = false; let currentRow = row.nextElementSibling; const standardRows = []; while (currentRow && !currentRow.classList.contains("special-row")) { if (!currentRow.classList.contains("special-row")) { hasStandardRowsBelow = true; standardRows.push(currentRow); } currentRow = currentRow.nextElementSibling; } if (hasStandardRowsBelow) { checkbox.disabled = false; } else { checkbox.disabled = true; checkbox.checked = false; } // Restaurer l'état de la case à cocher if (specialRowStates[rowId]) { checkbox.checked = specialRowStates[rowId].checked; } checkbox.addEventListener("change", function () { specialRowStates[rowId] = { checked: checkbox.checked, rows: standardRows }; standardRows.forEach(standardRow => { const firstCell = standardRow.querySelector("td:first-child"); if (checkbox.checked) { firstCell.style.backgroundColor = "yellow"; standardRow.classList.add("no-drag"); } else { firstCell.style.backgroundColor = ""; standardRow.classList.remove("no-drag"); } }); // Créer ou mettre à jour l'association if (checkbox.checked) { associations[rowId] = standardRows.map(row => row.dataset.rowId); } else { delete associations[rowId]; } console.log(`Lignes standards concernées : ${standardRows.map(row => row.rowIndex).join(", ")}`); }); document.getElementById("add-special-row").addEventListener("click", function () { addSpecialRow(row); hideSpecialContextMenu(); }); document.getElementById("delete-special-row").addEventListener("click", function () { row.remove(); hideSpecialContextMenu(); }); } document.addEventListener("click", function (event) { if (!contextMenu.contains(event.target)) { hideContextMenu(); } if (!specialContextMenu.contains(event.target)) { hideSpecialContextMenu(); } const inputs = document.querySelectorAll(".editable-input"); inputs.forEach(input => { if (!input.contains(event.target)) { input.classList.remove("editing"); } }); }); function hideContextMenu() { contextMenu.style.display = "none"; } function hideSpecialContextMenu() { specialContextMenu.style.display = "none"; } document.getElementById("addRowButton").addEventListener("click", addRow); document.getElementById("addSpecialRowButton").addEventListener("click", addSpecialRow); document.querySelectorAll("tbody tr td:first-child").forEach(td => { td.addEventListener("contextmenu", function (event) { event.preventDefault(); showContextMenu(event, td.parentElement); }); }); function dragStart(event) { const row = event.target.closest("tr"); if (row.classList.contains("no-drag")) { event.preventDefault(); return; } event.dataTransfer.setData("text/plain", row.rowIndex); row.classList.add("dragging"); } function dragOver(event) { event.preventDefault(); const draggingRow = document.querySelector(".dragging"); const targetRow = event.target.closest("tr"); if (targetRow && draggingRow !== targetRow && !targetRow.classList.contains("no-drag")) { const rect = targetRow.getBoundingClientRect(); const offset = rect.top + (rect.height / 2); if (event.clientY - offset > 0) { tableBody.insertBefore(draggingRow, targetRow.nextSibling); } else { tableBody.insertBefore(draggingRow, targetRow); } } } function drop(event) { event.preventDefault(); const draggingRow = document.querySelector(".dragging"); draggingRow.classList.remove("dragging"); // Si la ligne est une ligne spéciale, déplacer les lignes standards associées if (draggingRow.classList.contains("special-row")) { const specialRowId = draggingRow.dataset.rowId; if (associations[specialRowId]) { const associatedRowIds = associations[specialRowId]; associatedRowIds.forEach(rowId => { const associatedRow = document.querySelector(`tr[data-row-id="${rowId}"]`); if (associatedRow) { tableBody.insertBefore(associatedRow, draggingRow.nextSibling); } }); } } } // Ajouter un bouton pour afficher les associations const displayButton = document.createElement("button"); displayButton.textContent = "Afficher les associations"; document.body.appendChild(displayButton); displayButton.addEventListener("click", function () { const associationsContainer = document.createElement("div"); associationsContainer.innerHTML = "<h3>Associations entre lignes spéciales et standards :</h3>"; for (const specialRowId in associations) { const standardRowIds = associations[specialRowId]; const associationItem = document.createElement("p"); associationItem.textContent = `Ligne spéciale ${specialRowId} associée aux lignes standards : ${standardRowIds.join(", ")}`; associationsContainer.appendChild(associationItem); } document.body.appendChild(associationsContainer); }); }); </script> </body> </html>