89 lines
No EOL
3.2 KiB
TypeScript
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;
|
|
} |