¿Cómo crear días de semana recurrentes como columnas en un pivote?

8

Soy un recién llegado a la programación y las bases de datos y agradecería un poco de ayuda en el siguiente escenario.

Yo uso PHP con SQL Server. Estoy creando un sistema de asistencia de empleados y me gustaría crear una tabla (pivote) con meses como filas y todos los días de la semana como columnas (para un año específico). Los valores en las celdas serán el número de días (1, 2, 3 ... 31).

El color de fondo de la celda (ya existe como columna de tabla) declara el tipo de licencia de los empleados. La tabla tiene las siguientes columnas: employee_id, leave_date, leave_type, leave_type_color.

Quiero lograr un resultado como el siguiente:

ingrese la descripción de la imagen aquí

Gracias.

Mike T
fuente
¡Gracias por un problema interesante! No me entusiasma mezclar datos y presentaciones, pero en algunos casos tener toda la lógica en un solo lugar puede ser práctico.
Aaron Bertrand

Respuestas:

11

La parte más compleja de esto es simplemente construir el calendario en ese formato. Girar y rodearlo con HTML es bastante fácil. Primero, comencemos con esto, su tabla de empleados con fechas de licencia. leave_typeno parecía relevante para el problema en cuestión.

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

El procedimiento que se me ocurrió se ve así (y advertencia: se supone @@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

Resultados de esta convocatoria:

EXEC dbo.BuildLeaveHTMLTable @EmployeeID = 1;

Se parece a esto (me detuve en la columna del séptimo día):

ingrese la descripción de la imagen aquí

Tendrá que agregar el <table>/ </table>wrapper usted mismo, pero así es como se ve el resultado cuando se coloca entre ellos y se guarda como HTML (y, por supuesto, puede mejorarlo aún más con CSS):

! [ingrese la descripción de la imagen aquí

Cuando la licencia cae en un fin de semana, el color de la licencia triunfa sobre el color del fin de semana, pero eso es fácil de ajustar. Cambia esto:

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

A esto:

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

Para convertir un color en formato decimal (como 65280) a su equivalente RGB ( 00FF00), debe hacer un montón de manipulación. Consideraría almacenarlo como hexadecimal RGB en primer lugar, pero actualicé la solución aquí con algo similar a esto:

SELECT RIGHT(CONVERT(varchar(10),CONVERT(varbinary(8), 65280),1),6);
Aaron Bertrand
fuente
Sip. Lo que dijo Aaron.
Rob Farley
2
Eres muy extraño.
Erik Darling
Gracias por la ayuda. Recibo el error: Error de conversión al convertir el valor varchar '>' al tipo de datos int.
Mike T
@MikeT Ese código está completamente probado, ¿qué cambiaste? ¿Es la leave_type_colorcolumna numérica?
Aaron Bertrand
1) ¿"DECLARE @return_value int" juega un papel cuando ejecuto el procedimiento en SQL 2016? 2) Cambié algunos nombres de columna porque la tabla de abandono es una combinación de otras 2 tablas.leave_type_color es entero.
Mike T
1

Comience considerando lo que quiere tener como columnas, y eso es básicamente "Semana 1 Día 1 (Sol)", "Semana 1 Día 2 (lunes)", hasta "Semana 6 Día 7 (sábado)". Esencialmente, día 1-42. El 1 de enero es entonces "Semana 1 Día 2" de enero. Llamaré a este WeekPlusDay por ahora.

Para averiguar dónde comienza cada uno, solo considere la parte del día de la semana de la fecha.

Su conjunto de datos solo tiene que incluir ese valor "WeekPlusDay", y usted muestra el DayOfMonth.

Rob Farley
fuente