Initial commit
All checks were successful
Docker Build and Publish / build-and-push (push) Successful in 2m17s

This commit is contained in:
2026-01-11 18:55:03 -05:00
commit 493ea4688c
45 changed files with 7107 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
import PocketBase from 'pocketbase';
export const pb = new PocketBase('http://127.0.0.1:8090');

View File

@@ -0,0 +1,234 @@
/**
* usage: calculatedHours = calculateHours(duration, type, dayType)
*
* Rules based on IT Group Collective Agreement (IT-01 to IT-04):
* - Workday OT: 1.5x for first 7.5h, 2.0x thereafter.
* - Rest Day 1 (Sat): 1.5x for first 7.5h, 2.0x thereafter.
* - Rest Day 2 (Sun): 2.0x for all hours.
* - Holiday: 1.5x for first 7.5h, 2.0x thereafter (plus regular pay if applicable, but we track OT here).
* - Call-back: Minimum 3h pay (at applicable rate).
* - Reporting Pay: Minimum 4h pay (at straight time) or worked time at OT rate, whichever is greater.
* (Simplified: If worked < 3h? IT rules say 3h min for Callback. 4h min for Reporting on Rest Day).
*
* Assumptions:
* - This function returns the "Banked Equivalent" hours.
* e.g., 1h worked at 1.5x = 1.5 banked hours.
* - Non-Contiguous Hours (Workday): Greater of (Actual * OT) or (2h * Straight Time).
*/
export const calculateBankedHours = (duration, type, dayType) => {
let multiplier = 1.0;
let earned = 0;
// Convert duration to number
const hours = parseFloat(duration) || 0;
if (hours <= 0) return 0;
// --- STANDBY ---
if (type === 'standby') {
// Article 11.01: 0.5h pay for each 4h period. => 0.125 multiplier.
// Or we can just store it as 1h for every 8.
// Let's use exact calculation: hours * (0.5 / 4)
return hours * 0.125;
}
// --- CALL-BACK ---
if (type === 'callback') {
// Article 10.01: Greater of 3h pay or time worked at OT rate.
// We calculate OT rate first.
let workedValue = calculateOvertimeValue(hours, dayType);
// Clause 10.01 (a) says "three (3) hours' pay at the *applicable rate for overtime*"
const applicableRate = getBaseOvertimeRate(dayType);
const minPay = 3 * applicableRate;
// If we work long enough to hit 2.0x threshold on a 1.5x day, we need exact calculation
return Math.max(minPay, workedValue);
}
// --- NON-CONTIGUOUS HOURS ---
if (type === 'non_contiguous') {
// Article 9.01 (4): Greater of "time actually worked"(at OT rate) OR "minimum of two (2) hours' pay at straight time".
// Note: 9.01(4) generally applies "on that day" (workday).
// 2h pay at straight time = 2.0 banked hours.
let workedValue = calculateOvertimeValue(hours, dayType);
// Minimum is 2h pay at STRAIGHT time (1.0x).
const minPay = 2.0;
return Math.max(minPay, workedValue);
}
// --- REGULAR / OVERTIME ---
if (type === 'overtime' || type === 'regular') {
// "Regular" on a Time Tracker for Overtime usually implies "Regular Overtime" unless it is standard hours?
// If it's standard 7.5h workday, multiplier is 1.0.
// But the user tool is for "Overtime and Back hours".
// If type is 'regular' and day is 'workday', it's just 1.0x?
// Let's assume 'regular' means 1.0x (standard work).
if (type === 'regular') {
return hours * 1.0;
}
// Overtime Logic
return calculateOvertimeValue(hours, dayType);
}
return hours;
};
function getBaseOvertimeRate(dayType) {
if (dayType === 'rest_day_2') return 2.0;
return 1.5;
}
function calculateOvertimeValue(hours, dayType) {
// Second Day of Rest (Sunday) = All 2.0x
if (dayType === 'rest_day_2') {
return hours * 2.0;
}
// Workday, Rest Day 1, Holiday
// First 7.5h @ 1.5x
// Remaining @ 2.0x
if (hours <= 7.5) {
return hours * 1.5;
} else {
const firstPart = 7.5 * 1.5;
const secondPart = (hours - 7.5) * 2.0;
return firstPart + secondPart;
}
}
/**
* Returns the maximum allowed standby hours based on the day type.
* - Workday: 16 hours (24 - 8 regular work).
* - Rest Days / Holidays: 24 hours.
*/
export const getMaxStandbyHours = (dayType) => {
if (dayType === 'workday') return 16;
return 24;
};
/**
* Helper to check if a date is a Canadian Statutory Holiday.
* Covers fixed dates and major variable dates (Good Friday, Victoria Day, Labour Day, Thanksgiving, Easter Monday, Civic Holiday).
*/
export const isCanadianHoliday = (dateObj) => {
const year = dateObj.getFullYear();
const month = dateObj.getMonth(); // 0-indexed
const date = dateObj.getDate();
const day = dateObj.getDay(); // 0 = Sun, 1 = Mon, etc.
// --- Fixed Date Holidays ---
// New Year's Day (Jan 1)
if (month === 0 && date === 1) return true;
// Canada Day (Jul 1)
if (month === 6 && date === 1) return true;
// Remembrance Day (Nov 11)
if (month === 10 && date === 11) return true;
// Christmas Day (Dec 25)
if (month === 11 && date === 25) return true;
// Boxing Day (Dec 26)
if (month === 11 && date === 26) return true;
// --- Variable Date Holidays ---
// Good Friday & Easter Monday
// Easter calculation (Anonymous / Meeus/Jones/Butcher's algorithm)
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const easterMonth = Math.floor((h + l - 7 * m + 114) / 31) - 1; // 0-indexed
const easterDay = ((h + l - 7 * m + 114) % 31) + 1;
const easterDate = new Date(year, easterMonth, easterDay);
// Good Friday (2 days before Easter)
const goodFriday = new Date(easterDate);
goodFriday.setDate(easterDate.getDate() - 2);
if (month === goodFriday.getMonth() && date === goodFriday.getDate()) return true;
// Easter Monday (1 day after Easter) - specific to some agreements but often treated as holiday in fed gov
const easterMonday = new Date(easterDate);
easterMonday.setDate(easterDate.getDate() + 1);
if (month === easterMonday.getMonth() && date === easterMonday.getDate()) return true;
// Victoria Day (Money before May 25)
if (month === 4 && day === 1 && date >= 18 && date <= 24) return true;
// Canada Day Observed (if Jul 1 is Sun, then Jul 2 is holiday? - simplified, usually if falling on weekend)
// Checking only standard dates for now as per "statutory holidays".
// Civic Holiday (First Monday in August) - Not statutory everywhere but often observed
if (month === 7 && day === 1 && date <= 7) return true;
// Labour Day (First Monday in September)
if (month === 8 && day === 1 && date <= 7) return true;
// Thanksgiving (Second Monday in October)
if (month === 9 && day === 1 && date >= 8 && date <= 14) return true;
return false;
};
/**
* Helper to detect day type from a Date object (for auto-filling).
* Includes holiday detection.
*/
export const detectDayType = (dateObj) => {
if (isCanadianHoliday(dateObj)) return 'holiday';
const day = dateObj.getDay(); // 0 = Sun, 6 = Sat
if (day === 0) return 'rest_day_2'; // Sunday
if (day === 6) return 'rest_day_1'; // Saturday
return 'workday';
};
/**
* Generates a list of daily entries for a given range.
* Used for Bulk Standby Entry.
*/
export const generateStandbyEntries = (startDateStr, endDateStr) => {
const entries = [];
const [sy, sm, sd] = startDateStr.split('-').map(Number);
const [ey, em, ed] = endDateStr.split('-').map(Number);
// Use Noon UTC to avoid timezone rollover issues
const start = new Date(Date.UTC(sy, sm - 1, sd, 12, 0, 0));
const end = new Date(Date.UTC(ey, em - 1, ed, 12, 0, 0));
// Loop from start to end
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
// Create local date object for holiday checking (using getFullYear/getMonth/getDate)
// We used UTC above for iteration safety, but need local context or consistent context for day checks.
// detectDayType uses local methods.
// Let's create a new Date object from the UTC components to treat it as "Local Noon" for checking.
// Actually, detectDayType uses .getDay() which is local.
// If we want consistency, we should ensure we are checking the "intended" date.
// The simplistic approach:
const checkDate = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 12, 0, 0);
const dateStr = d.toISOString().split('T')[0];
const dayType = detectDayType(checkDate);
const maxHours = getMaxStandbyHours(dayType);
entries.push({
date: dateStr,
dayType: dayType,
duration: maxHours,
type: 'standby',
isBanked: true // Default to banked for standby? Usually yes.
});
}
return entries;
};