import { DayKey, DAY_KEYS, EXPENSE_TYPES, ExpenseRow, SHIFT_TYPES, ShiftRow, make_hours, ShiftsHours, ExpensesAmount } from "./timesheet.types"; import { isBetweenUTC, dayKeyFromDate, toTimeString, round2, toUTCDateOnly, endOfDayUTC, addDays } from "./timesheet.helpers"; import { WeekDto, ShiftDto, TimesheetPeriodDto, DayExpensesDto, ExpenseDto } from "../dtos/timesheet-period.dto"; import { getWeekStart, getWeekEnd, formatDateISO } from "src/common/utils/date-utils"; import { makeAmounts, makeEmptyWeek } from "./timesheet.mappers"; import { toDateString } from "src/modules/pay-periods/utils/pay-year.util"; import { MS_PER_HOUR } from "src/modules/shared/constants/date-time.constant"; export function computeWeekRange(week_offset = 0){ //sets current week Sunday -> Saturday const base = new Date(); const offset = new Date(base); offset.setDate(offset.getDate() + (week_offset * 7)); const start = getWeekStart(offset, 0); const end = getWeekEnd(start); const start_day = formatDateISO(start); const end_day = formatDateISO(end); const label = `${(start_day)}.${(end_day)}`; return { start, end, start_day, end_day, label } }; export function buildWeek( week_start: Date, week_end: Date, shifts: ShiftRow[], expenses: ExpenseRow[], ): WeekDto { const week = makeEmptyWeek(week_start); let all_approved = true; const day_times: Record> = DAY_KEYS.reduce((acc, key) => { acc[key] = []; return acc; }, {} as Record>); const day_hours: Record = DAY_KEYS.reduce((acc, key) => { acc[key] = make_hours(); return acc; }, {} as Record); const day_amounts: Record = DAY_KEYS.reduce((acc, key) => { acc[key] = makeAmounts(); return acc; }, {} as Record); const day_expense_rows: Record = DAY_KEYS.reduce((acc, key) => { acc[key] = { expenses: [{ type: '', amount: -1, mileage: -1, comment: '', is_approved: false, supervisor_comment: '', }], total_expense: -1, total_mileage: -1, }; return acc; }, {} as Record); //regroup hours per type of shifts const week_shifts = shifts.filter(shift => isBetweenUTC(shift.date, week_start, week_end)); for (const shift of week_shifts) { const key = dayKeyFromDate(shift.date, true); week.shifts[key].shifts.push({ date: toDateString(shift.date), type: shift.type, start_time: toTimeString(shift.start_time), end_time: toTimeString(shift.end_time), comment: shift.comment, is_approved: shift.is_approved ?? true, is_remote: shift.is_remote, } as ShiftDto); day_times[key].push({ start: shift.start_time, end: shift.end_time}); const duration = Math.max(0, (shift.end_time.getTime() - shift.start_time.getTime())/ MS_PER_HOUR); const type = (shift.type || '').toUpperCase(); if ( type === SHIFT_TYPES.REGULAR) day_hours[key].regular += duration; else if( type === SHIFT_TYPES.EVENING) day_hours[key].evening += duration; else if( type === SHIFT_TYPES.EMERGENCY) day_hours[key].emergency += duration; else if( type === SHIFT_TYPES.OVERTIME) day_hours[key].overtime += duration; else if( type === SHIFT_TYPES.SICK) day_hours[key].sick += duration; else if( type === SHIFT_TYPES.VACATION) day_hours[key].vacation += duration; else if( type === SHIFT_TYPES.HOLIDAY) day_hours[key].holiday += duration; all_approved = all_approved && (shift.is_approved ?? true ); } //regroupe amounts to type of expenses const week_expenses = expenses.filter(expense => isBetweenUTC(expense.date, week_start, week_end)); for (const expense of week_expenses) { const key = dayKeyFromDate(expense.date, true); const type = (expense.type || '').toUpperCase(); const row: ExpenseDto = { type, amount: round2(expense.amount ?? 0), mileage: round2(expense.mileage ?? 0), comment: expense.comment ?? '', is_approved: expense.is_approved ?? true, supervisor_comment: expense.supervisor_comment ?? '', }; day_expense_rows[key].expenses.push(row); if(type === EXPENSE_TYPES.MILEAGE) { day_amounts[key].mileage += row.mileage ?? 0; } else { day_amounts[key].expense += row.amount; } all_approved = all_approved && row.is_approved; } for (const key of DAY_KEYS) { //return exposed dto data week.shifts[key].regular_hours = round2(day_hours[key].regular); week.shifts[key].evening_hours = round2(day_hours[key].evening); week.shifts[key].overtime_hours = round2(day_hours[key].overtime); week.shifts[key].emergency_hours = round2(day_hours[key].emergency); //calculate gaps between shifts const times = day_times[key].sort((a,b) => a.start.getTime() - b.start.getTime()); let gaps = 0; for (let i = 1; i < times.length; i++) { const gap = (times[i].start.getTime() - times[i - 1].end.getTime()) / MS_PER_HOUR; if(gap > 0) gaps += gap; } week.shifts[key].break_durations = round2(gaps); //daily totals const totals = day_amounts[key]; day_expense_rows[key].total_mileage = round2(totals.mileage); day_expense_rows[key].total_expense = round2(totals.expense); } week.is_approved = all_approved; return week; } export function buildPeriod( period_start: Date, period_end: Date, shifts: ShiftRow[], expenses: ExpenseRow[], employeeFullName = '' ): TimesheetPeriodDto { const week1_start = toUTCDateOnly(period_start); const week1_end = endOfDayUTC(addDays(week1_start, 6)); const week2_start = toUTCDateOnly(addDays(week1_start, 7)); const week2_end = endOfDayUTC(period_end); const weeks: WeekDto[] = [ buildWeek(week1_start, week1_end, shifts, expenses), buildWeek(week2_start, week2_end, shifts, expenses), ]; return { weeks, employee_full_name: employeeFullName, }; }