Comment créer des jours de semaine récurrents en colonnes dans un pivot?

8

Je suis nouveau dans la programmation et les bases de données et je vous serais reconnaissant de m'aider dans le scénario suivant.

J'utilise PHP avec SQL Server. Je construis un système de présence des employés et je voudrais créer un tableau (pivot) avec des mois comme lignes et tous les jours de la semaine comme colonnes (pour une année spécifique). Les valeurs dans les cellules seront le nombre de jours (1, 2, 3 ... 31).

La couleur d'arrière-plan de la cellule (existe déjà en tant que colonne de tableau) déclare le type de congé des employés. Le tableau présente les colonnes suivantes: employee_id, leave_date, leave_type, leave_type_color.

Je veux obtenir un résultat comme ci-dessous:

entrez la description de l'image ici

Je vous remercie.

Mike T
la source
Merci pour un problème intéressant! Je ne suis pas enthousiaste à l'idée de mélanger les données et la présentation, mais dans certains cas, avoir toute la logique au même endroit peut être pratique.
Aaron Bertrand

Réponses:

11

La partie la plus complexe de cela consiste simplement à créer le calendrier dans ce format. Faire pivoter et l'entourer de HTML est assez facile. Tout d'abord, commençons par cela, votre table des employés avec les dates de congé. leave_typene semblait pas pertinent pour le problème en question.

CREATE TABLE dbo.EmpLeave
(
  EmployeeID int,
  leave_date date,
  leave_type_color char(6)
);

INSERT dbo.EmpLeave(EmployeeID,leave_date,leave_type_color)
  VALUES(1,'2018-01-02','7777cc'),(1,'2018-04-01','ffffac');

La procédure que j'ai trouvée ressemble à ceci (et avertissement: cela suppose @@DATEFIRST = 7):

CREATE PROCEDURE dbo.BuildLeaveHTMLTable
  @EmployeeID int,
  @Year smallint = NULL
AS
BEGIN
  SET NOCOUNT ON;
  SET @Year = COALESCE(@Year, DATEPART(YEAR, GETDATE()));
  DECLARE @FirstDay date = DATEADD(YEAR, @Year-1900, 0);

  ;WITH Numbers AS ( -- 366 possible days (leap year)
    SELECT n = 1 UNION ALL SELECT n + 1 FROM Numbers WHERE n <= 365
  ),
  Calendar AS ( -- a year's worth of dates and dateparts 
    SELECT [Date] = d,
      MonthStart = DATEADD(DAY, 1-DAY(d),d),
      Y  = CONVERT(smallint, DATEPART(YEAR,   d)),
      M  = CONVERT(tinyint,  DATEPART(MONTH,  d)),
      D  = CONVERT(tinyint,  DATEPART(DAY,    d)),
      WY = CONVERT(tinyint,  DATEPART(WEEK,   d)),
      DW = CONVERT(tinyint,  DATEPART(WEEKDAY,d))
    FROM
    (
      SELECT d = CONVERT(date,DATEADD(DAY, n-1, @FirstDay)) FROM Numbers
    ) AS c WHERE YEAR(d) = @Year -- in case it's not a leap year
  ),
  BaseSlots AS ( -- base set of 37 ints 
   -- month can be spread across 6 weeks, but no more than 2 days in 6th week
    SELECT TOP (37) slot = n FROM Numbers ORDER BY n
  ),
  Months AS ( -- base set of 12 ints
    SELECT TOP (12) m = slot FROM BaseSlots ORDER BY slot
  ),
  SlotAlignment AS ( -- align days of week to slot numbers
    -- this is the most cryptic part of this solution
    -- determines which set of 7 slots, and which slot 
    -- exactly, a given date will appear under
    SELECT c.*, slot = DW+(c.WY+1-DATEPART(WEEK,c.MonthStart)-1)*7
      FROM Calendar AS c 
      INNER JOIN Months AS m ON c.M = m.m
  ),
  SlotMatrix AS ( -- extrapolate actual dates to 37 x 12 matrix
    SELECT m.m, s.slot, sa.[Date] 
      FROM BaseSlots AS s 
      CROSS JOIN Months AS m
      LEFT OUTER JOIN SlotAlignment AS sa
      ON sa.m = m.m AND sa.slot = s.slot
  ),
  FinalHTML AS ( -- build some HTML!
    SELECT m = '<!-- ' + RIGHT('0' + RTRIM(m), 2) + ' -->', 
      slot, cell = CASE WHEN slot = 1 THEN '<tr><th>' 
        + COALESCE(DATENAME(MONTH,DATEADD(MONTH, m-1, 0)),'') 
        + '</th>' ELSE '' END + '<td' + COALESCE(' bgcolor=#' 
        + RIGHT(CONVERT(varchar(10),CONVERT(varbinary(8), el.leave_type_color),1),6),
          CASE WHEN DATEPART(WEEKDAY, [Date]) IN (1,7) 
          THEN ' bgcolor=#cccccc' ELSE '' END)
        + '>' + COALESCE(RTRIM(DATEPART(DAY,[Date])), '&nbsp;')
        + '</td>' + CASE WHEN slot = 37 THEN '</tr>' ELSE '' END
      FROM SlotMatrix AS q LEFT OUTER JOIN dbo.EmpLeave AS el
      ON q.Date = el.leave_date
      AND el.EmployeeID = @EmployeeID
  ) -- now turn it sideways
  SELECT m = '<!-- 00 -->', 
    [1]  = '<tr><th>Month</th><th>S</th>',    [2]  = '<th>M</th>', 
    [3]  = '<th>T</th>', [4]  = '<th>W</th>', [5]  = '<th>T</th>', 
    [6]  = '<th>F</th>', [7]  = '<th>S</th>', [8]  = '<th>S</th>', 
    [9]  = '<th>M</th>', [10] = '<th>T</th>', [11] = '<th>W</th>',
    [12] = '<th>T</th>', [13] = '<th>F</th>', [14] = '<th>S</th>', 
    [15] = '<th>S</th>', [16] = '<th>M</th>', [17] = '<th>T</th>',
    [18] = '<th>W</th>', [19] = '<th>T</th>', [20] = '<th>F</th>', 
    [21] = '<th>S</th>', [22] = '<th>S</th>', [23] = '<th>M</th>',
    [24] = '<th>T</th>', [25] = '<th>W</th>', [26] = '<th>T</th>', 
    [27] = '<th>F</th>', [28] = '<th>S</th>', [29] = '<th>S</th>', 
    [30] = '<th>M</th>', [31] = '<th>T</th>', [32] = '<th>W</th>', 
    [33] = '<th>T</th>', [34] = '<th>F</th>', [35] = '<th>S</th>',
    [36] = '<th>S</th>', [37] = '<th>M</th>'
  UNION ALL
  (
    SELECT * FROM FinalHTML PIVOT (MAX(cell) FOR slot IN 
    (
     [1], [2], [3], [4], [5], [6], [7], [8], [9], [10],[11],[12],[13],[14],
     [15],[16],[17],[18],[19],[20],[21],[22],[23],[24],[25],[26],[27],[28],
     [29],[30],[31],[32],[33],[34],[35],[36],[37]
    )) AS p
  )
  ORDER BY m OPTION (MAXRECURSION 366);
END
GO

Résultats de cet appel:

EXEC dbo.BuildLeaveHTMLTable @EmployeeID = 1;

Regardez comme ça (je me suis arrêté à la colonne du 7ème jour):

entrez la description de l'image ici

Vous devrez ajouter le <table>/ </table>wrapper vous-même, mais voici à quoi ressemble la sortie lorsqu'elle est placée entre ceux-ci et enregistrée au format HTML (et bien sûr, vous pouvez encore l'améliorer avec CSS):

! [entrez la description de l'image ici

Lorsque le congé tombe un week-end, la couleur du congé l'emporte sur la couleur du week-end, mais cela est facile à ajuster. Change ça:

  + COALESCE(' bgcolor=#' + RTRIM(el.leave_type_color),
      CASE WHEN DATEPART(WEEKDAY, [Date]) IN (1,7) 
      THEN ' bgcolor=#cccccc' ELSE '' END)

Pour ça:

  + CASE WHEN DATEPART(WEEKDAY, [Date]) IN (1,7) 
      THEN ' bgcolor=#cccccc' ELSE COALESCE(' bgcolor=#' 
      + RTRIM(el.leave_type_color), '') END

Pour convertir une couleur au format décimal (comme 65280) en son équivalent RVB ( 00FF00), vous devez faire un tas de manipulation. J'envisagerais de le stocker en tant qu'hexagone RVB en premier lieu, mais j'ai mis à jour la solution ici avec quelque chose de similaire à ceci:

SELECT RIGHT(CONVERT(varchar(10),CONVERT(varbinary(8), 65280),1),6);
Aaron Bertrand
la source
Ouaip. Ce qu'Aaron a dit.
Rob Farley
2
Vous êtes tellement bizarre.
Erik Darling
Merci pour l'aide.Je reçois l'erreur: La conversion a échoué lors de la conversion de la valeur varchar '>' en type de données int.
Mike T
@MikeT Ce code est entièrement testé, qu'avez-vous changé? La leave_type_colorcolonne est-elle numérique?
Aaron Bertrand
1) "DECLARE @return_value int" joue-t-il un rôle lorsque j'exécute la procédure dans SQL 2016? 2) J'ai changé quelques noms de colonnes car la table de congé est une jointure de 2 autres tables.leave_type_color est un entier.
Mike T du
1

Commencez par considérer ce que vous voulez avoir comme colonnes, et c'est essentiellement "Semaine 1 Jour 1 (Dim)", "Semaine 1 Jour 2 (Lun)", jusqu'à "Semaine 6 Jour 7 (Sam)". Essentiellement, Jour 1-42. Le 1er janvier est alors «Semaine 1 Jour 2» de janvier. Je vais appeler ce WeekPlusDay pour l'instant.

Pour déterminer où commence chacun, considérez simplement la partie du jour de la semaine.

Votre jeu de données n'a alors qu'à inclure cette valeur «WeekPlusDay», et vous affichez DayOfMonth.

Rob Farley
la source