The Timesheet < SPBCodes

The Timesheet

This is a live, editable timesheet as demonstrated in the video. User input into the timesheet will be reset every night. Bear in mind someone may be fiddling with this at the same time as you.

Please be gentle with the demo. It only has basic client-side validation and, especially given that others may be using at the same time, it is likely that overlaps and clashes will occur. The data is reset at midnight (UK local time) every day.

January 2025
DateDetailWorkedExpectedClaimedDifference
Balance Brought Forward-87:-3:00
Wednesday
1st January 2025
00:0006:0000:00-06:00
[-93:30:00]
Thursday
2nd January 2025
00:0006:0000:00-06:00
[-99:30:00]
Friday
3rd January 2025
00:0000:0000:0000:00:
[-99:30:00]
Saturday
4th January 2025
00:0000:0000:0000:00:
[-99:30:00]
Sunday
5th January 2025
00:0000:0000:0000:00:
[-99:30:00]
Monday
6th January 2025
00:0000:0000:0000:00:
[-99:30:00]
Tuesday
7th January 2025
00:0006:0000:00-06:00
[-105:30:00]
Wednesday
8th January 2025
00:0006:0000:00-06:00
[-111:30:00]
Thursday
9th January 2025
00:0006:0000:00-06:00
[-117:30:00]
Friday
10th January 2025
00:0000:0000:0000:00:
[-117:30:00]
Saturday
11th January 2025
00:0000:0000:0000:00:
[-117:30:00]
Sunday
12th January 2025
00:0000:0000:0000:00:
[-117:30:00]
Monday
13th January 2025
00:0000:0000:0000:00:
[-117:30:00]
Tuesday
14th January 2025
00:0006:0000:00-06:00
[-123:30:00]
Wednesday
15th January 2025
00:0006:0000:00-06:00
[-129:30:00]
Thursday
16th January 2025
00:0006:0000:00-06:00
[-135:30:00]
Friday
17th January 2025
00:0000:0000:0000:00:
[-135:30:00]
Saturday
18th January 2025
00:0000:0000:0000:00:
[-135:30:00]
Sunday
19th January 2025
00:0000:0000:0000:00:
[-135:30:00]
Monday
20th January 2025
00:0000:0000:0000:00:
[-135:30:00]
Tuesday
21st January 2025
00:0006:0000:00-06:00
[-141:30:00]
TODAY
22nd January 2025
00:0006:0000:00-06:00
[-147:30:00]
Thursday
23rd January 2025
00:0006:0000:00-06:00
[-153:30:00]
Friday
24th January 2025
00:0000:0000:0000:00:
[-153:30:00]
Saturday
25th January 2025
00:0000:0000:0000:00:
[-153:30:00]
Sunday
26th January 2025
00:0000:0000:0000:00:
[-153:30:00]
Monday
27th January 2025
00:0000:0000:0000:00:
[-153:30:00]
Tuesday
28th January 2025
00:0006:0000:00-06:00
[-159:30:00]
Wednesday
29th January 2025
00:0006:0000:00-06:00
[-165:30:00]
Thursday
30th January 2025
00:0006:0000:00-06:00
[-171:30:00]
Friday
31st January 2025
00:0000:0000:0000:00:
[-171:30:00]
Balance to Carry Forward-171:30:00

The SQL

This is the SQL I came up with. The original query was at least double the length of the one shown below as since I wrote the original I have learned more advanced features of SQL (see below).. Although the SQL is an improvement on the original, I am sure it can be improved upon further. I did ask ChatGPT to optimise the query. The AI offered some suggestions but it failed to produce a revised/improved query that worked even after I provided further information (e.g. initially I didn't tell it which database server I was using).

This query utilises the following, more advanced (new-ish to me), features of SQL:

  • Use of variables and parameters (e.g. to provide some initial filtering parameters and to calculate the cumulative TOIL - Time off In Lieu - balance). Note that the calculated balances are not cast as TIME fields owing to the capacity limitation of the field type (i.e. '-838:59:59' to '838:59:59'). Instead the value is calculated from the number of seconds accumulated and then appropriately formatted.
  • JSON aggregation and extracting values from JSON objects on the fly (e.g. to obtain the expected hours for a particular day from the work pattern that is in effect on that day).
  • Use of CTE (Common Table Expression) to generate the list of dates in the year without the dates having to be present in any of the database tables.
/* Set starting parameters */
SET @CurrentYearStart="2024-04-01";
SET @CurrentYearEnd="2025-03-31";
SET @ExtractStart="2024-12-01";
SET @ExtractEnd="2024-12-31";
SET @JobID=352;
select TIME_TO_SEC(TOILBalance) INTO @Balance FROM jobs WHERE ID=@JobID;	
	
/* Retrieve just the extract range */
SELECT dayname(Date) as Day,Date,StartingBalance,Worked,Shifts,`Leave`,LeaveNotes,Expected,Claimed,Difference,EndingBalance 
	FROM (
		/* Finalise some calculations, convert calculated decimal values to TIME values and give calculated columns meaningful names for the whole year  */
		SELECT 
			`Date`,
			CONCAT(FLOOR(@Balance/3600),":",lpad(FLOOR(@Balance%3600/60),2,"0"),":00") as StartingBalance,
			cast(sec_to_time(if(WorkedSeconds,WorkedSeconds,0)) AS TIME) AS Worked,
			Shifts,
			cast(sec_to_time(ExpectedSeconds*LeaveAllowance) AS TIME) AS `Leave`,
			LeaveNotes,
			cast(sec_to_time(ExpectedSeconds) as TIME) AS Expected,
			CAST(SEC_TO_TIME(( if(ExpectedSeconds,ExpectedSeconds,0)*if(LeaveAllowance,LeaveAllowance,0))+if(WorkedSeconds,WorkedSeconds,0)) AS TIME) AS Claimed,
			CAST(sec_to_time(( ExpectedSeconds*if(LeaveAllowance,LeaveAllowance,0) +  if(WorkedSeconds,WorkedSeconds,0) )  -ExpectedSeconds) as time) AS Difference,
			@Balance:=@Balance+(( ExpectedSeconds*if(LeaveAllowance,LeaveAllowance,0) +  if(WorkedSeconds,WorkedSeconds,0) )  -ExpectedSeconds) ,
			CONCAT(FLOOR(@Balance/3600),":",lpad(FLOOR(@Balance%3600/60),2,"0"),":00") AS EndingBalance
		FROM (
			/* Retrieve the raw data for the entire year  */
			SELECT 
				Dates.Date,
				sum(if(`leave`.ID,if(Dates.Date=`From`,`First`,if(Dates.Date=`To`,`Last`,1)),NULL)) AS LeaveAllowance,
				WorkedSeconds,
				Shifts,
				GROUP_CONCAT(`Type` SEPARATOR " and ") AS LeaveNotes,
				TIME_TO_SEC(JSON_UNQUOTE(JSON_EXTRACT(WorkingPattern,CONCAT("$.",DAYNAME(Dates.Date))))) AS ExpectedSeconds
			FROM (
				/* CTE to generate a list of dates for whole year */
				WITH recursive Date_Ranges AS (
					SELECT @CurrentYearStart as DATE
						union all 
						select Date + interval 1 day
						from Date_Ranges
						where DATE < @CurrentYearEnd)
					select * 
						from Date_Ranges ) AS Dates
			LEFT JOIN
				/* Foreach day retrieve whether there is any leave recorded  */
				`leave` ON Dates.Date BETWEEN `From` AND `To` AND `leave`.JobID=@JobID 
			LEFT JOIN 
				/* For each day, calculate the total worked hours in seconds. Also, store the shift data in a JSON object  */
				(SELECT 
					`checkins`.Date, 
					SUM(TIME_TO_SEC(`End`)-TIME_TO_SEC(`Start`)) AS WorkedSeconds,
					JSON_ARRAYAGG(JSON_OBJECT("ID",checkins.ID,"Start",checkins.`Start`,"End",checkins.`End`)) AS Shifts 
				FROM `checkins` GROUP BY `checkins`.Date) AS `checkins` ON Dates.Date = `checkins`.Date
			INNER JOIN 
			/* for each day, retrieve the applicable working pattern into a JSON object. as an inner join is used only dates that have a pattern defined will be retrieved */
				(SELECT 	
					`StartDate`,
					`EndDate`,
					JSON_OBJECT("Monday",Monday,"Tuesday",Tuesday,"Wednesday",Wednesday,"Thursday",Thursday,"Friday",Friday,"Saturday",Saturday,"Sunday",Sunday) as WorkingPattern 
				FROM `pattern` where JobID=@JobID) AS `pattern` ON Dates.Date  BETWEEN `pattern`.StartDate AND `pattern`.EndDate
				GROUP BY Dates.Date
			) AS CurrentYearCalculated
	) AS Extract
 HAVING Extract.Date BETWEEN @ExtractStart AND @ExtractEnd