Wie erstelle ich wiederkehrende Wochentage als Spalten in einem Pivot?

8

Ich bin ein Neuling in Programmierung und Datenbanken und wäre für einige Hilfe im folgenden Szenario dankbar.

Ich benutze PHP mit SQL Server. Ich baue ein Anwesenheitssystem für Mitarbeiter auf und möchte eine (Pivot-) Tabelle mit Monaten als Zeilen und allen Wochentagsnamen als Spalten (für ein bestimmtes Jahr) erstellen. Die Werte in Zellen sind die Anzahl der Tage (1, 2, 3 ... 31).

Die Hintergrundfarbe der Zelle (bereits als Tabellenspalte vorhanden) gibt die Art des Urlaubs der Mitarbeiter an. Die Tabelle enthält die folgenden Spalten : employee_id, leave_date, leave_type, leave_type_color.

Ich möchte ein Ergebnis wie das folgende erzielen:

Geben Sie hier die Bildbeschreibung ein

Vielen Dank.

Mike T.
quelle
Vielen Dank für ein interessantes Problem! Ich bin nicht begeistert davon, Daten und Präsentationen zu mischen, aber in einigen Fällen kann es praktisch sein, die gesamte Logik an einem Ort zu haben.
Aaron Bertrand

Antworten:

11

Der komplexeste Teil davon besteht darin, den Kalender in diesem Format zu erstellen. Das Schwenken und Umgeben mit HTML ist ziemlich einfach. Beginnen wir zunächst mit Ihrer Mitarbeitertabelle mit Urlaubsterminen. leave_typeschien für das vorliegende Problem nicht relevant zu sein.

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');

Das Verfahren, das ich mir ausgedacht habe, sieht folgendermaßen aus (und Warnung: es wird davon ausgegangen @@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

Ergebnisse dieses Aufrufs:

EXEC dbo.BuildLeaveHTMLTable @EmployeeID = 1;

Sieh so aus (ich habe an der 7. Tagesspalte angehalten):

Geben Sie hier die Bildbeschreibung ein

Sie müssen den <table>/ </table>wrapper selbst hinzufügen , aber so sieht die Ausgabe aus, wenn sie zwischen diese eingefügt und als HTML gespeichert wird (und natürlich können Sie sie mit CSS weiter verbessern):

! [Bildbeschreibung hier eingeben

Wenn der Urlaub auf ein Wochenende fällt, übertrifft die Urlaubsfarbe die Wochenendfarbe, aber das ist einfach anzupassen. Ändere das:

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

Dazu:

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

Um eine Farbe im Dezimalformat (wie 65280) in ihr RGB-Äquivalent ( 00FF00) zu konvertieren , müssen Sie einige Manipulationen vornehmen. Ich würde in Betracht ziehen, es zunächst als RGB-Hex zu speichern, aber ich habe die Lösung hier mit etwas Ähnlichem aktualisiert:

SELECT RIGHT(CONVERT(varchar(10),CONVERT(varbinary(8), 65280),1),6);
Aaron Bertrand
quelle
Jep. Was Aaron gesagt hat.
Rob Farley
2
Du bist so seltsam.
Erik Darling
Vielen Dank für die Hilfe. Ich erhalte die Fehlermeldung: Die Konvertierung ist fehlgeschlagen, als der varchar-Wert '>' in den Datentyp int konvertiert wurde.
Mike T
@MikeT Dieser Code wurde vollständig getestet. Was haben Sie geändert? Ist die leave_type_colorSpalte numerisch?
Aaron Bertrand
1) Spielt "DECLARE @return_value int" eine Rolle, wenn ich die Prozedur in SQL 2016 ausführe? 2) Ich habe einige Spaltennamen geändert, da die Leave-Tabelle eine Verknüpfung von zwei anderen Tabellen ist. Leave_type_color ist eine Ganzzahl.
Mike T
1

Überlegen Sie zunächst, was Sie als Spalten haben möchten, und das ist im Grunde genommen „Woche 1 Tag 1 (So)“, „Woche 1 Tag 2 (Mo)“ bis „Woche 6 Tag 7 (Sa)“. Im Wesentlichen Tag 1-42. Der 1. Januar ist dann „Woche 1 Tag 2“ im Januar. Ich werde diesen WeekPlusDay jetzt anrufen.

Um herauszufinden, wo jeder beginnt, berücksichtigen Sie einfach den Wochentagsteil des Datums.

Ihr Datensatz muss dann nur noch den Wert "WeekPlusDay" enthalten, und Sie zeigen den DayOfMonth an.

Rob Farley
quelle