Description du script

Ce script ajoute automatiquement une frise chronologique interactive sur la page issuehistory.pl dans Koha. Il permet de visualiser l’historique des prêts par code-barres sous forme de barres chronologiques colorées, facilitant ainsi l’analyse des périodes d’emprunt d’un document.

Explication détaillée de l’algorithme

  1. Le script vérifie que l'utilisateur est sur la page issuehistory.pl.
  2. Il charge dynamiquement la bibliothèque D3.js (v7) pour générer la visualisation.
  3. Une feuille de style minimale est injectée pour la mise en page de la frise.
  4. Le script parcourt les lignes du tableau de l'historique des prêts pour en extraire :
  5. Il construit un ensemble de données structurées par code-barres, contenant les événements de prêt.
  6. Il détermine la date la plus ancienne et la plus récente pour adapter l’échelle de la frise.
  7. Avec D3.js, il crée un graphique SVG :

Code JavaScript

//Frise chronologique historique de prêt notices
if (window.location.href.includes("issuehistory.pl")) {
  const script = document.createElement('script');
  script.src = '<https://d3js.org/d3.v7.min.js>';
  document.head.appendChild(script);

  script.onload = function() {
    const style = document.createElement('style');
    style.innerHTML = `
      .timeline-container-wrapper {
        width: 100%;
        overflow-x: scroll;
        margin-top: 20px;
        background-color: white;
      }
      .timeline-chart-container {
        position: relative;
        width: 100%;
        height: 300px;
      }
      .custom-tooltip {
        display: none;
        position: absolute;
        background-color: #333;
        color: white;
        padding: 10px;
        border-radius: 5px;
        font-size: 12px;
        z-index: 10;
        max-width: 350px;
        box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
        white-space: pre-wrap;
        word-wrap: break-word;
      }
    `;
    document.head.appendChild(style);

    function createEventData(name, detail, startDate, endDate) {
      return {
        label: `${name} - ${detail}`,
        startDate: startDate,
        endDate: endDate,
        duration: (endDate - startDate) / (1000 * 3600 * 24)
      };
    }

    function formatDate(date) {
      const day = String(date.getDate()).padStart(2, '0');
      const month = String(date.getMonth() + 1).padStart(2, '0');
      const year = date.getFullYear();
      return `${day}/${month}/${year}`;
    }

    function parseDate(dateString) {
      const dateParts = dateString.split(' ')[0].split('/').map(Number); // Extraire la date sans l'heure
      const timeParts = dateString.split(' ')[1]?.split(':'); // Extraire l'heure, si elle existe

      if (dateParts.length === 3 && timeParts?.length === 2) {
        const [day, month, year] = dateParts;
        const [hours, minutes] = timeParts.map(Number);
        const parsedDate = new Date(year, month - 1, day, hours, minutes);
        console.log("Date parsée :", dateString, "->", parsedDate);
        return parsedDate;
      }
      console.error("Erreur de parsing de la date :", dateString);
      return new Date(); // Retourner la date actuelle en cas d'erreur
    }

    const timelineContainer = document.createElement("div");
    timelineContainer.classList.add("timeline-container-wrapper");

    const searchResultsContainer = document.querySelector('.searchresults');
    if (searchResultsContainer) {
      searchResultsContainer.appendChild(timelineContainer);
    } else {
      document.body.appendChild(timelineContainer);
    }

    const barcodeEvents = {};
    let minDate = new Date();
    let maxDate = new Date(0);

    const table = document.getElementById("table_issues");
    const rows = table.querySelectorAll("tbody tr");

    rows.forEach(row => {
      const cells = row.querySelectorAll("td");
      if (cells.length < 8) return;

      const name = cells[0].textContent.trim();
      const barcode = cells[1].textContent.trim();
      const detail = cells[2].textContent.trim();
      const startDate = parseDate(cells[5].textContent.trim());  // Utilisation de parseDate ici
      const endDate = cells[7] ? parseDate(cells[7].textContent.trim()) : new Date(startDate);

      if (startDate < minDate) minDate = startDate;
      if (endDate > maxDate) maxDate = endDate;

      if (!barcodeEvents[barcode]) {
        barcodeEvents[barcode] = { label: barcode, events: [] };
      }
      barcodeEvents[barcode].events.push(createEventData(name, detail, startDate, endDate));
    });

    const chartData = [];
    Object.keys(barcodeEvents).forEach(barcode => {
      barcodeEvents[barcode].events.forEach(event => {
        const startPosition = (event.startDate - minDate) / (1000 * 3600 * 24);
        const duration = event.duration;

        chartData.push({
          label: event.label,
          startDate: startPosition,
          duration: duration,
          barcode: barcode
        });
      });
    });
    const width = 1000;
    const height = 300;
  const margin = { top: 20, right: 20, bottom: 50, left: 80 };  // Augmenter la marge gauche de 40 à 80

    const xScale = d3.scaleLinear()
      .domain([0, (maxDate - minDate) / (1000 * 3600 * 24)])
      .range([margin.left, width - margin.right]);

    const yScale = d3.scaleBand()
      .domain(chartData.map(d => d.barcode))
      .range([margin.top, height - margin.bottom])
      .padding(0.1);

    const svg = d3.select(timelineContainer)
      .append("svg")
      .attr("width", width)
      .attr("height", height)
      .attr("viewBox", `0 0 ${width} ${height}`)
      .attr("style", "border: 1px solid #c9c9c9;");

    // Ajouter les traits verticaux pour l'axe des X
    svg.selectAll(".x-axis-line")
      .data(xScale.ticks(10))
      .enter()
      .append("line")
      .attr("class", "x-axis-line")
      .attr("x1", d => xScale(d))
      .attr("x2", d => xScale(d))
      .attr("y1", margin.top)
      .attr("y2", height - margin.bottom)
      .attr("stroke", "#ccc")
      .attr("stroke-width", 1);

    svg.selectAll(".event-bar")
      .data(chartData)
      .enter()
      .append("rect")
      .attr("class", "event-bar")
      .attr("x", d => xScale(d.startDate))
      .attr("y", d => yScale(d.barcode))
      .attr("width", d => xScale(d.startDate + d.duration) - xScale(d.startDate))
      .attr("height", yScale.bandwidth())
      .attr("fill", "rgba(0, 123, 255, 0.5)")
      .attr("stroke", "rgba(0, 123, 255, 0.5)")
      .attr("stroke-width", 1)
      .on("mouseover", function(event, d) {
        const tooltip = d3.select(".custom-tooltip");
        tooltip.style("display", "block")
          .html(`<strong>${d.label}</strong><br/>
De: ${formatDate(new Date(minDate.getTime() + d.startDate * 1000 * 3600 * 24))}<br/>
À: ${formatDate(new Date(minDate.getTime() + (d.startDate + d.duration) * 1000 * 3600 * 24))}<br/>
Durée: ${Math.round(d.duration)} jours<br/>
Code-barres: ${d.barcode}`);
      })
      .on("mousemove", function(event) {
        const tooltip = d3.select(".custom-tooltip");
        tooltip.style("top", (event.pageY + 10) + "px")
          .style("left", (event.pageX + 10) + "px");
      })
      .on("mouseout", function() {
        d3.select(".custom-tooltip").style("display", "none");
      });

    const tooltip = d3.select("body").append("div").attr("class", "custom-tooltip");

  
svg.append("text")
  .attr("x", width / 2)
  .attr("y", margin.top / 2 + 10)  // Ajout de 20px pour faire descendre le texte
  .attr("text-anchor", "middle")
  .style("font-size", "18px")
  .style("font-weight", "bold")
  .style("margin", "-15px")
  .text("Historique de prêt");

    svg.append("g")
      .selectAll(".tick")
      .data(xScale.ticks(10))
      .enter()
      .append("text")
      .attr("x", d => xScale(d))
      .attr("y", height - margin.bottom + 20)
      .attr("text-anchor", "middle")
      .text(d => formatDate(new Date(minDate.getTime() + d * 1000 * 3600 * 24)))
      .style("font-size", "12px");

    svg.append("g")
      .selectAll(".tick")
      .data(yScale.domain())
      .enter()
      .append("text")
      .attr("x", margin.left - 10)
      .attr("y", d => yScale(d) + yScale.bandwidth() / 2)
      .attr("dy", ".35em")
      .attr("text-anchor", "end")
      .text(d => d)
      .style("font-size", "12px");
  };
                  }

Éléments à modifier

Points clés pour les mises à jour de Koha

Dépendances