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.
Date | Detail | Worked | Expected | Claimed | Difference |
---|---|---|---|---|---|
Balance Brought Forward | -87:-3:00 | ||||
Wednesday 1st January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-93:30:00] | |
Thursday 2nd January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-99:30:00] | |
Friday 3rd January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-99:30:00] | |
Saturday 4th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-99:30:00] | |
Sunday 5th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-99:30:00] | |
Monday 6th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-99:30:00] | |
Tuesday 7th January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-105:30:00] | |
Wednesday 8th January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-111:30:00] | |
Thursday 9th January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-117:30:00] | |
Friday 10th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-117:30:00] | |
Saturday 11th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-117:30:00] | |
Sunday 12th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-117:30:00] | |
Monday 13th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-117:30:00] | |
Tuesday 14th January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-123:30:00] | |
Wednesday 15th January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-129:30:00] | |
Thursday 16th January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-135:30:00] | |
Friday 17th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-135:30:00] | |
Saturday 18th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-135:30:00] | |
Sunday 19th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-135:30:00] | |
Monday 20th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-135:30:00] | |
Tuesday 21st January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-141:30:00] | |
TODAY 22nd January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-147:30:00] | |
Thursday 23rd January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-153:30:00] | |
Friday 24th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-153:30:00] | |
Saturday 25th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-153:30:00] | |
Sunday 26th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-153:30:00] | |
Monday 27th January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-153:30:00] | |
Tuesday 28th January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-159:30:00] | |
Wednesday 29th January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-165:30:00] | |
Thursday 30th January 2025 | 00:00 | 06:00 | 00:00 | -06:00 [-171:30:00] | |
Friday 31st January 2025 | 00:00 | 00:00 | 00:00 | 00:00: [-171:30:00] | |
Balance to Carry Forward | -171:30:00 |
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:
/* 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