MEGAINFRA-WEBAPP/src/lib/workforce-hours.ts

89 lines
No EOL
3.2 KiB
TypeScript

export interface WeeklyTrendPoint {
/** YYYY-MM-DD, Monday of the week */
weekStart: string;
hours: number;
}
export const MS_PER_DAY = 86_400_000;
/** UTC midnight of the Monday that starts `now`'s ISO week. */
export function startOfIsoWeekUtc(now: Date): number {
const todayUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const dayOfWeek = new Date(todayUtc).getUTCDay(); // 0=Sun..6=Sat
const mondayOffset = (dayOfWeek + 6) % 7; // days since Monday
return todayUtc - mondayOffset * MS_PER_DAY;
}
/**
* Build a `weekCount`-week hours sparkline per employee, oldest first.
* Weeks with no entries show as 0.
*/
export function buildWeeklyHours(
timesheetsByEmployee: Map<string, { date: string; hours: number }[]>,
now: Date,
weekCount = 8,
): Map<string, number[]> {
const result = new Map<string, number[]>();
const todayUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const thisMonday = startOfIsoWeekUtc(now);
const startMonday = thisMonday - (weekCount - 1) * 7 * MS_PER_DAY;
for (const [empId, list] of timesheetsByEmployee) {
const weekly = new Array<number>(weekCount).fill(0);
for (const t of list) {
const d = new Date(t.date);
if (Number.isNaN(d.getTime())) continue;
const dayUtc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
if (dayUtc < startMonday || dayUtc > todayUtc + MS_PER_DAY) continue;
const idx = Math.floor((dayUtc - startMonday) / (7 * MS_PER_DAY));
if (idx >= 0 && idx < weekCount) weekly[idx] += t.hours;
}
result.set(empId, weekly);
}
return result;
}
/**
* Sum total hours for each of the last `weekCount` weeks across ALL
* employees, oldest first. Used for the workforce-wide trend chart.
*/
export function buildWeeklyTrend(
timesheets: { date: string; hours: number }[],
now: Date,
weekCount = 12,
): WeeklyTrendPoint[] {
const todayUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const thisMonday = startOfIsoWeekUtc(now);
const startMonday = thisMonday - (weekCount - 1) * 7 * MS_PER_DAY;
const buckets: WeeklyTrendPoint[] = [];
for (let i = 0; i < weekCount; i++) {
const weekStart = new Date(startMonday + i * 7 * MS_PER_DAY);
buckets.push({ weekStart: weekStart.toISOString().slice(0, 10), hours: 0 });
}
for (const t of timesheets) {
const d = new Date(t.date);
if (Number.isNaN(d.getTime())) continue;
const dayUtc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
if (dayUtc < startMonday || dayUtc > todayUtc + MS_PER_DAY) continue;
const idx = Math.floor((dayUtc - startMonday) / (7 * MS_PER_DAY));
if (idx >= 0 && idx < weekCount) buckets[idx].hours += t.hours;
}
return buckets;
}
/** Sum hours in the half-open window [startUtc, endUtc). */
export function sumHoursInWindow(
list: { date: string; hours: number }[],
startUtc: number,
endUtc: number,
): number {
let total = 0;
for (const t of list) {
const d = new Date(t.date);
if (Number.isNaN(d.getTime())) continue;
const dayUtc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
if (dayUtc >= startUtc && dayUtc < endUtc) total += t.hours;
}
return total;
}