Initial commit
All checks were successful
Docker Build and Publish / build-and-push (push) Successful in 2m17s
All checks were successful
Docker Build and Publish / build-and-push (push) Successful in 2m17s
This commit is contained in:
3
frontend/src/lib/pocketbase.js
Normal file
3
frontend/src/lib/pocketbase.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const pb = new PocketBase('http://127.0.0.1:8090');
|
||||
234
frontend/src/lib/time-rules.js
Normal file
234
frontend/src/lib/time-rules.js
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user