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 | 50:00:00 | ||||
| Monday 1st April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:06:00] | |
| Tuesday 2nd April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:12:00] | |
| Wednesday 3rd April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:18:00] | |
| Thursday 4th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:24:00] | |
| Friday 5th April 2024 | 07:00 | 07:24 | 07:00 | -00:24 [50:00:00] | |
| Saturday 6th April 2024 | 00:00 | 00:00 | 00:00 | 00:00: [50:00:00] | |
| Sunday 7th April 2024 | 00:00 | 00:00 | 00:00 | 00:00: [50:00:00] | |
| Monday 8th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:06:00] | |
| Tuesday 9th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:12:00] | |
| Wednesday 10th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:18:00] | |
| Thursday 11th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:24:00] | |
| Friday 12th April 2024 | 07:00 | 07:24 | 07:00 | -00:24 [50:00:00] | |
| Saturday 13th April 2024 | 00:00 | 00:00 | 00:00 | 00:00: [50:00:00] | |
| Sunday 14th April 2024 | 00:00 | 00:00 | 00:00 | 00:00: [50:00:00] | |
| Monday 15th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:06:00] | |
| Tuesday 16th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:12:00] | |
| Wednesday 17th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:18:00] | |
| Thursday 18th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:24:00] | |
| Friday 19th April 2024 | 07:00 | 07:24 | 07:00 | -00:24 [50:00:00] | |
| Saturday 20th April 2024 | 00:00 | 00:00 | 00:00 | 00:00: [50:00:00] | |
| Sunday 21st April 2024 | 00:00 | 00:00 | 00:00 | 00:00: [50:00:00] | |
| Monday 22nd April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:06:00] | |
| Tuesday 23rd April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:12:00] | |
| Wednesday 24th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:18:00] | |
| Thursday 25th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:24:00] | |
| Friday 26th April 2024 | 07:00 | 07:24 | 07:00 | -00:24 [50:00:00] | |
| Saturday 27th April 2024 | 00:00 | 00:00 | 00:00 | 00:00: [50:00:00] | |
| Sunday 28th April 2024 | 00:00 | 00:00 | 00:00 | 00:00: [50:00:00] | |
| Monday 29th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:06:00] | |
| Tuesday 30th April 2024 | 07:30 | 07:24 | 07:30 | 00:06 [50:12:00] | |
| Balance to Carry Forward | 50:12: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 & lt; @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