Build a timesheet with automatic overtime rules
Compute regular and overtime hours per entry — anything over 40 hours/week or 8/day flows to overtime.
Published Dec 3, 2025
Northwind’s contractors bill regular and overtime hours at different rates, so the timesheet has to split every entry into the two buckets. The rule sounds simple — anything over eight hours in a day or forty in a week is overtime — but it is fiddly to apply by hand, because the daily and weekly limits interact: a long Monday can use up the week’s regular allowance early, pushing later days into overtime even if they were short.
This script does the split for you. It reads the raw timesheet, groups entries by person and week, then walks each week in date order, filling the regular bucket up to both limits before anything spills into overtime. It writes the two numbers back next to each entry, ready for billing.
What you’ll need
- A timesheet sheet with a header row and columns named
person,date,hours,regular, andovertime. One row per person per day. - The
regularandovertimecolumns can start empty — the script fills them. hoursanddatestored as real numbers and dates, not text.
The script
// The spreadsheet holding the raw timesheet entries.
const TIMESHEET_SHEET_ID = '1abcTimesheetsId';
// Overtime thresholds. Hours beyond either limit count as overtime.
const DAILY_LIMIT = 8; // regular hours allowed in a single day
const WEEKLY_LIMIT = 40; // regular hours allowed in a single week
/**
* Splits every timesheet entry into regular and overtime hours,
* applying both the daily and the weekly limit, and writes the
* two figures back beside each row.
*/
function computeOvertime() {
const sheet = SpreadsheetApp.openById(TIMESHEET_SHEET_ID).getSheets()[0];
const [header, ...rows] = sheet.getDataRange().getValues();
// Stop early if there is nothing but a header.
if (!rows.length) {
Logger.log('No timesheet entries — nothing to compute.');
return;
}
// Map column names to their index so we can read by name.
const col = Object.fromEntries(header.map((key, i) => [key, i]));
// 1. Group entries by person and week, keeping the sheet row number.
const grouped = new Map(); // "person|weekStart" -> entry list
rows.forEach((r, i) => {
const week = startOfWeek(r[col.date]);
const key = `${r[col.person]}|${week.toISOString()}`;
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push({ rowIndex: i + 2, hours: r[col.hours] });
});
// 2. Work through each person-week, filling regular before overtime.
for (const [, entries] of grouped) {
let weeklyRegular = 0; // regular hours used so far this week
for (const entry of entries) {
// Cap today's regular hours at the daily limit.
const regularToday = Math.min(entry.hours, DAILY_LIMIT);
// ...and at whatever regular room is left in the week.
const weeklyRoom = Math.max(0, WEEKLY_LIMIT - weeklyRegular);
const regular = Math.min(regularToday, weeklyRoom);
// Everything else on this entry is overtime.
const overtime = entry.hours - regular;
weeklyRegular += regular;
// 3. Write the split back beside the entry.
sheet.getRange(entry.rowIndex, col.regular + 1).setValue(regular);
sheet.getRange(entry.rowIndex, col.overtime + 1).setValue(overtime);
}
}
Logger.log('Overtime computed for ' + grouped.size + ' person-weeks.');
}
/**
* Returns the Sunday-midnight start of the week containing `d`,
* used as a stable key for grouping a week's entries.
*/
function startOfWeek(d) {
const week = new Date(d);
week.setDate(week.getDate() - week.getDay());
week.setHours(0, 0, 0, 0);
return week;
}
How it works
computeOvertimereads the whole timesheet and builds acollookup so it can address columns by name rather than by position.- It groups the rows into a
Mapkeyed by person plus the start of their week.startOfWeeksnaps any date back to the Sunday midnight before it, so every entry in the same calendar week shares one key. Each grouped entry keeps its real sheet row number for writing back later. - For each person-week it processes the entries in turn, tracking
weeklyRegular— the regular hours already used. - An entry’s regular hours are the smallest of three things: the hours worked,
the
DAILY_LIMIT, and the regular room left in the week. Whatever is left over becomes overtime. - The regular figure is added to the weekly tally, so once a week reaches forty regular hours every later entry flows entirely to overtime.
- The two numbers are written back into the
regularandovertimecolumns next to each entry.
Example run
Say one contractor logs this week (Sunday-start week, WEEKLY_LIMIT 40,
DAILY_LIMIT 8):
| date | hours | → regular | → overtime |
|---|---|---|---|
| Mon | 10 | 8 | 2 |
| Tue | 9 | 8 | 1 |
| Wed | 8 | 8 | 0 |
| Thu | 8 | 8 | 0 |
| Fri | 7 | 7 | 0 |
By Friday the week has 39 regular hours, so all 7 of Friday’s hours stay regular. The daily limit catches Monday and Tuesday; the weekly limit would catch a sixth day entirely. Total: 39 regular, 3 overtime.
Trigger it
Run the split on a daily schedule so the timesheet is always current:
- In the Apps Script editor, open Triggers (the clock icon).
- Add a trigger for
computeOvertime, time-driven, day timer, late in the evening after the day’s entries are in.
Running daily means the regular and overtime columns are ready first thing
for billing, without anyone running it by hand.
Watch out for
- Entries are processed in the order they appear in the sheet. If the rows for a person-week are out of date order, the weekly allowance fills in the wrong sequence. Keep the sheet sorted by person then date.
- The week starts on Sunday, because
getDay()returns 0 for Sunday. If Northwind’s payroll week starts on Monday, adjuststartOfWeekaccordingly. - The script overwrites the
regularandovertimecolumns every run. Do not hand-edit those cells — your changes will be replaced. - It assumes one row per person per day. Two entries for the same person and date are treated independently, so neither is capped at eight by itself — sum them into a single row first.
- Times are taken as plain hours. A
datevalue carrying a time component still groups correctly, but make surehoursis a number, not a duration.
Related
Build an OKR tracker with progress rollups
Aggregate Northwind's key-result progress into objective-level scores automatically.
Updated Jan 14, 2026
Build a recurring-task generator
Spawn new Northwind task rows on a daily or weekly cadence from a Recurring sheet.
Updated Jan 10, 2026
Build a multi-sheet search-and-jump tool
Find a value across every tab of a workbook and click through to the cell that contains it.
Updated Jan 7, 2026
Auto-rebuild grouped summaries on edit
Refresh pivot-style rollups the instant data changes — no manual recompute.
Updated Jan 4, 2026
Build a data-quality scorecard
Grade any sheet on completeness, validity, and freshness — surface gaps as a single score.
Updated Dec 31, 2025