iobroker.time-tracker 0.2.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/services/aggregation.ts"],
4
- "sourcesContent": ["/**\n * Aggregation service: keeps the day, month and year aggregates in sync with the punches.\n *\n * The daily figures come from the calculation service (a pure function); this service only supplies the\n * data (punches, employment profile, break rules, holidays, absences) and stores the result. Months and\n * years are always derived from the day rows, so a single day can be recalculated without leaving the\n * reports inconsistent.\n *\n * Days after \"today\" are not aggregated by default: future weekdays would otherwise add target time and\n * produce a phantom negative balance. Callers can pass `upToDate` (tests, imports) or `includeFuture`.\n */\n\nimport type { Db } from \"../db/database\";\nimport type { AbsencesRepository, AbsenceWithType } from \"../db/repositories/absences\";\nimport type { EntriesRepository } from \"../db/repositories/entries\";\nimport type { HolidaysRepository } from \"../db/repositories/holidays\";\nimport type { RulesRepository } from \"../db/repositories/rules\";\nimport type { SettingsRepository } from \"../db/repositories/settings\";\nimport type { UsersRepository, WorkProfileRecord } from \"../db/repositories/users\";\nimport type { WorkProfile } from \"../domain/target\";\nimport { ValidationError } from \"../errors\";\nimport { absenceDaysInRange, calculateDay, overtimeAfterPeriod } from \"../domain/calculation\";\nimport type { PauseMode } from \"../domain/breaks\";\nimport { toPunchEntries } from \"../db/repositories/entries\";\nimport { dateRange, daysInMonth, localDate as resolveLocalDate, utcToWallTime } from \"../util/time\";\n\n/** Daily aggregate as stored in `day_aggregates`. */\nexport interface DayAggregateRecord {\n\t/** Owner of the day */\n\tuserId: number;\n\t/** Local date, `YYYY-MM-DD` */\n\tlocalDate: string;\n\t/** Net working time in minutes */\n\tworkedMin: number;\n\t/** Deducted breaks in minutes */\n\tbreakMin: number;\n\t/** Part of the deducted break that is paid (in minutes) */\n\tpaidBreakMin: number;\n\t/** Target time in minutes */\n\ttargetMin: number;\n\t/** `workedMin - targetMin` */\n\tbalanceMin: number;\n\t/** Code of the absence covering the day, `null` when there is none */\n\tabsenceCode: string | null;\n\t/** True when the day is a public holiday */\n\tisHoliday: boolean;\n\t/** Instant of the first punch of the day */\n\tfirstInUtc: number | null;\n\t/** Instant of the last completed clock-out */\n\tlastOutUtc: number | null;\n\t/** True when the last punch has no counterpart yet */\n\thasOpenEntry: boolean;\n\t/** Instant of the last recalculation */\n\tupdatedAt: number;\n}\n\n/** Monthly aggregate as stored in `month_aggregates`. */\nexport interface MonthAggregateRecord {\n\t/** Owner of the month */\n\tuserId: number;\n\t/** Four digit year */\n\tyear: number;\n\t/** Month, 1 to 12 */\n\tmonth: number;\n\t/** Net working time in minutes */\n\tworkedMin: number;\n\t/** Target time in minutes */\n\ttargetMin: number;\n\t/** `workedMin - targetMin` */\n\tbalanceMin: number;\n\t/** Overtime carried at the end of the month (per model) */\n\tovertimeMin: number;\n\t/** Vacation days taken in the month */\n\tvacationUsed: number;\n\t/** Instant of the last recalculation */\n\tupdatedAt: number;\n}\n\n/** Yearly aggregate as stored in `year_aggregates`. */\nexport interface YearAggregateRecord {\n\t/** Owner of the year */\n\tuserId: number;\n\t/** Four digit year */\n\tyear: number;\n\t/** Net working time in minutes */\n\tworkedMin: number;\n\t/** Target time in minutes */\n\ttargetMin: number;\n\t/** Overtime carried at the end of the year */\n\tovertimeMin: number;\n\t/** Overtime taken over from the previous year */\n\tovertimeStart: number;\n\t/** Vacation entitlement including the carryover */\n\tvacationDays: number;\n\t/** Vacation days taken */\n\tvacationUsed: number;\n\t/** Remaining vacation days */\n\tvacationLeft: number;\n\t/** Vacation days approved but not yet taken */\n\tvacationPlannedDays: number;\n\t/** True when the year is closed */\n\tsettled: boolean;\n\t/** Instant of the last recalculation */\n\tupdatedAt: number;\n}\n\n/** Result of a range recalculation. */\nexport interface RangeResult {\n\t/** Number of recalculated days */\n\tdays: number;\n\t/** Number of days with an open punch */\n\topenDays: number;\n\t/** Net working time of the range in minutes */\n\tworkedMin: number;\n\t/** Pause of the range in minutes */\n\tbreakMin: number;\n\t/** Part of the pause that is paid (in minutes) */\n\tpaidBreakMin: number;\n\t/** Target time of the range in minutes */\n\ttargetMin: number;\n\t/** Balance of the range in minutes */\n\tbalanceMin: number;\n}\n\n/** Options shared by the recalculation methods. */\nexport interface RecalculateOptions {\n\t/** Local date up to which days are aggregated (default: today in the user's time zone) */\n\tupToDate?: string;\n\t/** True to aggregate days after `upToDate` as well */\n\tincludeFuture?: boolean;\n\t/** True to treat the year as complete for the `yearly` overtime model */\n\tsettled?: boolean;\n\t/** Instant of the recalculation, defaults to now */\n\tnow?: number;\n}\n\n/** Data sources of the aggregation service. */\nexport interface AggregationDeps {\n\t/** Open database handle */\n\tdb: Db;\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Punch storage */\n\tentries: EntriesRepository;\n\t/** Absence storage */\n\tabsences: AbsencesRepository;\n\t/** Holiday storage */\n\tholidays: HolidaysRepository;\n\t/** Break and surcharge rules */\n\trules: RulesRepository;\n\t/** Instance settings */\n\tsettings: SettingsRepository;\n}\n\n/** Aggregation operations. */\nexport interface AggregationService {\n\t/** Recalculates one day and stores the aggregate */\n\trecalculateDay(userId: number, localDate: string, options?: RecalculateOptions): DayAggregateRecord;\n\t/** Recalculates every day of a range */\n\trecalculateRange(userId: number, from: string, to: string, options?: RecalculateOptions): RangeResult;\n\t/** Recalculates a month from its days and stores the monthly aggregate */\n\trecalculateMonth(userId: number, year: number, month: number, options?: RecalculateOptions): MonthAggregateRecord;\n\t/** Recalculates a year from its months and stores the yearly aggregate */\n\trecalculateYear(userId: number, year: number, options?: RecalculateOptions): YearAggregateRecord;\n\t/** Stored day aggregate */\n\tday(userId: number, localDate: string): DayAggregateRecord | null;\n\t/** Stored day aggregates of a range */\n\tdays(userId: number, from: string, to: string): DayAggregateRecord[];\n\t/** Stored monthly aggregate */\n\tmonth(userId: number, year: number, month: number): MonthAggregateRecord | null;\n\t/** Stored yearly aggregate */\n\tyear(userId: number, year: number): YearAggregateRecord | null;\n\t/** Recomputes the local date/time cache of all punches of a user (after a time zone change) */\n\trecalculateLocalCaches(userId: number): number;\n}\n\ninterface DayRow {\n\tuser_id: number;\n\tlocal_date: string;\n\tworked_min: number;\n\tbreak_min: number;\n\tpaid_break_min: number;\n\ttarget_min: number;\n\tbalance_min: number;\n\tabsence_code: string | null;\n\tis_holiday: number;\n\tfirst_in_utc: number | null;\n\tlast_out_utc: number | null;\n\thas_open_entry: number;\n\tupdated_at: number;\n}\n\ninterface MonthRow {\n\tuser_id: number;\n\tyear: number;\n\tmonth: number;\n\tworked_min: number;\n\ttarget_min: number;\n\tbalance_min: number;\n\tovertime_min: number;\n\tvacation_used: number;\n\tupdated_at: number;\n}\n\ninterface YearRow {\n\tuser_id: number;\n\tyear: number;\n\tworked_min: number;\n\ttarget_min: number;\n\tovertime_min: number;\n\tovertime_start: number;\n\tvacation_days: number;\n\tvacation_used: number;\n\tvacation_left: number;\n\tvacation_planned_days: number;\n\tsettled: number;\n\tupdated_at: number;\n}\n\nconst DAY_COLUMNS = `user_id, local_date, worked_min, break_min, paid_break_min, target_min, balance_min, absence_code,\n\\tis_holiday, first_in_utc, last_out_utc, has_open_entry, updated_at`;\n\nconst MONTH_COLUMNS = `user_id, year, month, worked_min, target_min, balance_min, overtime_min, vacation_used,\n\\tupdated_at`;\n\nconst YEAR_COLUMNS = `user_id, year, worked_min, target_min, overtime_min, overtime_start, vacation_days,\n\\tvacation_used, vacation_left, vacation_planned_days, settled, updated_at`;\n\n/**\n * Employment defaults for users without stored profile: no target time and no carryovers.\n *\n * @param userId - owner of the profile\n * @returns profile with zero values\n */\nfunction defaultProfile(userId: number): WorkProfileRecord {\n\treturn {\n\t\tuserId,\n\t\tpercent: 0,\n\t\tweeklyHours: 0,\n\t\tworkdays: \"0;1;1;1;1;1;0\",\n\t\tstartDate: null,\n\t\tendDate: null,\n\t\tovertimeCarryover: 0,\n\t\tvorholzeitPerYear: 0,\n\t\tvacationCarryover: 0,\n\t\tvacationPerYear: 0,\n\t\tovertimeModel: \"monthly\",\n\t\tholidayFlags: null,\n\t\tpausePaidMinutes: 0,\n\t};\n}\n\n/**\n * Maps a database row to a day aggregate record.\n *\n * @param row - raw database row\n * @returns day aggregate\n */\nexport function mapDayRow(row: DayRow): DayAggregateRecord {\n\treturn {\n\t\tuserId: row.user_id,\n\t\tlocalDate: row.local_date,\n\t\tworkedMin: row.worked_min,\n\t\tbreakMin: row.break_min,\n\t\tpaidBreakMin: row.paid_break_min,\n\t\ttargetMin: row.target_min,\n\t\tbalanceMin: row.balance_min,\n\t\tabsenceCode: row.absence_code,\n\t\tisHoliday: row.is_holiday !== 0,\n\t\tfirstInUtc: row.first_in_utc,\n\t\tlastOutUtc: row.last_out_utc,\n\t\thasOpenEntry: row.has_open_entry !== 0,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n/**\n * Maps a database row to a monthly aggregate record.\n *\n * @param row - raw database row\n * @returns monthly aggregate\n */\nexport function mapMonthRow(row: MonthRow): MonthAggregateRecord {\n\treturn {\n\t\tuserId: row.user_id,\n\t\tyear: row.year,\n\t\tmonth: row.month,\n\t\tworkedMin: row.worked_min,\n\t\ttargetMin: row.target_min,\n\t\tbalanceMin: row.balance_min,\n\t\tovertimeMin: row.overtime_min,\n\t\tvacationUsed: row.vacation_used,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n/**\n * Maps a database row to a yearly aggregate record.\n *\n * @param row - raw database row\n * @returns yearly aggregate\n */\nexport function mapYearRow(row: YearRow): YearAggregateRecord {\n\treturn {\n\t\tuserId: row.user_id,\n\t\tyear: row.year,\n\t\tworkedMin: row.worked_min,\n\t\ttargetMin: row.target_min,\n\t\tovertimeMin: row.overtime_min,\n\t\tovertimeStart: row.overtime_start,\n\t\tvacationDays: row.vacation_days,\n\t\tvacationUsed: row.vacation_used,\n\t\tvacationLeft: row.vacation_left,\n\t\tvacationPlannedDays: row.vacation_planned_days,\n\t\tsettled: row.settled !== 0,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n/**\n * Creates the aggregation service.\n *\n * @param deps - data sources\n * @returns service instance\n */\nexport function createAggregationService(deps: AggregationDeps): AggregationService {\n\tconst { db, users, entries, absences, holidays, rules, settings } = deps;\n\n\tconst selectDay = db.prepare(`SELECT ${DAY_COLUMNS} FROM day_aggregates WHERE user_id = ? AND local_date = ?`);\n\tconst selectDays = db.prepare(\n\t\t`SELECT ${DAY_COLUMNS} FROM day_aggregates\n\t\t WHERE user_id = ? AND local_date >= ? AND local_date <= ?\n\t\t ORDER BY local_date`,\n\t);\n\tconst upsertDay = db.prepare(\n\t\t`INSERT INTO day_aggregates (${DAY_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t ON CONFLICT(user_id, local_date) DO UPDATE SET\n\t\t worked_min = excluded.worked_min,\n\t\t break_min = excluded.break_min,\n\t\t paid_break_min = excluded.paid_break_min,\n\t\t target_min = excluded.target_min,\n\t\t balance_min = excluded.balance_min,\n\t\t absence_code = excluded.absence_code,\n\t\t is_holiday = excluded.is_holiday,\n\t\t first_in_utc = excluded.first_in_utc,\n\t\t last_out_utc = excluded.last_out_utc,\n\t\t has_open_entry = excluded.has_open_entry,\n\t\t updated_at = excluded.updated_at`,\n\t);\n\tconst selectMonth = db.prepare(\n\t\t`SELECT ${MONTH_COLUMNS} FROM month_aggregates WHERE user_id = ? AND year = ? AND month = ?`,\n\t);\n\tconst upsertMonth = db.prepare(\n\t\t`INSERT INTO month_aggregates (${MONTH_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t ON CONFLICT(user_id, year, month) DO UPDATE SET\n\t\t worked_min = excluded.worked_min,\n\t\t target_min = excluded.target_min,\n\t\t balance_min = excluded.balance_min,\n\t\t overtime_min = excluded.overtime_min,\n\t\t vacation_used = excluded.vacation_used,\n\t\t updated_at = excluded.updated_at`,\n\t);\n\tconst selectYear = db.prepare(`SELECT ${YEAR_COLUMNS} FROM year_aggregates WHERE user_id = ? AND year = ?`);\n\tconst upsertYear = db.prepare(\n\t\t`INSERT INTO year_aggregates (${YEAR_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t ON CONFLICT(user_id, year) DO UPDATE SET\n\t\t worked_min = excluded.worked_min,\n\t\t target_min = excluded.target_min,\n\t\t overtime_min = excluded.overtime_min,\n\t\t overtime_start = excluded.overtime_start,\n\t\t vacation_days = excluded.vacation_days,\n\t\t vacation_used = excluded.vacation_used,\n\t\t vacation_left = excluded.vacation_left,\n\t\t vacation_planned_days = excluded.vacation_planned_days,\n\t\t settled = excluded.settled,\n\t\t updated_at = excluded.updated_at`,\n\t);\n\tconst selectPayouts = db.prepare(\n\t\t\"SELECT COALESCE(SUM(minutes), 0) AS minutes FROM payouts WHERE user_id = ? AND year = ?\",\n\t);\n\tconst selectEntryCache = db.prepare(\"SELECT id, ts_utc AS tsUtc FROM time_entries WHERE user_id = ?\");\n\tconst updateEntryCache = db.prepare(\"UPDATE time_entries SET ts_local = ?, local_date = ? WHERE id = ?\");\n\n\tconst timeZoneOf = (userId: number): string => users.findById(userId)?.timezone ?? \"UTC\";\n\tconst holidayRegion = (): string => settings.get(\"holiday_country\") ?? \"DE\";\n\n\t/**\n\t * How the pause of a day is determined.\n\t *\n\t * A stored value that is not one of the three modes falls back to the default, so a hand edited setting can\n\t * never spoil a calculation.\n\t *\n\t * @returns the configured pause mode\n\t */\n\tconst pauseMode = (): PauseMode => {\n\t\tconst stored = settings.get(\"pause_mode\");\n\t\treturn stored === \"punched\" || stored === \"staffel\" ? stored : \"auto\";\n\t};\n\n\t/**\n\t * Reads the stored profile or the defaults (no target time, no carryovers).\n\t *\n\t * @param userId - owner of the profile\n\t * @returns employment parameters\n\t */\n\tconst profileOf = (userId: number): WorkProfileRecord => users.getWorkProfile(userId) ?? defaultProfile(userId);\n\n\t/**\n\t * Picks the absence of a day: the one crediting the largest part of the day wins, ties are broken by id.\n\t *\n\t * @param userId - owner of the absence\n\t * @param date - local date\n\t * @returns the covering absence or `null`\n\t */\n\tconst absenceFor = (userId: number, date: string): AbsenceWithType | null => {\n\t\tconst covering = absences.withTypesInRange(userId, date, date);\n\t\tlet best: AbsenceWithType | null = null;\n\t\tfor (const absence of covering) {\n\t\t\tconst share = (Math.max(0, Math.min(100, absence.factor)) / 100) * absence.dayPortion;\n\t\t\tconst bestShare = best ? (Math.max(0, Math.min(100, best.factor)) / 100) * best.dayPortion : -1;\n\t\t\tif (share > bestShare || (share === bestShare && best !== null && absence.id < best.id)) {\n\t\t\t\tbest = absence;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t};\n\n\t/**\n\t * Sum of the payouts of a year.\n\t *\n\t * @param userId - owner of the payouts\n\t * @param year - four digit year\n\t * @returns paid out minutes\n\t */\n\tconst payoutsOf = (userId: number, year: number): number =>\n\t\t(selectPayouts.get(userId, year) as { minutes: number }).minutes;\n\n\t/**\n\t * Counts the months of a year that lie inside the employment period.\n\t *\n\t * @param profile - employment parameters\n\t * @param year - four digit year\n\t * @param upToMonth - last month to count (1 to 12)\n\t * @param timeZone - time zone used to resolve the employment dates\n\t * @returns number of active months\n\t */\n\tconst activeMonthsInYear = (profile: WorkProfile, year: number, upToMonth: number, timeZone: string): number => {\n\t\tconst startDate = profile.startDate == null ? null : resolveLocalDate(profile.startDate, timeZone);\n\t\tconst endDate = profile.endDate == null ? null : resolveLocalDate(profile.endDate, timeZone);\n\t\tconst limit = Math.max(0, Math.min(12, upToMonth));\n\t\tlet months = 0;\n\n\t\tfor (let month = 1; month <= limit; month++) {\n\t\t\tconst key = `${year}-${String(month).padStart(2, \"0\")}`;\n\t\t\tconst first = `${key}-01`;\n\t\t\tconst last = `${key}-${String(daysInMonth(year, month)).padStart(2, \"0\")}`;\n\t\t\tif ((startDate === null || last >= startDate) && (endDate === null || first <= endDate)) {\n\t\t\t\tmonths++;\n\t\t\t}\n\t\t}\n\t\treturn months;\n\t};\n\n\t/**\n\t * Overtime taken over at the beginning of a year: the previous year's result or the profile carryover.\n\t *\n\t * @param userId - owner\n\t * @param year - four digit year\n\t * @param carryover - carryover of the profile (used for the first year)\n\t * @returns overtime in minutes\n\t */\n\tconst overtimeStartFor = (userId: number, year: number, carryover: number): number => {\n\t\tconst previous = selectYear.get(userId, year - 1) as YearRow | undefined;\n\t\treturn previous ? previous.overtime_min : carryover;\n\t};\n\n\t/**\n\t * Overtime carried after a month, following the model of the profile.\n\t *\n\t * @param args - month, balances, profile and flags\n\t * @param args.userId - owner\n\t * @param args.year - four digit year\n\t * @param args.month - month, 1 to 12\n\t * @param args.monthBalance - freshly calculated balance of that month (`null` to read it from the database)\n\t * @param args.profile - employment parameters\n\t * @param args.timeZone - time zone of the employee\n\t * @param args.completeYear - true when the calendar year is finished\n\t * @returns carried overtime in minutes\n\t */\n\tconst overtimeAfterMonth = (args: {\n\t\tuserId: number;\n\t\tyear: number;\n\t\tmonth: number;\n\t\tmonthBalance: number | null;\n\t\tprofile: WorkProfileRecord;\n\t\ttimeZone: string;\n\t\tcompleteYear: boolean;\n\t}): number => {\n\t\tconst balances: number[] = [];\n\t\tfor (let index = 1; index <= args.month; index++) {\n\t\t\tif (index === args.month && args.monthBalance !== null) {\n\t\t\t\tbalances.push(args.monthBalance);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst row = selectMonth.get(args.userId, args.year, index) as MonthRow | undefined;\n\t\t\tbalances.push(row?.balance_min ?? 0);\n\t\t}\n\n\t\treturn overtimeAfterPeriod({\n\t\t\tmodel: args.profile.overtimeModel,\n\t\t\tmonthBalances: balances,\n\t\t\tovertimeStartMinutes: overtimeStartFor(args.userId, args.year, args.profile.overtimeCarryover),\n\t\t\tvorholzeitPerYearMinutes: args.profile.vorholzeitPerYear,\n\t\t\tactiveMonths: activeMonthsInYear(args.profile, args.year, args.month, args.timeZone),\n\t\t\tpayoutMinutes: payoutsOf(args.userId, args.year),\n\t\t\tcompleteYear: args.completeYear,\n\t\t}).overtimeMinutes;\n\t};\n\n\t/**\n\t * Vacation days of one kind inside a month or year.\n\t *\n\t * @param args - user, period and status\n\t * @param args.userId - owner\n\t * @param args.from - first local date\n\t * @param args.to - last local date\n\t * @param args.status - `taken` for used days, `planned` for approved days\n\t * @param args.profile - employment parameters (working days)\n\t * @param args.timeZone - time zone of the employee\n\t * @returns vacation days, rounded to two decimals\n\t */\n\tconst vacationDaysInRange = (args: {\n\t\tuserId: number;\n\t\tfrom: string;\n\t\tto: string;\n\t\tstatus: \"taken\" | \"planned\";\n\t\tprofile: WorkProfileRecord;\n\t\ttimeZone: string;\n\t}): number => {\n\t\tconst year = Number(args.to.slice(0, 4));\n\t\tconst holidayDates = holidays.dateSet(year, holidayRegion());\n\t\tconst overlapping = absences.withTypesInRange(args.userId, args.from, args.to);\n\n\t\tlet days = 0;\n\t\tfor (const absence of overlapping) {\n\t\t\tif (!absence.reduceVacation || absence.status !== args.status) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst from = absence.dateFrom < args.from ? args.from : absence.dateFrom;\n\t\t\tconst to = absence.dateTo > args.to ? args.to : absence.dateTo;\n\t\t\tif (from > to) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdays += absenceDaysInRange({\n\t\t\t\tfrom,\n\t\t\t\tto,\n\t\t\t\tworkdays: args.profile.workdays,\n\t\t\t\ttimeZone: args.timeZone,\n\t\t\t\tholidays: holidayDates,\n\t\t\t\tdayPortion: absence.dayPortion,\n\t\t\t});\n\t\t}\n\t\treturn Math.round(days * 100) / 100;\n\t};\n\n\t/**\n\t * True when a calendar year is finished (needed by the `yearly` overtime model).\n\t *\n\t * @param year - four digit year\n\t * @param todayDate - local date of the user\n\t * @param settled - explicit flag of the caller\n\t * @returns true when the year is closed\n\t */\n\tconst isCompleteYear = (year: number, todayDate: string, settled?: boolean): boolean =>\n\t\tsettled === true || Number(todayDate.slice(0, 4)) > year;\n\n\t/**\n\t * Calculates one day from the stored data (profile, rules, holidays, absences, punches).\n\t *\n\t * @param userId - owner\n\t * @param localDate - local date to calculate\n\t * @param now - instant of the recalculation\n\t * @returns the calculated day\n\t */\n\tconst calculateDayFor = (userId: number, localDate: string, now: number): DayAggregateRecord => {\n\t\tconst timeZone = timeZoneOf(userId);\n\t\tconst absence = absenceFor(userId, localDate);\n\t\tconst isHoliday = holidays.isHoliday(localDate, holidayRegion());\n\t\tconst profile = profileOf(userId);\n\t\tconst day = calculateDay({\n\t\t\tprofile,\n\t\t\tentries: toPunchEntries(entries.listByDate(userId, localDate)),\n\t\t\tpauseRules: rules.pauseRules(userId),\n\t\t\tpauseMode: pauseMode(),\n\t\t\tpausePaidMinutes: profile.pausePaidMinutes,\n\t\t\ttimeZone,\n\t\t\tlocalDate,\n\t\t\tisHoliday,\n\t\t\tabsence: absence ? { factor: absence.factor, dayPortion: absence.dayPortion } : null,\n\t\t});\n\n\t\treturn {\n\t\t\tuserId,\n\t\t\tlocalDate,\n\t\t\tworkedMin: day.workedMinutes,\n\t\t\tbreakMin: day.breakMinutes,\n\t\t\tpaidBreakMin: day.paidPauseMinutes,\n\t\t\ttargetMin: day.targetMinutes,\n\t\t\tbalanceMin: day.balanceMinutes,\n\t\t\tabsenceCode: absence?.typeCode ?? null,\n\t\t\tisHoliday,\n\t\t\tfirstInUtc: day.firstInUtc,\n\t\t\tlastOutUtc: day.lastOutUtc,\n\t\t\thasOpenEntry: day.hasOpenEntry,\n\t\t\tupdatedAt: now,\n\t\t};\n\t};\n\n\treturn {\n\t\trecalculateDay(userId: number, localDate: string, options?: RecalculateOptions): DayAggregateRecord {\n\t\t\tconst now = options?.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst record = calculateDayFor(userId, localDate, now);\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupsertDay.run(\n\t\t\t\t\trecord.userId,\n\t\t\t\t\trecord.localDate,\n\t\t\t\t\trecord.workedMin,\n\t\t\t\t\trecord.breakMin,\n\t\t\t\t\trecord.paidBreakMin,\n\t\t\t\t\trecord.targetMin,\n\t\t\t\t\trecord.balanceMin,\n\t\t\t\t\trecord.absenceCode,\n\t\t\t\t\trecord.isHoliday ? 1 : 0,\n\t\t\t\t\trecord.firstInUtc,\n\t\t\t\t\trecord.lastOutUtc,\n\t\t\t\t\trecord.hasOpenEntry ? 1 : 0,\n\t\t\t\t\trecord.updatedAt,\n\t\t\t\t);\n\t\t\t});\n\t\t\trun();\n\t\t\treturn record;\n\t\t},\n\n\t\trecalculateRange(userId: number, from: string, to: string, options?: RecalculateOptions): RangeResult {\n\t\t\tconst totals: RangeResult = {\n\t\t\t\tdays: 0,\n\t\t\t\topenDays: 0,\n\t\t\t\tworkedMin: 0,\n\t\t\t\tbreakMin: 0,\n\t\t\t\tpaidBreakMin: 0,\n\t\t\t\ttargetMin: 0,\n\t\t\t\tbalanceMin: 0,\n\t\t\t};\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tfor (const date of dateRange(from, to)) {\n\t\t\t\t\tconst record = this.recalculateDay(userId, date, options);\n\t\t\t\t\ttotals.days++;\n\t\t\t\t\ttotals.workedMin += record.workedMin;\n\t\t\t\t\ttotals.breakMin += record.breakMin;\n\t\t\t\t\ttotals.paidBreakMin += record.paidBreakMin;\n\t\t\t\t\ttotals.targetMin += record.targetMin;\n\t\t\t\t\ttotals.balanceMin += record.balanceMin;\n\t\t\t\t\ttotals.openDays += record.hasOpenEntry ? 1 : 0;\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\t\t\treturn totals;\n\t\t},\n\n\t\trecalculateMonth(\n\t\t\tuserId: number,\n\t\t\tyear: number,\n\t\t\tmonth: number,\n\t\t\toptions?: RecalculateOptions,\n\t\t): MonthAggregateRecord {\n\t\t\tif (!Number.isInteger(month) || month < 1 || month > 12) {\n\t\t\t\tthrow new ValidationError(`month must be between 1 and 12 (got ${month})`);\n\t\t\t}\n\n\t\t\tconst now = options?.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst timeZone = timeZoneOf(userId);\n\t\t\tconst profile = profileOf(userId);\n\t\t\tconst upToDate = options?.upToDate ?? resolveLocalDate(now, timeZone);\n\t\t\tconst key = `${year}-${String(month).padStart(2, \"0\")}`;\n\t\t\tconst firstDay = `${key}-01`;\n\t\t\tconst lastDay = `${key}-${String(daysInMonth(year, month)).padStart(2, \"0\")}`;\n\t\t\t// days after `upToDate` are skipped unless the caller asks for them\n\t\t\tconst endDate = options?.includeFuture === true || lastDay <= upToDate ? lastDay : upToDate;\n\n\t\t\tlet workedMin = 0;\n\t\t\tlet targetMin = 0;\n\t\t\tlet balanceMin = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tfor (const date of dateRange(firstDay, endDate)) {\n\t\t\t\t\tconst record = this.recalculateDay(userId, date, options);\n\t\t\t\t\tworkedMin += record.workedMin;\n\t\t\t\t\ttargetMin += record.targetMin;\n\t\t\t\t\tbalanceMin += record.balanceMin;\n\t\t\t\t}\n\n\t\t\t\tconst vacationUsed = vacationDaysInRange({\n\t\t\t\t\tuserId,\n\t\t\t\t\tfrom: firstDay,\n\t\t\t\t\tto: lastDay,\n\t\t\t\t\tstatus: \"taken\",\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t});\n\t\t\t\tconst overtimeMin = overtimeAfterMonth({\n\t\t\t\t\tuserId,\n\t\t\t\t\tyear,\n\t\t\t\t\tmonth,\n\t\t\t\t\tmonthBalance: balanceMin,\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t\tcompleteYear: isCompleteYear(year, upToDate, options?.settled),\n\t\t\t\t});\n\n\t\t\t\tupsertMonth.run(userId, year, month, workedMin, targetMin, balanceMin, overtimeMin, vacationUsed, now);\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = selectMonth.get(userId, year, month) as MonthRow | undefined;\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`month aggregate ${key} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn mapMonthRow(saved);\n\t\t},\n\n\t\trecalculateYear(userId: number, year: number, options?: RecalculateOptions): YearAggregateRecord {\n\t\t\tconst now = options?.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst timeZone = timeZoneOf(userId);\n\t\t\tconst profile = profileOf(userId);\n\t\t\tconst upToDate = options?.upToDate ?? resolveLocalDate(now, timeZone);\n\t\t\tconst settled = isCompleteYear(year, upToDate, options?.settled);\n\t\t\tconst yearStart = `${year}-01-01`;\n\t\t\tconst yearEnd = `${year}-12-31`;\n\n\t\t\tlet workedMin = 0;\n\t\t\tlet targetMin = 0;\n\t\t\tlet overtimeMin = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tfor (let month = 1; month <= 12; month++) {\n\t\t\t\t\tconst row = this.recalculateMonth(userId, year, month, options);\n\t\t\t\t\tworkedMin += row.workedMin;\n\t\t\t\t\ttargetMin += row.targetMin;\n\t\t\t\t}\n\t\t\t\t// the carried overtime after December is the result of the year\n\t\t\t\tovertimeMin = overtimeAfterMonth({\n\t\t\t\t\tuserId,\n\t\t\t\t\tyear,\n\t\t\t\t\tmonth: 12,\n\t\t\t\t\tmonthBalance: null,\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t\tcompleteYear: settled,\n\t\t\t\t});\n\n\t\t\t\tconst vacationDays = profile.vacationCarryover + profile.vacationPerYear;\n\t\t\t\tconst vacationUsed = vacationDaysInRange({\n\t\t\t\t\tuserId,\n\t\t\t\t\tfrom: yearStart,\n\t\t\t\t\tto: yearEnd,\n\t\t\t\t\tstatus: \"taken\",\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t});\n\t\t\t\tconst vacationPlanned = vacationDaysInRange({\n\t\t\t\t\tuserId,\n\t\t\t\t\tfrom: yearStart,\n\t\t\t\t\tto: yearEnd,\n\t\t\t\t\tstatus: \"planned\",\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t});\n\n\t\t\t\tupsertYear.run(\n\t\t\t\t\tuserId,\n\t\t\t\t\tyear,\n\t\t\t\t\tworkedMin,\n\t\t\t\t\ttargetMin,\n\t\t\t\t\tovertimeMin,\n\t\t\t\t\tovertimeStartFor(userId, year, profile.overtimeCarryover),\n\t\t\t\t\tMath.round(vacationDays * 100) / 100,\n\t\t\t\t\tvacationUsed,\n\t\t\t\t\tMath.round((vacationDays - vacationUsed) * 100) / 100,\n\t\t\t\t\tvacationPlanned,\n\t\t\t\t\tsettled ? 1 : 0,\n\t\t\t\t\tnow,\n\t\t\t\t);\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = selectYear.get(userId, year) as YearRow | undefined;\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`year aggregate ${year} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn mapYearRow(saved);\n\t\t},\n\n\t\tday(userId: number, localDate: string): DayAggregateRecord | null {\n\t\t\tconst row = selectDay.get(userId, localDate) as DayRow | undefined;\n\t\t\treturn row ? mapDayRow(row) : null;\n\t\t},\n\n\t\tdays(userId: number, from: string, to: string): DayAggregateRecord[] {\n\t\t\treturn (selectDays.all(userId, from, to) as DayRow[]).map(mapDayRow);\n\t\t},\n\n\t\tmonth(userId: number, year: number, month: number): MonthAggregateRecord | null {\n\t\t\tconst row = selectMonth.get(userId, year, month) as MonthRow | undefined;\n\t\t\treturn row ? mapMonthRow(row) : null;\n\t\t},\n\n\t\tyear(userId: number, year: number): YearAggregateRecord | null {\n\t\t\tconst row = selectYear.get(userId, year) as YearRow | undefined;\n\t\t\treturn row ? mapYearRow(row) : null;\n\t\t},\n\n\t\trecalculateLocalCaches(userId: number): number {\n\t\t\tconst timeZone = timeZoneOf(userId);\n\t\t\tconst rows = selectEntryCache.all(userId) as { id: number; tsUtc: number }[];\n\t\t\tlet changed = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tfor (const row of rows) {\n\t\t\t\t\tconst tsLocal = utcToWallTime(row.tsUtc, timeZone);\n\t\t\t\t\tconst date = resolveLocalDate(row.tsUtc, timeZone);\n\t\t\t\t\tif (cacheMatches(db, row.id, tsLocal, date)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tupdateEntryCache.run(tsLocal, date, row.id);\n\t\t\t\t\tchanged++;\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\t\t\treturn changed;\n\t\t},\n\t};\n}\n\n/**\n * Checks whether the local cache fields of a punch already match.\n *\n * @param db - open database handle\n * @param entryId - punch id\n * @param tsLocal - expected wall clock time\n * @param localDate - expected local date\n * @returns true when both values are already stored\n */\nfunction cacheMatches(db: Db, entryId: number, tsLocal: number, localDate: string): boolean {\n\tconst row = db\n\t\t.prepare(\"SELECT ts_local AS tsLocal, local_date AS localDate FROM time_entries WHERE id = ?\")\n\t\t.get(entryId) as { tsLocal: number; localDate: string } | undefined;\n\treturn row !== undefined && row.tsLocal === tsLocal && row.localDate === localDate;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Aggregation service: keeps the day, month and year aggregates in sync with the punches.\n *\n * The daily figures come from the calculation service (a pure function); this service only supplies the\n * data (punches, employment profile, break rules, holidays, absences) and stores the result. Months and\n * years are always derived from the day rows, so a single day can be recalculated without leaving the\n * reports inconsistent.\n *\n * Days after \"today\" are not aggregated by default: future weekdays would otherwise add target time and\n * produce a phantom negative balance. Callers can pass `upToDate` (tests, imports) or `includeFuture`.\n */\n\nimport type { Db } from \"../db/database\";\nimport type { AbsencesRepository, AbsenceWithType } from \"../db/repositories/absences\";\nimport type { EntriesRepository } from \"../db/repositories/entries\";\nimport type { HolidaysRepository } from \"../db/repositories/holidays\";\nimport type { RulesRepository } from \"../db/repositories/rules\";\nimport type { SettingsRepository } from \"../db/repositories/settings\";\nimport type { UsersRepository, WorkProfileRecord } from \"../db/repositories/users\";\nimport type { WorkProfile } from \"../domain/target\";\nimport { ValidationError } from \"../errors\";\nimport { absenceDaysInRange, calculateDay, overtimeAfterPeriod } from \"../domain/calculation\";\nimport type { PauseMode } from \"../domain/breaks\";\nimport { toPunchEntries } from \"../db/repositories/entries\";\nimport { dateRange, daysInMonth, localDate as resolveLocalDate, utcToWallTime } from \"../util/time\";\n\n/** Daily aggregate as stored in `day_aggregates`. */\nexport interface DayAggregateRecord {\n\t/** Owner of the day */\n\tuserId: number;\n\t/** Local date, `YYYY-MM-DD` */\n\tlocalDate: string;\n\t/** Net working time in minutes */\n\tworkedMin: number;\n\t/** Deducted breaks in minutes */\n\tbreakMin: number;\n\t/** Part of the deducted break that is paid (in minutes) */\n\tpaidBreakMin: number;\n\t/** Target time in minutes */\n\ttargetMin: number;\n\t/** `workedMin - targetMin` */\n\tbalanceMin: number;\n\t/** Code of the absence covering the day, `null` when there is none */\n\tabsenceCode: string | null;\n\t/** True when the day is a public holiday */\n\tisHoliday: boolean;\n\t/** Instant of the first punch of the day */\n\tfirstInUtc: number | null;\n\t/** Instant of the last completed clock-out */\n\tlastOutUtc: number | null;\n\t/** True when the last punch has no counterpart yet */\n\thasOpenEntry: boolean;\n\t/** Instant of the last recalculation */\n\tupdatedAt: number;\n}\n\n/** Monthly aggregate as stored in `month_aggregates`. */\nexport interface MonthAggregateRecord {\n\t/** Owner of the month */\n\tuserId: number;\n\t/** Four digit year */\n\tyear: number;\n\t/** Month, 1 to 12 */\n\tmonth: number;\n\t/** Net working time in minutes */\n\tworkedMin: number;\n\t/** Target time in minutes */\n\ttargetMin: number;\n\t/** `workedMin - targetMin` */\n\tbalanceMin: number;\n\t/** Overtime carried at the end of the month (per model) */\n\tovertimeMin: number;\n\t/** Vacation days taken in the month */\n\tvacationUsed: number;\n\t/** Instant of the last recalculation */\n\tupdatedAt: number;\n}\n\n/** Yearly aggregate as stored in `year_aggregates`. */\nexport interface YearAggregateRecord {\n\t/** Owner of the year */\n\tuserId: number;\n\t/** Four digit year */\n\tyear: number;\n\t/** Net working time in minutes */\n\tworkedMin: number;\n\t/** Target time in minutes */\n\ttargetMin: number;\n\t/** Overtime carried at the end of the year */\n\tovertimeMin: number;\n\t/** Overtime taken over from the previous year */\n\tovertimeStart: number;\n\t/** Vacation entitlement including the carryover */\n\tvacationDays: number;\n\t/** Vacation days taken */\n\tvacationUsed: number;\n\t/** Remaining vacation days */\n\tvacationLeft: number;\n\t/** Vacation days approved but not yet taken */\n\tvacationPlannedDays: number;\n\t/** True when the year is closed */\n\tsettled: boolean;\n\t/** Instant of the last recalculation */\n\tupdatedAt: number;\n}\n\n/** Result of a range recalculation. */\nexport interface RangeResult {\n\t/** Number of recalculated days */\n\tdays: number;\n\t/** Number of days with an open punch */\n\topenDays: number;\n\t/** Net working time of the range in minutes */\n\tworkedMin: number;\n\t/** Pause of the range in minutes */\n\tbreakMin: number;\n\t/** Part of the pause that is paid (in minutes) */\n\tpaidBreakMin: number;\n\t/** Target time of the range in minutes */\n\ttargetMin: number;\n\t/** Balance of the range in minutes */\n\tbalanceMin: number;\n}\n\n/** Options shared by the recalculation methods. */\nexport interface RecalculateOptions {\n\t/** Local date up to which days are aggregated (default: today in the user's time zone) */\n\tupToDate?: string;\n\t/** True to aggregate days after `upToDate` as well */\n\tincludeFuture?: boolean;\n\t/** True to treat the year as complete for the `yearly` overtime model */\n\tsettled?: boolean;\n\t/** Instant of the recalculation, defaults to now */\n\tnow?: number;\n}\n\n/** Data sources of the aggregation service. */\nexport interface AggregationDeps {\n\t/** Open database handle */\n\tdb: Db;\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Punch storage */\n\tentries: EntriesRepository;\n\t/** Absence storage */\n\tabsences: AbsencesRepository;\n\t/** Holiday storage */\n\tholidays: HolidaysRepository;\n\t/** Break and surcharge rules */\n\trules: RulesRepository;\n\t/** Instance settings */\n\tsettings: SettingsRepository;\n}\n\n/** Aggregation operations. */\nexport interface AggregationService {\n\t/** Recalculates one day and stores the aggregate */\n\trecalculateDay(userId: number, localDate: string, options?: RecalculateOptions): DayAggregateRecord;\n\t/** Recalculates every day of a range */\n\trecalculateRange(userId: number, from: string, to: string, options?: RecalculateOptions): RangeResult;\n\t/** Recalculates a month from its days and stores the monthly aggregate */\n\trecalculateMonth(userId: number, year: number, month: number, options?: RecalculateOptions): MonthAggregateRecord;\n\t/** Recalculates a year from its months and stores the yearly aggregate */\n\trecalculateYear(userId: number, year: number, options?: RecalculateOptions): YearAggregateRecord;\n\t/** Stored day aggregate */\n\tday(userId: number, localDate: string): DayAggregateRecord | null;\n\t/** Stored day aggregates of a range */\n\tdays(userId: number, from: string, to: string): DayAggregateRecord[];\n\t/** Stored monthly aggregate */\n\tmonth(userId: number, year: number, month: number): MonthAggregateRecord | null;\n\t/** Stored yearly aggregate */\n\tyear(userId: number, year: number): YearAggregateRecord | null;\n\t/** Recomputes the local date/time cache of all punches of a user (after a time zone change) */\n\trecalculateLocalCaches(userId: number): number;\n}\n\ninterface DayRow {\n\tuser_id: number;\n\tlocal_date: string;\n\tworked_min: number;\n\tbreak_min: number;\n\tpaid_break_min: number;\n\ttarget_min: number;\n\tbalance_min: number;\n\tabsence_code: string | null;\n\tis_holiday: number;\n\tfirst_in_utc: number | null;\n\tlast_out_utc: number | null;\n\thas_open_entry: number;\n\tupdated_at: number;\n}\n\ninterface MonthRow {\n\tuser_id: number;\n\tyear: number;\n\tmonth: number;\n\tworked_min: number;\n\ttarget_min: number;\n\tbalance_min: number;\n\tovertime_min: number;\n\tvacation_used: number;\n\tupdated_at: number;\n}\n\ninterface YearRow {\n\tuser_id: number;\n\tyear: number;\n\tworked_min: number;\n\ttarget_min: number;\n\tovertime_min: number;\n\tovertime_start: number;\n\tvacation_days: number;\n\tvacation_used: number;\n\tvacation_left: number;\n\tvacation_planned_days: number;\n\tsettled: number;\n\tupdated_at: number;\n}\n\nconst DAY_COLUMNS = `user_id, local_date, worked_min, break_min, paid_break_min, target_min, balance_min, absence_code,\n\\tis_holiday, first_in_utc, last_out_utc, has_open_entry, updated_at`;\n\nconst MONTH_COLUMNS = `user_id, year, month, worked_min, target_min, balance_min, overtime_min, vacation_used,\n\\tupdated_at`;\n\nconst YEAR_COLUMNS = `user_id, year, worked_min, target_min, overtime_min, overtime_start, vacation_days,\n\\tvacation_used, vacation_left, vacation_planned_days, settled, updated_at`;\n\n/**\n * Employment defaults for users without stored profile: no target time and no carryovers.\n *\n * @param userId - owner of the profile\n * @returns profile with zero values\n */\nfunction defaultProfile(userId: number): WorkProfileRecord {\n\treturn {\n\t\tuserId,\n\t\tpercent: 0,\n\t\tweeklyHours: 0,\n\t\tworkdays: \"0;1;1;1;1;1;0\",\n\t\tstartDate: null,\n\t\tendDate: null,\n\t\tovertimeCarryover: 0,\n\t\tvorholzeitPerYear: 0,\n\t\tvacationCarryover: 0,\n\t\tvacationPerYear: 0,\n\t\tovertimeModel: \"monthly\",\n\t\tholidayFlags: null,\n\t\tpausePaidMinutes: 0,\n\t\tshowWorkedTime: false,\n\t};\n}\n\n/**\n * Maps a database row to a day aggregate record.\n *\n * @param row - raw database row\n * @returns day aggregate\n */\nexport function mapDayRow(row: DayRow): DayAggregateRecord {\n\treturn {\n\t\tuserId: row.user_id,\n\t\tlocalDate: row.local_date,\n\t\tworkedMin: row.worked_min,\n\t\tbreakMin: row.break_min,\n\t\tpaidBreakMin: row.paid_break_min,\n\t\ttargetMin: row.target_min,\n\t\tbalanceMin: row.balance_min,\n\t\tabsenceCode: row.absence_code,\n\t\tisHoliday: row.is_holiday !== 0,\n\t\tfirstInUtc: row.first_in_utc,\n\t\tlastOutUtc: row.last_out_utc,\n\t\thasOpenEntry: row.has_open_entry !== 0,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n/**\n * Maps a database row to a monthly aggregate record.\n *\n * @param row - raw database row\n * @returns monthly aggregate\n */\nexport function mapMonthRow(row: MonthRow): MonthAggregateRecord {\n\treturn {\n\t\tuserId: row.user_id,\n\t\tyear: row.year,\n\t\tmonth: row.month,\n\t\tworkedMin: row.worked_min,\n\t\ttargetMin: row.target_min,\n\t\tbalanceMin: row.balance_min,\n\t\tovertimeMin: row.overtime_min,\n\t\tvacationUsed: row.vacation_used,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n/**\n * Maps a database row to a yearly aggregate record.\n *\n * @param row - raw database row\n * @returns yearly aggregate\n */\nexport function mapYearRow(row: YearRow): YearAggregateRecord {\n\treturn {\n\t\tuserId: row.user_id,\n\t\tyear: row.year,\n\t\tworkedMin: row.worked_min,\n\t\ttargetMin: row.target_min,\n\t\tovertimeMin: row.overtime_min,\n\t\tovertimeStart: row.overtime_start,\n\t\tvacationDays: row.vacation_days,\n\t\tvacationUsed: row.vacation_used,\n\t\tvacationLeft: row.vacation_left,\n\t\tvacationPlannedDays: row.vacation_planned_days,\n\t\tsettled: row.settled !== 0,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n/**\n * Creates the aggregation service.\n *\n * @param deps - data sources\n * @returns service instance\n */\nexport function createAggregationService(deps: AggregationDeps): AggregationService {\n\tconst { db, users, entries, absences, holidays, rules, settings } = deps;\n\n\tconst selectDay = db.prepare(`SELECT ${DAY_COLUMNS} FROM day_aggregates WHERE user_id = ? AND local_date = ?`);\n\tconst selectDays = db.prepare(\n\t\t`SELECT ${DAY_COLUMNS} FROM day_aggregates\n\t\t WHERE user_id = ? AND local_date >= ? AND local_date <= ?\n\t\t ORDER BY local_date`,\n\t);\n\tconst upsertDay = db.prepare(\n\t\t`INSERT INTO day_aggregates (${DAY_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t ON CONFLICT(user_id, local_date) DO UPDATE SET\n\t\t worked_min = excluded.worked_min,\n\t\t break_min = excluded.break_min,\n\t\t paid_break_min = excluded.paid_break_min,\n\t\t target_min = excluded.target_min,\n\t\t balance_min = excluded.balance_min,\n\t\t absence_code = excluded.absence_code,\n\t\t is_holiday = excluded.is_holiday,\n\t\t first_in_utc = excluded.first_in_utc,\n\t\t last_out_utc = excluded.last_out_utc,\n\t\t has_open_entry = excluded.has_open_entry,\n\t\t updated_at = excluded.updated_at`,\n\t);\n\tconst selectMonth = db.prepare(\n\t\t`SELECT ${MONTH_COLUMNS} FROM month_aggregates WHERE user_id = ? AND year = ? AND month = ?`,\n\t);\n\tconst upsertMonth = db.prepare(\n\t\t`INSERT INTO month_aggregates (${MONTH_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t ON CONFLICT(user_id, year, month) DO UPDATE SET\n\t\t worked_min = excluded.worked_min,\n\t\t target_min = excluded.target_min,\n\t\t balance_min = excluded.balance_min,\n\t\t overtime_min = excluded.overtime_min,\n\t\t vacation_used = excluded.vacation_used,\n\t\t updated_at = excluded.updated_at`,\n\t);\n\tconst selectYear = db.prepare(`SELECT ${YEAR_COLUMNS} FROM year_aggregates WHERE user_id = ? AND year = ?`);\n\tconst upsertYear = db.prepare(\n\t\t`INSERT INTO year_aggregates (${YEAR_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t ON CONFLICT(user_id, year) DO UPDATE SET\n\t\t worked_min = excluded.worked_min,\n\t\t target_min = excluded.target_min,\n\t\t overtime_min = excluded.overtime_min,\n\t\t overtime_start = excluded.overtime_start,\n\t\t vacation_days = excluded.vacation_days,\n\t\t vacation_used = excluded.vacation_used,\n\t\t vacation_left = excluded.vacation_left,\n\t\t vacation_planned_days = excluded.vacation_planned_days,\n\t\t settled = excluded.settled,\n\t\t updated_at = excluded.updated_at`,\n\t);\n\tconst selectPayouts = db.prepare(\n\t\t\"SELECT COALESCE(SUM(minutes), 0) AS minutes FROM payouts WHERE user_id = ? AND year = ?\",\n\t);\n\tconst selectEntryCache = db.prepare(\"SELECT id, ts_utc AS tsUtc FROM time_entries WHERE user_id = ?\");\n\tconst updateEntryCache = db.prepare(\"UPDATE time_entries SET ts_local = ?, local_date = ? WHERE id = ?\");\n\n\tconst timeZoneOf = (userId: number): string => users.findById(userId)?.timezone ?? \"UTC\";\n\tconst holidayRegion = (): string => settings.get(\"holiday_country\") ?? \"DE\";\n\n\t/**\n\t * How the pause of a day is determined.\n\t *\n\t * A stored value that is not one of the three modes falls back to the default, so a hand edited setting can\n\t * never spoil a calculation.\n\t *\n\t * @returns the configured pause mode\n\t */\n\tconst pauseMode = (): PauseMode => {\n\t\tconst stored = settings.get(\"pause_mode\");\n\t\treturn stored === \"punched\" || stored === \"staffel\" ? stored : \"auto\";\n\t};\n\n\t/**\n\t * Reads the stored profile or the defaults (no target time, no carryovers).\n\t *\n\t * @param userId - owner of the profile\n\t * @returns employment parameters\n\t */\n\tconst profileOf = (userId: number): WorkProfileRecord => users.getWorkProfile(userId) ?? defaultProfile(userId);\n\n\t/**\n\t * Picks the absence of a day: the one crediting the largest part of the day wins, ties are broken by id.\n\t *\n\t * @param userId - owner of the absence\n\t * @param date - local date\n\t * @returns the covering absence or `null`\n\t */\n\tconst absenceFor = (userId: number, date: string): AbsenceWithType | null => {\n\t\tconst covering = absences.withTypesInRange(userId, date, date);\n\t\tlet best: AbsenceWithType | null = null;\n\t\tfor (const absence of covering) {\n\t\t\tconst share = (Math.max(0, Math.min(100, absence.factor)) / 100) * absence.dayPortion;\n\t\t\tconst bestShare = best ? (Math.max(0, Math.min(100, best.factor)) / 100) * best.dayPortion : -1;\n\t\t\tif (share > bestShare || (share === bestShare && best !== null && absence.id < best.id)) {\n\t\t\t\tbest = absence;\n\t\t\t}\n\t\t}\n\t\treturn best;\n\t};\n\n\t/**\n\t * Sum of the payouts of a year.\n\t *\n\t * @param userId - owner of the payouts\n\t * @param year - four digit year\n\t * @returns paid out minutes\n\t */\n\tconst payoutsOf = (userId: number, year: number): number =>\n\t\t(selectPayouts.get(userId, year) as { minutes: number }).minutes;\n\n\t/**\n\t * Counts the months of a year that lie inside the employment period.\n\t *\n\t * @param profile - employment parameters\n\t * @param year - four digit year\n\t * @param upToMonth - last month to count (1 to 12)\n\t * @param timeZone - time zone used to resolve the employment dates\n\t * @returns number of active months\n\t */\n\tconst activeMonthsInYear = (profile: WorkProfile, year: number, upToMonth: number, timeZone: string): number => {\n\t\tconst startDate = profile.startDate == null ? null : resolveLocalDate(profile.startDate, timeZone);\n\t\tconst endDate = profile.endDate == null ? null : resolveLocalDate(profile.endDate, timeZone);\n\t\tconst limit = Math.max(0, Math.min(12, upToMonth));\n\t\tlet months = 0;\n\n\t\tfor (let month = 1; month <= limit; month++) {\n\t\t\tconst key = `${year}-${String(month).padStart(2, \"0\")}`;\n\t\t\tconst first = `${key}-01`;\n\t\t\tconst last = `${key}-${String(daysInMonth(year, month)).padStart(2, \"0\")}`;\n\t\t\tif ((startDate === null || last >= startDate) && (endDate === null || first <= endDate)) {\n\t\t\t\tmonths++;\n\t\t\t}\n\t\t}\n\t\treturn months;\n\t};\n\n\t/**\n\t * Overtime taken over at the beginning of a year: the previous year's result or the profile carryover.\n\t *\n\t * @param userId - owner\n\t * @param year - four digit year\n\t * @param carryover - carryover of the profile (used for the first year)\n\t * @returns overtime in minutes\n\t */\n\tconst overtimeStartFor = (userId: number, year: number, carryover: number): number => {\n\t\tconst previous = selectYear.get(userId, year - 1) as YearRow | undefined;\n\t\treturn previous ? previous.overtime_min : carryover;\n\t};\n\n\t/**\n\t * Overtime carried after a month, following the model of the profile.\n\t *\n\t * @param args - month, balances, profile and flags\n\t * @param args.userId - owner\n\t * @param args.year - four digit year\n\t * @param args.month - month, 1 to 12\n\t * @param args.monthBalance - freshly calculated balance of that month (`null` to read it from the database)\n\t * @param args.profile - employment parameters\n\t * @param args.timeZone - time zone of the employee\n\t * @param args.completeYear - true when the calendar year is finished\n\t * @returns carried overtime in minutes\n\t */\n\tconst overtimeAfterMonth = (args: {\n\t\tuserId: number;\n\t\tyear: number;\n\t\tmonth: number;\n\t\tmonthBalance: number | null;\n\t\tprofile: WorkProfileRecord;\n\t\ttimeZone: string;\n\t\tcompleteYear: boolean;\n\t}): number => {\n\t\tconst balances: number[] = [];\n\t\tfor (let index = 1; index <= args.month; index++) {\n\t\t\tif (index === args.month && args.monthBalance !== null) {\n\t\t\t\tbalances.push(args.monthBalance);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst row = selectMonth.get(args.userId, args.year, index) as MonthRow | undefined;\n\t\t\tbalances.push(row?.balance_min ?? 0);\n\t\t}\n\n\t\treturn overtimeAfterPeriod({\n\t\t\tmodel: args.profile.overtimeModel,\n\t\t\tmonthBalances: balances,\n\t\t\tovertimeStartMinutes: overtimeStartFor(args.userId, args.year, args.profile.overtimeCarryover),\n\t\t\tvorholzeitPerYearMinutes: args.profile.vorholzeitPerYear,\n\t\t\tactiveMonths: activeMonthsInYear(args.profile, args.year, args.month, args.timeZone),\n\t\t\tpayoutMinutes: payoutsOf(args.userId, args.year),\n\t\t\tcompleteYear: args.completeYear,\n\t\t}).overtimeMinutes;\n\t};\n\n\t/**\n\t * Vacation days of one kind inside a month or year.\n\t *\n\t * @param args - user, period and status\n\t * @param args.userId - owner\n\t * @param args.from - first local date\n\t * @param args.to - last local date\n\t * @param args.status - `taken` for used days, `planned` for approved days\n\t * @param args.profile - employment parameters (working days)\n\t * @param args.timeZone - time zone of the employee\n\t * @returns vacation days, rounded to two decimals\n\t */\n\tconst vacationDaysInRange = (args: {\n\t\tuserId: number;\n\t\tfrom: string;\n\t\tto: string;\n\t\tstatus: \"taken\" | \"planned\";\n\t\tprofile: WorkProfileRecord;\n\t\ttimeZone: string;\n\t}): number => {\n\t\tconst year = Number(args.to.slice(0, 4));\n\t\tconst holidayDates = holidays.dateSet(year, holidayRegion());\n\t\tconst overlapping = absences.withTypesInRange(args.userId, args.from, args.to);\n\n\t\tlet days = 0;\n\t\tfor (const absence of overlapping) {\n\t\t\tif (!absence.reduceVacation || absence.status !== args.status) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst from = absence.dateFrom < args.from ? args.from : absence.dateFrom;\n\t\t\tconst to = absence.dateTo > args.to ? args.to : absence.dateTo;\n\t\t\tif (from > to) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdays += absenceDaysInRange({\n\t\t\t\tfrom,\n\t\t\t\tto,\n\t\t\t\tworkdays: args.profile.workdays,\n\t\t\t\ttimeZone: args.timeZone,\n\t\t\t\tholidays: holidayDates,\n\t\t\t\tdayPortion: absence.dayPortion,\n\t\t\t});\n\t\t}\n\t\treturn Math.round(days * 100) / 100;\n\t};\n\n\t/**\n\t * True when a calendar year is finished (needed by the `yearly` overtime model).\n\t *\n\t * @param year - four digit year\n\t * @param todayDate - local date of the user\n\t * @param settled - explicit flag of the caller\n\t * @returns true when the year is closed\n\t */\n\tconst isCompleteYear = (year: number, todayDate: string, settled?: boolean): boolean =>\n\t\tsettled === true || Number(todayDate.slice(0, 4)) > year;\n\n\t/**\n\t * Calculates one day from the stored data (profile, rules, holidays, absences, punches).\n\t *\n\t * @param userId - owner\n\t * @param localDate - local date to calculate\n\t * @param now - instant of the recalculation\n\t * @returns the calculated day\n\t */\n\tconst calculateDayFor = (userId: number, localDate: string, now: number): DayAggregateRecord => {\n\t\tconst timeZone = timeZoneOf(userId);\n\t\tconst absence = absenceFor(userId, localDate);\n\t\tconst isHoliday = holidays.isHoliday(localDate, holidayRegion());\n\t\tconst profile = profileOf(userId);\n\t\tconst day = calculateDay({\n\t\t\tprofile,\n\t\t\tentries: toPunchEntries(entries.listByDate(userId, localDate)),\n\t\t\tpauseRules: rules.pauseRules(userId),\n\t\t\tpauseMode: pauseMode(),\n\t\t\tpausePaidMinutes: profile.pausePaidMinutes,\n\t\t\ttimeZone,\n\t\t\tlocalDate,\n\t\t\tisHoliday,\n\t\t\tabsence: absence ? { factor: absence.factor, dayPortion: absence.dayPortion } : null,\n\t\t});\n\n\t\treturn {\n\t\t\tuserId,\n\t\t\tlocalDate,\n\t\t\tworkedMin: day.workedMinutes,\n\t\t\tbreakMin: day.breakMinutes,\n\t\t\tpaidBreakMin: day.paidPauseMinutes,\n\t\t\ttargetMin: day.targetMinutes,\n\t\t\tbalanceMin: day.balanceMinutes,\n\t\t\tabsenceCode: absence?.typeCode ?? null,\n\t\t\tisHoliday,\n\t\t\tfirstInUtc: day.firstInUtc,\n\t\t\tlastOutUtc: day.lastOutUtc,\n\t\t\thasOpenEntry: day.hasOpenEntry,\n\t\t\tupdatedAt: now,\n\t\t};\n\t};\n\n\treturn {\n\t\trecalculateDay(userId: number, localDate: string, options?: RecalculateOptions): DayAggregateRecord {\n\t\t\tconst now = options?.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst record = calculateDayFor(userId, localDate, now);\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupsertDay.run(\n\t\t\t\t\trecord.userId,\n\t\t\t\t\trecord.localDate,\n\t\t\t\t\trecord.workedMin,\n\t\t\t\t\trecord.breakMin,\n\t\t\t\t\trecord.paidBreakMin,\n\t\t\t\t\trecord.targetMin,\n\t\t\t\t\trecord.balanceMin,\n\t\t\t\t\trecord.absenceCode,\n\t\t\t\t\trecord.isHoliday ? 1 : 0,\n\t\t\t\t\trecord.firstInUtc,\n\t\t\t\t\trecord.lastOutUtc,\n\t\t\t\t\trecord.hasOpenEntry ? 1 : 0,\n\t\t\t\t\trecord.updatedAt,\n\t\t\t\t);\n\t\t\t});\n\t\t\trun();\n\t\t\treturn record;\n\t\t},\n\n\t\trecalculateRange(userId: number, from: string, to: string, options?: RecalculateOptions): RangeResult {\n\t\t\tconst totals: RangeResult = {\n\t\t\t\tdays: 0,\n\t\t\t\topenDays: 0,\n\t\t\t\tworkedMin: 0,\n\t\t\t\tbreakMin: 0,\n\t\t\t\tpaidBreakMin: 0,\n\t\t\t\ttargetMin: 0,\n\t\t\t\tbalanceMin: 0,\n\t\t\t};\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tfor (const date of dateRange(from, to)) {\n\t\t\t\t\tconst record = this.recalculateDay(userId, date, options);\n\t\t\t\t\ttotals.days++;\n\t\t\t\t\ttotals.workedMin += record.workedMin;\n\t\t\t\t\ttotals.breakMin += record.breakMin;\n\t\t\t\t\ttotals.paidBreakMin += record.paidBreakMin;\n\t\t\t\t\ttotals.targetMin += record.targetMin;\n\t\t\t\t\ttotals.balanceMin += record.balanceMin;\n\t\t\t\t\ttotals.openDays += record.hasOpenEntry ? 1 : 0;\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\t\t\treturn totals;\n\t\t},\n\n\t\trecalculateMonth(\n\t\t\tuserId: number,\n\t\t\tyear: number,\n\t\t\tmonth: number,\n\t\t\toptions?: RecalculateOptions,\n\t\t): MonthAggregateRecord {\n\t\t\tif (!Number.isInteger(month) || month < 1 || month > 12) {\n\t\t\t\tthrow new ValidationError(`month must be between 1 and 12 (got ${month})`);\n\t\t\t}\n\n\t\t\tconst now = options?.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst timeZone = timeZoneOf(userId);\n\t\t\tconst profile = profileOf(userId);\n\t\t\tconst upToDate = options?.upToDate ?? resolveLocalDate(now, timeZone);\n\t\t\tconst key = `${year}-${String(month).padStart(2, \"0\")}`;\n\t\t\tconst firstDay = `${key}-01`;\n\t\t\tconst lastDay = `${key}-${String(daysInMonth(year, month)).padStart(2, \"0\")}`;\n\t\t\t// days after `upToDate` are skipped unless the caller asks for them\n\t\t\tconst endDate = options?.includeFuture === true || lastDay <= upToDate ? lastDay : upToDate;\n\n\t\t\tlet workedMin = 0;\n\t\t\tlet targetMin = 0;\n\t\t\tlet balanceMin = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tfor (const date of dateRange(firstDay, endDate)) {\n\t\t\t\t\tconst record = this.recalculateDay(userId, date, options);\n\t\t\t\t\tworkedMin += record.workedMin;\n\t\t\t\t\ttargetMin += record.targetMin;\n\t\t\t\t\tbalanceMin += record.balanceMin;\n\t\t\t\t}\n\n\t\t\t\tconst vacationUsed = vacationDaysInRange({\n\t\t\t\t\tuserId,\n\t\t\t\t\tfrom: firstDay,\n\t\t\t\t\tto: lastDay,\n\t\t\t\t\tstatus: \"taken\",\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t});\n\t\t\t\tconst overtimeMin = overtimeAfterMonth({\n\t\t\t\t\tuserId,\n\t\t\t\t\tyear,\n\t\t\t\t\tmonth,\n\t\t\t\t\tmonthBalance: balanceMin,\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t\tcompleteYear: isCompleteYear(year, upToDate, options?.settled),\n\t\t\t\t});\n\n\t\t\t\tupsertMonth.run(userId, year, month, workedMin, targetMin, balanceMin, overtimeMin, vacationUsed, now);\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = selectMonth.get(userId, year, month) as MonthRow | undefined;\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`month aggregate ${key} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn mapMonthRow(saved);\n\t\t},\n\n\t\trecalculateYear(userId: number, year: number, options?: RecalculateOptions): YearAggregateRecord {\n\t\t\tconst now = options?.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst timeZone = timeZoneOf(userId);\n\t\t\tconst profile = profileOf(userId);\n\t\t\tconst upToDate = options?.upToDate ?? resolveLocalDate(now, timeZone);\n\t\t\tconst settled = isCompleteYear(year, upToDate, options?.settled);\n\t\t\tconst yearStart = `${year}-01-01`;\n\t\t\tconst yearEnd = `${year}-12-31`;\n\n\t\t\tlet workedMin = 0;\n\t\t\tlet targetMin = 0;\n\t\t\tlet overtimeMin = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tfor (let month = 1; month <= 12; month++) {\n\t\t\t\t\tconst row = this.recalculateMonth(userId, year, month, options);\n\t\t\t\t\tworkedMin += row.workedMin;\n\t\t\t\t\ttargetMin += row.targetMin;\n\t\t\t\t}\n\t\t\t\t// the carried overtime after December is the result of the year\n\t\t\t\tovertimeMin = overtimeAfterMonth({\n\t\t\t\t\tuserId,\n\t\t\t\t\tyear,\n\t\t\t\t\tmonth: 12,\n\t\t\t\t\tmonthBalance: null,\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t\tcompleteYear: settled,\n\t\t\t\t});\n\n\t\t\t\tconst vacationDays = profile.vacationCarryover + profile.vacationPerYear;\n\t\t\t\tconst vacationUsed = vacationDaysInRange({\n\t\t\t\t\tuserId,\n\t\t\t\t\tfrom: yearStart,\n\t\t\t\t\tto: yearEnd,\n\t\t\t\t\tstatus: \"taken\",\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t});\n\t\t\t\tconst vacationPlanned = vacationDaysInRange({\n\t\t\t\t\tuserId,\n\t\t\t\t\tfrom: yearStart,\n\t\t\t\t\tto: yearEnd,\n\t\t\t\t\tstatus: \"planned\",\n\t\t\t\t\tprofile,\n\t\t\t\t\ttimeZone,\n\t\t\t\t});\n\n\t\t\t\tupsertYear.run(\n\t\t\t\t\tuserId,\n\t\t\t\t\tyear,\n\t\t\t\t\tworkedMin,\n\t\t\t\t\ttargetMin,\n\t\t\t\t\tovertimeMin,\n\t\t\t\t\tovertimeStartFor(userId, year, profile.overtimeCarryover),\n\t\t\t\t\tMath.round(vacationDays * 100) / 100,\n\t\t\t\t\tvacationUsed,\n\t\t\t\t\tMath.round((vacationDays - vacationUsed) * 100) / 100,\n\t\t\t\t\tvacationPlanned,\n\t\t\t\t\tsettled ? 1 : 0,\n\t\t\t\t\tnow,\n\t\t\t\t);\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = selectYear.get(userId, year) as YearRow | undefined;\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`year aggregate ${year} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn mapYearRow(saved);\n\t\t},\n\n\t\tday(userId: number, localDate: string): DayAggregateRecord | null {\n\t\t\tconst row = selectDay.get(userId, localDate) as DayRow | undefined;\n\t\t\treturn row ? mapDayRow(row) : null;\n\t\t},\n\n\t\tdays(userId: number, from: string, to: string): DayAggregateRecord[] {\n\t\t\treturn (selectDays.all(userId, from, to) as DayRow[]).map(mapDayRow);\n\t\t},\n\n\t\tmonth(userId: number, year: number, month: number): MonthAggregateRecord | null {\n\t\t\tconst row = selectMonth.get(userId, year, month) as MonthRow | undefined;\n\t\t\treturn row ? mapMonthRow(row) : null;\n\t\t},\n\n\t\tyear(userId: number, year: number): YearAggregateRecord | null {\n\t\t\tconst row = selectYear.get(userId, year) as YearRow | undefined;\n\t\t\treturn row ? mapYearRow(row) : null;\n\t\t},\n\n\t\trecalculateLocalCaches(userId: number): number {\n\t\t\tconst timeZone = timeZoneOf(userId);\n\t\t\tconst rows = selectEntryCache.all(userId) as { id: number; tsUtc: number }[];\n\t\t\tlet changed = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tfor (const row of rows) {\n\t\t\t\t\tconst tsLocal = utcToWallTime(row.tsUtc, timeZone);\n\t\t\t\t\tconst date = resolveLocalDate(row.tsUtc, timeZone);\n\t\t\t\t\tif (cacheMatches(db, row.id, tsLocal, date)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tupdateEntryCache.run(tsLocal, date, row.id);\n\t\t\t\t\tchanged++;\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\t\t\treturn changed;\n\t\t},\n\t};\n}\n\n/**\n * Checks whether the local cache fields of a punch already match.\n *\n * @param db - open database handle\n * @param entryId - punch id\n * @param tsLocal - expected wall clock time\n * @param localDate - expected local date\n * @returns true when both values are already stored\n */\nfunction cacheMatches(db: Db, entryId: number, tsLocal: number, localDate: string): boolean {\n\tconst row = db\n\t\t.prepare(\"SELECT ts_local AS tsLocal, local_date AS localDate FROM time_entries WHERE id = ?\")\n\t\t.get(entryId) as { tsLocal: number; localDate: string } | undefined;\n\treturn row !== undefined && row.tsLocal === tsLocal && row.localDate === localDate;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA,oBAAgC;AAChC,yBAAsE;AAEtE,qBAA+B;AAC/B,kBAAqF;AAmMrF,MAAM,cAAc;AAAA;AAGpB,MAAM,gBAAgB;AAAA;AAGtB,MAAM,eAAe;AAAA;AASrB,SAAS,eAAe,QAAmC;AAC1D,SAAO;AAAA,IACN;AAAA,IACA,SAAS;AAAA,IACT,aAAa;AAAA,IACb,UAAU;AAAA,IACV,WAAW;AAAA,IACX,SAAS;AAAA,IACT,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,cAAc;AAAA,IACd,kBAAkB;AAAA,IAClB,gBAAgB;AAAA,EACjB;AACD;AAQO,SAAS,UAAU,KAAiC;AAC1D,SAAO;AAAA,IACN,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,UAAU,IAAI;AAAA,IACd,cAAc,IAAI;AAAA,IAClB,WAAW,IAAI;AAAA,IACf,YAAY,IAAI;AAAA,IAChB,aAAa,IAAI;AAAA,IACjB,WAAW,IAAI,eAAe;AAAA,IAC9B,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,cAAc,IAAI,mBAAmB;AAAA,IACrC,WAAW,IAAI;AAAA,EAChB;AACD;AAQO,SAAS,YAAY,KAAqC;AAChE,SAAO;AAAA,IACN,QAAQ,IAAI;AAAA,IACZ,MAAM,IAAI;AAAA,IACV,OAAO,IAAI;AAAA,IACX,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,YAAY,IAAI;AAAA,IAChB,aAAa,IAAI;AAAA,IACjB,cAAc,IAAI;AAAA,IAClB,WAAW,IAAI;AAAA,EAChB;AACD;AAQO,SAAS,WAAW,KAAmC;AAC7D,SAAO;AAAA,IACN,QAAQ,IAAI;AAAA,IACZ,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,aAAa,IAAI;AAAA,IACjB,eAAe,IAAI;AAAA,IACnB,cAAc,IAAI;AAAA,IAClB,cAAc,IAAI;AAAA,IAClB,cAAc,IAAI;AAAA,IAClB,qBAAqB,IAAI;AAAA,IACzB,SAAS,IAAI,YAAY;AAAA,IACzB,WAAW,IAAI;AAAA,EAChB;AACD;AAQO,SAAS,yBAAyB,MAA2C;AACnF,QAAM,EAAE,IAAI,OAAO,SAAS,UAAU,UAAU,OAAO,SAAS,IAAI;AAEpE,QAAM,YAAY,GAAG,QAAQ,UAAU,WAAW,2DAA2D;AAC7G,QAAM,aAAa,GAAG;AAAA,IACrB,UAAU,WAAW;AAAA;AAAA;AAAA,EAGtB;AACA,QAAM,YAAY,GAAG;AAAA,IACpB,+BAA+B,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAa3C;AACA,QAAM,cAAc,GAAG;AAAA,IACtB,UAAU,aAAa;AAAA,EACxB;AACA,QAAM,cAAc,GAAG;AAAA,IACtB,iCAAiC,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ/C;AACA,QAAM,aAAa,GAAG,QAAQ,UAAU,YAAY,sDAAsD;AAC1G,QAAM,aAAa,GAAG;AAAA,IACrB,gCAAgC,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAY7C;AACA,QAAM,gBAAgB,GAAG;AAAA,IACxB;AAAA,EACD;AACA,QAAM,mBAAmB,GAAG,QAAQ,gEAAgE;AACpG,QAAM,mBAAmB,GAAG,QAAQ,mEAAmE;AAEvG,QAAM,aAAa,CAAC,WAAwB;AAhY7C;AAgYgD,6BAAM,SAAS,MAAM,MAArB,mBAAwB,aAAxB,YAAoC;AAAA;AACnF,QAAM,gBAAgB,MAAW;AAjYlC;AAiYqC,0BAAS,IAAI,iBAAiB,MAA9B,YAAmC;AAAA;AAUvE,QAAM,YAAY,MAAiB;AAClC,UAAM,SAAS,SAAS,IAAI,YAAY;AACxC,WAAO,WAAW,aAAa,WAAW,YAAY,SAAS;AAAA,EAChE;AAQA,QAAM,YAAY,CAAC,WAAmC;AAtZvD;AAsZ0D,uBAAM,eAAe,MAAM,MAA3B,YAAgC,eAAe,MAAM;AAAA;AAS9G,QAAM,aAAa,CAAC,QAAgB,SAAyC;AAC5E,UAAM,WAAW,SAAS,iBAAiB,QAAQ,MAAM,IAAI;AAC7D,QAAI,OAA+B;AACnC,eAAW,WAAW,UAAU;AAC/B,YAAM,QAAS,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,MAAM,CAAC,IAAI,MAAO,QAAQ;AAC3E,YAAM,YAAY,OAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,IAAI,MAAO,KAAK,aAAa;AAC7F,UAAI,QAAQ,aAAc,UAAU,aAAa,SAAS,QAAQ,QAAQ,KAAK,KAAK,IAAK;AACxF,eAAO;AAAA,MACR;AAAA,IACD;AACA,WAAO;AAAA,EACR;AASA,QAAM,YAAY,CAAC,QAAgB,SACjC,cAAc,IAAI,QAAQ,IAAI,EAA0B;AAW1D,QAAM,qBAAqB,CAAC,SAAsB,MAAc,WAAmB,aAA6B;AAC/G,UAAM,YAAY,QAAQ,aAAa,OAAO,WAAO,YAAAA,WAAiB,QAAQ,WAAW,QAAQ;AACjG,UAAM,UAAU,QAAQ,WAAW,OAAO,WAAO,YAAAA,WAAiB,QAAQ,SAAS,QAAQ;AAC3F,UAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,SAAS,CAAC;AACjD,QAAI,SAAS;AAEb,aAAS,QAAQ,GAAG,SAAS,OAAO,SAAS;AAC5C,YAAM,MAAM,GAAG,IAAI,IAAI,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC;AACrD,YAAM,QAAQ,GAAG,GAAG;AACpB,YAAM,OAAO,GAAG,GAAG,IAAI,WAAO,yBAAY,MAAM,KAAK,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AACxE,WAAK,cAAc,QAAQ,QAAQ,eAAe,YAAY,QAAQ,SAAS,UAAU;AACxF;AAAA,MACD;AAAA,IACD;AACA,WAAO;AAAA,EACR;AAUA,QAAM,mBAAmB,CAAC,QAAgB,MAAc,cAA8B;AACrF,UAAM,WAAW,WAAW,IAAI,QAAQ,OAAO,CAAC;AAChD,WAAO,WAAW,SAAS,eAAe;AAAA,EAC3C;AAeA,QAAM,qBAAqB,CAAC,SAQd;AAlff;AAmfE,UAAM,WAAqB,CAAC;AAC5B,aAAS,QAAQ,GAAG,SAAS,KAAK,OAAO,SAAS;AACjD,UAAI,UAAU,KAAK,SAAS,KAAK,iBAAiB,MAAM;AACvD,iBAAS,KAAK,KAAK,YAAY;AAC/B;AAAA,MACD;AACA,YAAM,MAAM,YAAY,IAAI,KAAK,QAAQ,KAAK,MAAM,KAAK;AACzD,eAAS,MAAK,gCAAK,gBAAL,YAAoB,CAAC;AAAA,IACpC;AAEA,eAAO,wCAAoB;AAAA,MAC1B,OAAO,KAAK,QAAQ;AAAA,MACpB,eAAe;AAAA,MACf,sBAAsB,iBAAiB,KAAK,QAAQ,KAAK,MAAM,KAAK,QAAQ,iBAAiB;AAAA,MAC7F,0BAA0B,KAAK,QAAQ;AAAA,MACvC,cAAc,mBAAmB,KAAK,SAAS,KAAK,MAAM,KAAK,OAAO,KAAK,QAAQ;AAAA,MACnF,eAAe,UAAU,KAAK,QAAQ,KAAK,IAAI;AAAA,MAC/C,cAAc,KAAK;AAAA,IACpB,CAAC,EAAE;AAAA,EACJ;AAcA,QAAM,sBAAsB,CAAC,SAOf;AACb,UAAM,OAAO,OAAO,KAAK,GAAG,MAAM,GAAG,CAAC,CAAC;AACvC,UAAM,eAAe,SAAS,QAAQ,MAAM,cAAc,CAAC;AAC3D,UAAM,cAAc,SAAS,iBAAiB,KAAK,QAAQ,KAAK,MAAM,KAAK,EAAE;AAE7E,QAAI,OAAO;AACX,eAAW,WAAW,aAAa;AAClC,UAAI,CAAC,QAAQ,kBAAkB,QAAQ,WAAW,KAAK,QAAQ;AAC9D;AAAA,MACD;AACA,YAAM,OAAO,QAAQ,WAAW,KAAK,OAAO,KAAK,OAAO,QAAQ;AAChE,YAAM,KAAK,QAAQ,SAAS,KAAK,KAAK,KAAK,KAAK,QAAQ;AACxD,UAAI,OAAO,IAAI;AACd;AAAA,MACD;AACA,kBAAQ,uCAAmB;AAAA,QAC1B;AAAA,QACA;AAAA,QACA,UAAU,KAAK,QAAQ;AAAA,QACvB,UAAU,KAAK;AAAA,QACf,UAAU;AAAA,QACV,YAAY,QAAQ;AAAA,MACrB,CAAC;AAAA,IACF;AACA,WAAO,KAAK,MAAM,OAAO,GAAG,IAAI;AAAA,EACjC;AAUA,QAAM,iBAAiB,CAAC,MAAc,WAAmB,YACxD,YAAY,QAAQ,OAAO,UAAU,MAAM,GAAG,CAAC,CAAC,IAAI;AAUrD,QAAM,kBAAkB,CAAC,QAAgB,WAAmB,QAAoC;AAzkBjG;AA0kBE,UAAM,WAAW,WAAW,MAAM;AAClC,UAAM,UAAU,WAAW,QAAQ,SAAS;AAC5C,UAAM,YAAY,SAAS,UAAU,WAAW,cAAc,CAAC;AAC/D,UAAM,UAAU,UAAU,MAAM;AAChC,UAAM,UAAM,iCAAa;AAAA,MACxB;AAAA,MACA,aAAS,+BAAe,QAAQ,WAAW,QAAQ,SAAS,CAAC;AAAA,MAC7D,YAAY,MAAM,WAAW,MAAM;AAAA,MACnC,WAAW,UAAU;AAAA,MACrB,kBAAkB,QAAQ;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,UAAU,EAAE,QAAQ,QAAQ,QAAQ,YAAY,QAAQ,WAAW,IAAI;AAAA,IACjF,CAAC;AAED,WAAO;AAAA,MACN;AAAA,MACA;AAAA,MACA,WAAW,IAAI;AAAA,MACf,UAAU,IAAI;AAAA,MACd,cAAc,IAAI;AAAA,MAClB,WAAW,IAAI;AAAA,MACf,YAAY,IAAI;AAAA,MAChB,cAAa,wCAAS,aAAT,YAAqB;AAAA,MAClC;AAAA,MACA,YAAY,IAAI;AAAA,MAChB,YAAY,IAAI;AAAA,MAChB,cAAc,IAAI;AAAA,MAClB,WAAW;AAAA,IACZ;AAAA,EACD;AAEA,SAAO;AAAA,IACN,eAAe,QAAgB,WAAmB,SAAkD;AA5mBtG;AA6mBG,YAAM,OAAM,wCAAS,QAAT,YAAgB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxD,YAAM,SAAS,gBAAgB,QAAQ,WAAW,GAAG;AAErD,YAAM,MAAM,GAAG,YAAY,MAAY;AACtC,kBAAU;AAAA,UACT,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO,YAAY,IAAI;AAAA,UACvB,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO,eAAe,IAAI;AAAA,UAC1B,OAAO;AAAA,QACR;AAAA,MACD,CAAC;AACD,UAAI;AACJ,aAAO;AAAA,IACR;AAAA,IAEA,iBAAiB,QAAgB,MAAc,IAAY,SAA2C;AACrG,YAAM,SAAsB;AAAA,QAC3B,MAAM;AAAA,QACN,UAAU;AAAA,QACV,WAAW;AAAA,QACX,UAAU;AAAA,QACV,cAAc;AAAA,QACd,WAAW;AAAA,QACX,YAAY;AAAA,MACb;AACA,YAAM,MAAM,GAAG,YAAY,MAAY;AACtC,mBAAW,YAAQ,uBAAU,MAAM,EAAE,GAAG;AACvC,gBAAM,SAAS,KAAK,eAAe,QAAQ,MAAM,OAAO;AACxD,iBAAO;AACP,iBAAO,aAAa,OAAO;AAC3B,iBAAO,YAAY,OAAO;AAC1B,iBAAO,gBAAgB,OAAO;AAC9B,iBAAO,aAAa,OAAO;AAC3B,iBAAO,cAAc,OAAO;AAC5B,iBAAO,YAAY,OAAO,eAAe,IAAI;AAAA,QAC9C;AAAA,MACD,CAAC;AACD,UAAI;AACJ,aAAO;AAAA,IACR;AAAA,IAEA,iBACC,QACA,MACA,OACA,SACuB;AApqB1B;AAqqBG,UAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,IAAI;AACxD,cAAM,IAAI,8BAAgB,uCAAuC,KAAK,GAAG;AAAA,MAC1E;AAEA,YAAM,OAAM,wCAAS,QAAT,YAAgB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxD,YAAM,WAAW,WAAW,MAAM;AAClC,YAAM,UAAU,UAAU,MAAM;AAChC,YAAM,YAAW,wCAAS,aAAT,gBAAqB,YAAAA,WAAiB,KAAK,QAAQ;AACpE,YAAM,MAAM,GAAG,IAAI,IAAI,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC;AACrD,YAAM,WAAW,GAAG,GAAG;AACvB,YAAM,UAAU,GAAG,GAAG,IAAI,WAAO,yBAAY,MAAM,KAAK,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAE3E,YAAM,WAAU,mCAAS,mBAAkB,QAAQ,WAAW,WAAW,UAAU;AAEnF,UAAI,YAAY;AAChB,UAAI,YAAY;AAChB,UAAI,aAAa;AAEjB,YAAM,MAAM,GAAG,YAAY,MAAY;AACtC,mBAAW,YAAQ,uBAAU,UAAU,OAAO,GAAG;AAChD,gBAAM,SAAS,KAAK,eAAe,QAAQ,MAAM,OAAO;AACxD,uBAAa,OAAO;AACpB,uBAAa,OAAO;AACpB,wBAAc,OAAO;AAAA,QACtB;AAEA,cAAM,eAAe,oBAAoB;AAAA,UACxC;AAAA,UACA,MAAM;AAAA,UACN,IAAI;AAAA,UACJ,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,QACD,CAAC;AACD,cAAM,cAAc,mBAAmB;AAAA,UACtC;AAAA,UACA;AAAA,UACA;AAAA,UACA,cAAc;AAAA,UACd;AAAA,UACA;AAAA,UACA,cAAc,eAAe,MAAM,UAAU,mCAAS,OAAO;AAAA,QAC9D,CAAC;AAED,oBAAY,IAAI,QAAQ,MAAM,OAAO,WAAW,WAAW,YAAY,aAAa,cAAc,GAAG;AAAA,MACtG,CAAC;AACD,UAAI;AAEJ,YAAM,QAAQ,YAAY,IAAI,QAAQ,MAAM,KAAK;AACjD,UAAI,CAAC,OAAO;AACX,cAAM,IAAI,MAAM,mBAAmB,GAAG,oCAAoC;AAAA,MAC3E;AACA,aAAO,YAAY,KAAK;AAAA,IACzB;AAAA,IAEA,gBAAgB,QAAgB,MAAc,SAAmD;AA5tBnG;AA6tBG,YAAM,OAAM,wCAAS,QAAT,YAAgB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxD,YAAM,WAAW,WAAW,MAAM;AAClC,YAAM,UAAU,UAAU,MAAM;AAChC,YAAM,YAAW,wCAAS,aAAT,gBAAqB,YAAAA,WAAiB,KAAK,QAAQ;AACpE,YAAM,UAAU,eAAe,MAAM,UAAU,mCAAS,OAAO;AAC/D,YAAM,YAAY,GAAG,IAAI;AACzB,YAAM,UAAU,GAAG,IAAI;AAEvB,UAAI,YAAY;AAChB,UAAI,YAAY;AAChB,UAAI,cAAc;AAElB,YAAM,MAAM,GAAG,YAAY,MAAY;AACtC,iBAAS,QAAQ,GAAG,SAAS,IAAI,SAAS;AACzC,gBAAM,MAAM,KAAK,iBAAiB,QAAQ,MAAM,OAAO,OAAO;AAC9D,uBAAa,IAAI;AACjB,uBAAa,IAAI;AAAA,QAClB;AAEA,sBAAc,mBAAmB;AAAA,UAChC;AAAA,UACA;AAAA,UACA,OAAO;AAAA,UACP,cAAc;AAAA,UACd;AAAA,UACA;AAAA,UACA,cAAc;AAAA,QACf,CAAC;AAED,cAAM,eAAe,QAAQ,oBAAoB,QAAQ;AACzD,cAAM,eAAe,oBAAoB;AAAA,UACxC;AAAA,UACA,MAAM;AAAA,UACN,IAAI;AAAA,UACJ,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,QACD,CAAC;AACD,cAAM,kBAAkB,oBAAoB;AAAA,UAC3C;AAAA,UACA,MAAM;AAAA,UACN,IAAI;AAAA,UACJ,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,QACD,CAAC;AAED,mBAAW;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,iBAAiB,QAAQ,MAAM,QAAQ,iBAAiB;AAAA,UACxD,KAAK,MAAM,eAAe,GAAG,IAAI;AAAA,UACjC;AAAA,UACA,KAAK,OAAO,eAAe,gBAAgB,GAAG,IAAI;AAAA,UAClD;AAAA,UACA,UAAU,IAAI;AAAA,UACd;AAAA,QACD;AAAA,MACD,CAAC;AACD,UAAI;AAEJ,YAAM,QAAQ,WAAW,IAAI,QAAQ,IAAI;AACzC,UAAI,CAAC,OAAO;AACX,cAAM,IAAI,MAAM,kBAAkB,IAAI,oCAAoC;AAAA,MAC3E;AACA,aAAO,WAAW,KAAK;AAAA,IACxB;AAAA,IAEA,IAAI,QAAgB,WAA8C;AACjE,YAAM,MAAM,UAAU,IAAI,QAAQ,SAAS;AAC3C,aAAO,MAAM,UAAU,GAAG,IAAI;AAAA,IAC/B;AAAA,IAEA,KAAK,QAAgB,MAAc,IAAkC;AACpE,aAAQ,WAAW,IAAI,QAAQ,MAAM,EAAE,EAAe,IAAI,SAAS;AAAA,IACpE;AAAA,IAEA,MAAM,QAAgB,MAAc,OAA4C;AAC/E,YAAM,MAAM,YAAY,IAAI,QAAQ,MAAM,KAAK;AAC/C,aAAO,MAAM,YAAY,GAAG,IAAI;AAAA,IACjC;AAAA,IAEA,KAAK,QAAgB,MAA0C;AAC9D,YAAM,MAAM,WAAW,IAAI,QAAQ,IAAI;AACvC,aAAO,MAAM,WAAW,GAAG,IAAI;AAAA,IAChC;AAAA,IAEA,uBAAuB,QAAwB;AAC9C,YAAM,WAAW,WAAW,MAAM;AAClC,YAAM,OAAO,iBAAiB,IAAI,MAAM;AACxC,UAAI,UAAU;AAEd,YAAM,MAAM,GAAG,YAAY,MAAY;AACtC,mBAAW,OAAO,MAAM;AACvB,gBAAM,cAAU,2BAAc,IAAI,OAAO,QAAQ;AACjD,gBAAM,WAAO,YAAAA,WAAiB,IAAI,OAAO,QAAQ;AACjD,cAAI,aAAa,IAAI,IAAI,IAAI,SAAS,IAAI,GAAG;AAC5C;AAAA,UACD;AACA,2BAAiB,IAAI,SAAS,MAAM,IAAI,EAAE;AAC1C;AAAA,QACD;AAAA,MACD,CAAC;AACD,UAAI;AACJ,aAAO;AAAA,IACR;AAAA,EACD;AACD;AAWA,SAAS,aAAa,IAAQ,SAAiB,SAAiB,WAA4B;AAC3F,QAAM,MAAM,GACV,QAAQ,oFAAoF,EAC5F,IAAI,OAAO;AACb,SAAO,QAAQ,UAAa,IAAI,YAAY,WAAW,IAAI,cAAc;AAC1E;",
6
6
  "names": ["resolveLocalDate"]
7
7
  }
@@ -192,7 +192,8 @@ const PROFILE_FIELDS = [
192
192
  "vacationPerYear",
193
193
  "overtimeModel",
194
194
  "holidayFlags",
195
- "pausePaidMinutes"
195
+ "pausePaidMinutes",
196
+ "showWorkedTime"
196
197
  ];
197
198
  function readProfilePatch(body) {
198
199
  const patch = {};
@@ -255,6 +256,12 @@ function readProfilePatch(body) {
255
256
  continue;
256
257
  }
257
258
  patch.pausePaidMinutes = minutes;
259
+ } else if (key === "showWorkedTime") {
260
+ if (typeof value !== "boolean") {
261
+ issues.push({ path: key, message: "must be true or false" });
262
+ continue;
263
+ }
264
+ patch.showWorkedTime = value;
258
265
  } else {
259
266
  const number = Number(value);
260
267
  if (!Number.isInteger(number)) {
@@ -1953,11 +1960,17 @@ function createApi(deps) {
1953
1960
  const visible = terminal.userIds.length === 0 ? candidates : candidates.filter((user) => terminal.userIds.includes(user.id));
1954
1961
  return (0, import_router.json)(200, {
1955
1962
  users: visible.map((user) => {
1956
- var _a3;
1963
+ var _a3, _b2;
1964
+ const date = (0, import_time.localDate)(timestamp, user.timezone);
1965
+ const day = aggregation.recalculateDay(user.id, date, { now: timestamp });
1966
+ const open = (day == null ? void 0 : day.hasOpenEntry) ? entries.listByDate(user.id, date).at(-1) : void 0;
1967
+ const runningMin = open ? Math.max(0, Math.floor((timestamp - open.tsUtc) / 60)) : 0;
1968
+ const showWorkedTime = ((_a3 = users.getWorkProfile(user.id)) == null ? void 0 : _a3.showWorkedTime) === true;
1957
1969
  return {
1958
1970
  id: user.id,
1959
1971
  displayName: user.displayName,
1960
- present: ((_a3 = aggregation.day(user.id, (0, import_time.localDate)(timestamp, user.timezone))) == null ? void 0 : _a3.hasOpenEntry) === true,
1972
+ present: (day == null ? void 0 : day.hasOpenEntry) === true,
1973
+ workedMin: showWorkedTime ? ((_b2 = day == null ? void 0 : day.workedMin) != null ? _b2 : 0) + runningMin : void 0,
1961
1974
  // the picture is fetched by the browser from its own route; the session of this device travels
1962
1975
  // in the query, so a plain `<img>` can load it
1963
1976
  avatarUrl: user.avatar ? `/api/users/${user.id}/avatar?terminalSession=${encodeURIComponent(terminalSession)}&v=${user.updatedAt}` : null
@@ -1971,7 +1984,7 @@ function createApi(deps) {
1971
1984
  "/terminal/punch",
1972
1985
  { public: true, csrf: false, rateLimit: { name: "terminal-punch", limit: 60, windowSeconds: 60 } },
1973
1986
  (context) => {
1974
- var _a2, _b2;
1987
+ var _a2, _b2, _c2;
1975
1988
  const terminal = requireTerminal(context);
1976
1989
  const body = context.jsonBody();
1977
1990
  const badge = optionalString(body, "badge");
@@ -2030,7 +2043,9 @@ function createApi(deps) {
2030
2043
  localDate: stored.entry.localDate
2031
2044
  },
2032
2045
  day: {
2033
- workedMin: day.workedMin,
2046
+ // the worked time of the day only when the work profile of the employee allows it: the
2047
+ // presence card is public, so the same rule as in `/terminal/users` applies here
2048
+ workedMin: ((_c2 = users.getWorkProfile(user.id)) == null ? void 0 : _c2.showWorkedTime) === true ? day.workedMin : void 0,
2034
2049
  targetMin: day.targetMin,
2035
2050
  balanceMin: day.balanceMin,
2036
2051
  hasOpenEntry: day.hasOpenEntry