iobroker.time-tracker 0.2.0

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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -0
  3. package/admin/i18n/de.json +34 -0
  4. package/admin/i18n/en.json +1 -0
  5. package/admin/i18n/es.json +34 -0
  6. package/admin/i18n/fr.json +34 -0
  7. package/admin/i18n/it.json +34 -0
  8. package/admin/i18n/nl.json +34 -0
  9. package/admin/i18n/pl.json +34 -0
  10. package/admin/i18n/pt.json +34 -0
  11. package/admin/i18n/ru.json +34 -0
  12. package/admin/i18n/uk.json +34 -0
  13. package/admin/i18n/zh-cn.json +34 -0
  14. package/admin/jsonConfig.json +266 -0
  15. package/admin/time-tracker.png +0 -0
  16. package/build/lib/adapter/automation.js +100 -0
  17. package/build/lib/adapter/automation.js.map +7 -0
  18. package/build/lib/adapter/commands.js +160 -0
  19. package/build/lib/adapter/commands.js.map +7 -0
  20. package/build/lib/adapter/messages.js +207 -0
  21. package/build/lib/adapter/messages.js.map +7 -0
  22. package/build/lib/adapter/presence.js +129 -0
  23. package/build/lib/adapter/presence.js.map +7 -0
  24. package/build/lib/adapter/states.js +330 -0
  25. package/build/lib/adapter/states.js.map +7 -0
  26. package/build/lib/adapter/triggers.js +92 -0
  27. package/build/lib/adapter/triggers.js.map +7 -0
  28. package/build/lib/db/database.js +96 -0
  29. package/build/lib/db/database.js.map +7 -0
  30. package/build/lib/db/migrations.js +603 -0
  31. package/build/lib/db/migrations.js.map +7 -0
  32. package/build/lib/db/repositories/absences.js +432 -0
  33. package/build/lib/db/repositories/absences.js.map +7 -0
  34. package/build/lib/db/repositories/audit.js +90 -0
  35. package/build/lib/db/repositories/audit.js.map +7 -0
  36. package/build/lib/db/repositories/automations.js +326 -0
  37. package/build/lib/db/repositories/automations.js.map +7 -0
  38. package/build/lib/db/repositories/entries.js +335 -0
  39. package/build/lib/db/repositories/entries.js.map +7 -0
  40. package/build/lib/db/repositories/holidays.js +156 -0
  41. package/build/lib/db/repositories/holidays.js.map +7 -0
  42. package/build/lib/db/repositories/payouts.js +221 -0
  43. package/build/lib/db/repositories/payouts.js.map +7 -0
  44. package/build/lib/db/repositories/rfid.js +264 -0
  45. package/build/lib/db/repositories/rfid.js.map +7 -0
  46. package/build/lib/db/repositories/rules.js +356 -0
  47. package/build/lib/db/repositories/rules.js.map +7 -0
  48. package/build/lib/db/repositories/settings.js +91 -0
  49. package/build/lib/db/repositories/settings.js.map +7 -0
  50. package/build/lib/db/repositories/terminals.js +243 -0
  51. package/build/lib/db/repositories/terminals.js.map +7 -0
  52. package/build/lib/db/repositories/triggers.js +240 -0
  53. package/build/lib/db/repositories/triggers.js.map +7 -0
  54. package/build/lib/db/repositories/users.js +491 -0
  55. package/build/lib/db/repositories/users.js.map +7 -0
  56. package/build/lib/db/seed.js +180 -0
  57. package/build/lib/db/seed.js.map +7 -0
  58. package/build/lib/domain/avatar.js +63 -0
  59. package/build/lib/domain/avatar.js.map +7 -0
  60. package/build/lib/domain/breaks.js +70 -0
  61. package/build/lib/domain/breaks.js.map +7 -0
  62. package/build/lib/domain/calculation.js +150 -0
  63. package/build/lib/domain/calculation.js.map +7 -0
  64. package/build/lib/domain/holidays.js +113 -0
  65. package/build/lib/domain/holidays.js.map +7 -0
  66. package/build/lib/domain/punch.js +99 -0
  67. package/build/lib/domain/punch.js.map +7 -0
  68. package/build/lib/domain/target.js +89 -0
  69. package/build/lib/domain/target.js.map +7 -0
  70. package/build/lib/errors.js +56 -0
  71. package/build/lib/errors.js.map +7 -0
  72. package/build/lib/reports/labels.js +358 -0
  73. package/build/lib/reports/labels.js.map +7 -0
  74. package/build/lib/reports/pdf.js +308 -0
  75. package/build/lib/reports/pdf.js.map +7 -0
  76. package/build/lib/reports/types.js +32 -0
  77. package/build/lib/reports/types.js.map +7 -0
  78. package/build/lib/reports/xls.js +211 -0
  79. package/build/lib/reports/xls.js.map +7 -0
  80. package/build/lib/services/aggregation.js +495 -0
  81. package/build/lib/services/aggregation.js.map +7 -0
  82. package/build/lib/services/auth.js +381 -0
  83. package/build/lib/services/auth.js.map +7 -0
  84. package/build/lib/services/backup.js +372 -0
  85. package/build/lib/services/backup.js.map +7 -0
  86. package/build/lib/services/closing.js +106 -0
  87. package/build/lib/services/closing.js.map +7 -0
  88. package/build/lib/services/sessionSecret.js +100 -0
  89. package/build/lib/services/sessionSecret.js.map +7 -0
  90. package/build/lib/services/sync.js +278 -0
  91. package/build/lib/services/sync.js.map +7 -0
  92. package/build/lib/util/time.js +125 -0
  93. package/build/lib/util/time.js.map +7 -0
  94. package/build/lib/web/api.js +2589 -0
  95. package/build/lib/web/api.js.map +7 -0
  96. package/build/lib/web/cookies.js +80 -0
  97. package/build/lib/web/cookies.js.map +7 -0
  98. package/build/lib/web/events.js +50 -0
  99. package/build/lib/web/events.js.map +7 -0
  100. package/build/lib/web/pin-guard.js +86 -0
  101. package/build/lib/web/pin-guard.js.map +7 -0
  102. package/build/lib/web/problem.js +129 -0
  103. package/build/lib/web/problem.js.map +7 -0
  104. package/build/lib/web/rate-limit.js +65 -0
  105. package/build/lib/web/rate-limit.js.map +7 -0
  106. package/build/lib/web/router.js +274 -0
  107. package/build/lib/web/router.js.map +7 -0
  108. package/build/lib/web/server.js +189 -0
  109. package/build/lib/web/server.js.map +7 -0
  110. package/build/lib/web/static.js +151 -0
  111. package/build/lib/web/static.js.map +7 -0
  112. package/build/lib/web/stream.js +135 -0
  113. package/build/lib/web/stream.js.map +7 -0
  114. package/build/main.js +986 -0
  115. package/build/main.js.map +7 -0
  116. package/io-package.json +221 -0
  117. package/package.json +131 -0
  118. package/src-pwa/src/i18n/de.json +359 -0
  119. package/src-pwa/src/i18n/en.json +359 -0
  120. package/src-pwa/src/i18n/es.json +359 -0
  121. package/src-pwa/src/i18n/fr.json +359 -0
  122. package/src-pwa/src/i18n/index.ts +96 -0
  123. package/src-pwa/src/i18n/it.json +359 -0
  124. package/src-pwa/src/i18n/nl.json +359 -0
  125. package/src-pwa/src/i18n/pl.json +359 -0
  126. package/src-pwa/src/i18n/pt.json +359 -0
  127. package/src-pwa/src/i18n/ru.json +359 -0
  128. package/src-pwa/src/i18n/uk.json +359 -0
  129. package/src-pwa/src/i18n/zh-cn.json +359 -0
  130. package/www/assets/index-QI3wrEvW.js +119 -0
  131. package/www/assets/index-QI3wrEvW.js.map +1 -0
  132. package/www/assets/workbox-window.prod.es5-Bd17z0YL.js +3 -0
  133. package/www/assets/workbox-window.prod.es5-Bd17z0YL.js.map +1 -0
  134. package/www/favicon.svg +16 -0
  135. package/www/icon-192.png +0 -0
  136. package/www/icon-512.png +0 -0
  137. package/www/index.html +38 -0
  138. package/www/manifest.webmanifest +1 -0
  139. package/www/person.png +0 -0
  140. package/www/sw.js +3 -0
  141. package/www/sw.js.map +1 -0
  142. package/www/workbox-9c191d2f.js +3 -0
  143. package/www/workbox-9c191d2f.js.map +1 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/reports/xls.ts"],
4
+ "sourcesContent": ["/**\n * Monthly work time statement as an Excel workbook.\n *\n * `/api/reports/xls` returns this file. The layout is a statement, not a data dump: a header block with the\n * employee and the period, one row per day of the month, a totals row, the absences of the month and the two\n * signature lines at the end.\n *\n * Two details matter for correctness:\n * - durations are written as **numbers** (minutes / 1440) with the number format `[h]:mm`. Excel then shows\n * `9:30`, can sum them and keeps them usable; a text like `9:30` would look right but break every formula.\n * - dates and times are formatted through `Intl` with the locale and time zone of the **employee**, so the\n * statement matches the person it describes (`users.locale`, `users.timezone`).\n */\n\nimport ExcelJS from \"exceljs\";\nimport type { ReportAbsence, ReportInput } from \"./types\";\n\nexport type { ReportAbsence };\n\n/** Input of the monthly statement. */\nexport type MonthReportInput = ReportInput & {\n\t/** Name of the generator, e.g. `time-tracker 0.0.1` */\n\tgenerator: string;\n};\n\n/** Number format that shows a duration in minutes as `[h]:mm`. */\nconst DURATION_FORMAT = \"[h]:mm\";\n\n/** Header row of the day table. */\nconst HEADER_ROW = 6;\n\n/** Line below the day table where the totals row is written. */\nconst FIRST_DAY_ROW = HEADER_ROW + 1;\n\n/**\n * Formats a local date for display.\n *\n * @param date - local date, `YYYY-MM-DD`\n * @param locale - locale of the employee\n * @returns formatted date, e.g. `Mo., 14.09.2026`\n */\nfunction formatDate(date: string, locale: string): string {\n\treturn new Intl.DateTimeFormat(locale, {\n\t\tweekday: \"short\",\n\t\tday: \"2-digit\",\n\t\tmonth: \"2-digit\",\n\t\tyear: \"numeric\",\n\t\ttimeZone: \"UTC\",\n\t}).format(new Date(`${date}T12:00:00Z`));\n}\n\n/**\n * Formats an instant as time of day in a time zone.\n *\n * @param tsUtc - UTC epoch seconds or `null`\n * @param timeZone - IANA time zone of the employee\n * @param locale - locale of the employee\n * @returns formatted time or an empty string\n */\nfunction formatTime(tsUtc: number | null, timeZone: string, locale: string): string {\n\tif (tsUtc === null) {\n\t\treturn \"\";\n\t}\n\treturn new Intl.DateTimeFormat(locale, { timeZone, hour: \"2-digit\", minute: \"2-digit\" }).format(\n\t\tnew Date(tsUtc * 1000),\n\t);\n}\n\n/**\n * Converts minutes into the fraction of a day Excel uses for durations.\n *\n * @param minutes - duration in minutes\n * @returns value for a cell with the `[h]:mm` format\n */\nfunction duration(minutes: number): number {\n\treturn minutes / 1440;\n}\n\n/**\n * Formats an instant for the header block.\n *\n * @param tsUtc - UTC epoch seconds\n * @param locale - locale of the employee\n * @param timeZone - IANA time zone of the employee\n * @returns formatted date and time\n */\nfunction formatStamp(tsUtc: number, locale: string, timeZone: string): string {\n\treturn new Intl.DateTimeFormat(locale, {\n\t\ttimeZone,\n\t\tdateStyle: \"short\",\n\t\ttimeStyle: \"short\",\n\t}).format(new Date(tsUtc * 1000));\n}\n\n/**\n * Builds the monthly statement as an Excel workbook.\n *\n * @param input - employee, month, days, absences and labels\n * @returns the workbook as bytes\n */\nexport async function buildMonthReport(input: MonthReportInput): Promise<Buffer> {\n\tconst { labels, locale, user } = input;\n\tconst workbook = new ExcelJS.Workbook();\n\tworkbook.creator = input.generator;\n\tworkbook.created = new Date(input.generatedAt * 1000);\n\tworkbook.modified = workbook.created;\n\n\t// the sheet name is technical and stays readable in every language\n\tconst sheet = workbook.addWorksheet(`${input.year}-${String(input.month).padStart(2, \"0\")}`, {\n\t\tviews: [{ state: \"frozen\", ySplit: HEADER_ROW }],\n\t});\n\n\tsheet.columns = [\n\t\t{ key: \"date\", width: 24 },\n\t\t{ key: \"timeIn\", width: 9 },\n\t\t{ key: \"timeOut\", width: 9 },\n\t\t{ key: \"worked\", width: 11 },\n\t\t{ key: \"breaks\", width: 9 },\n\t\t{ key: \"paidBreaks\", width: 12 },\n\t\t{ key: \"target\", width: 10 },\n\t\t{ key: \"balance\", width: 10 },\n\t\t{ key: \"absence\", width: 12 },\n\t\t{ key: \"note\", width: 18 },\n\t];\n\n\t// --- header block ---------------------------------------------------------\n\tconst from = `${input.year}-${String(input.month).padStart(2, \"0\")}-01`;\n\tconst lastDay = new Date(Date.UTC(input.year, input.month, 0)).getUTCDate();\n\tconst to = `${input.year}-${String(input.month).padStart(2, \"0\")}-${String(lastDay).padStart(2, \"0\")}`;\n\n\tconst title = sheet.getCell(\"A1\");\n\ttitle.value = labels.title;\n\ttitle.font = { bold: true, size: 14 };\n\tsheet.mergeCells(\"A1:J1\");\n\n\tsheet.getCell(\"A2\").value = `${labels.employee}: ${user.displayName} (${user.login})`;\n\tsheet.getCell(\"A3\").value = `${labels.period}: ${formatDate(from, locale)} - ${formatDate(to, locale)}`;\n\tsheet.getCell(\"A4\").value = `${labels.created}: ${formatStamp(input.generatedAt, locale, user.timezone)}`;\n\tfor (const row of [2, 3, 4]) {\n\t\tsheet.getRow(row).font = { size: 10 };\n\t}\n\n\t// --- header of the day table ---------------------------------------------\n\tconst header = sheet.getRow(HEADER_ROW);\n\theader.values = [\n\t\tlabels.date,\n\t\tlabels.timeIn,\n\t\tlabels.timeOut,\n\t\tlabels.worked,\n\t\tlabels.breaks,\n\t\tlabels.paidBreaks,\n\t\tlabels.target,\n\t\tlabels.balance,\n\t\tlabels.absence,\n\t\tlabels.note,\n\t];\n\theader.font = { bold: true };\n\tfor (let column = 1; column <= 10; column++) {\n\t\tconst cell = header.getCell(column);\n\t\tcell.fill = { type: \"pattern\", pattern: \"solid\", fgColor: { argb: \"FFEEF1F5\" } };\n\t\tcell.border = { bottom: { style: \"thin\", color: { argb: \"FF9AA5B1\" } } };\n\t}\n\n\t// --- one row per day ------------------------------------------------------\n\tlet rowIndex = FIRST_DAY_ROW;\n\tconst totals = { worked: 0, breaks: 0, paidBreaks: 0, target: 0, balance: 0 };\n\n\tfor (const day of input.days) {\n\t\tconst row = sheet.getRow(rowIndex++);\n\t\tconst notes: string[] = [];\n\t\tif (day.isHoliday) {\n\t\t\tnotes.push(labels.holiday);\n\t\t}\n\t\tif (day.hasOpenEntry) {\n\t\t\tnotes.push(labels.openEntry);\n\t\t}\n\n\t\trow.values = [\n\t\t\tformatDate(day.localDate, locale),\n\t\t\tformatTime(day.firstInUtc, user.timezone, locale),\n\t\t\tformatTime(day.lastOutUtc, user.timezone, locale),\n\t\t\tduration(day.workedMin),\n\t\t\tduration(day.breakMin),\n\t\t\tduration(day.paidBreakMin),\n\t\t\tduration(day.targetMin),\n\t\t\tduration(day.balanceMin),\n\t\t\tday.absenceCode ?? \"\",\n\t\t\tnotes.join(\", \"),\n\t\t];\n\t\tfor (const column of [4, 5, 6, 7, 8]) {\n\t\t\trow.getCell(column).numFmt = DURATION_FORMAT;\n\t\t}\n\t\t// a negative balance is the one number a reader looks for\n\t\tif (day.balanceMin < 0) {\n\t\t\trow.getCell(8).font = { color: { argb: \"FFC62828\" } };\n\t\t}\n\t\tif (day.isHoliday || day.workedMin === 0) {\n\t\t\trow.getCell(1).font = { color: { argb: \"FF757575\" } };\n\t\t}\n\n\t\ttotals.worked += day.workedMin;\n\t\ttotals.breaks += day.breakMin;\n\t\ttotals.paidBreaks += day.paidBreakMin;\n\t\ttotals.target += day.targetMin;\n\t\ttotals.balance += day.balanceMin;\n\t}\n\n\t// --- totals ---------------------------------------------------------------\n\tconst totalRow = sheet.getRow(rowIndex);\n\ttotalRow.values = [\n\t\tlabels.total,\n\t\t\"\",\n\t\t\"\",\n\t\tduration(totals.worked),\n\t\tduration(totals.breaks),\n\t\tduration(totals.paidBreaks),\n\t\tduration(totals.target),\n\t\tduration(totals.balance),\n\t\t`${labels.days}: ${input.days.length}`,\n\t\t\"\",\n\t];\n\ttotalRow.font = { bold: true };\n\tfor (const column of [4, 5, 6, 7, 8]) {\n\t\tconst cell = totalRow.getCell(column);\n\t\tcell.numFmt = DURATION_FORMAT;\n\t\tcell.border = { top: { style: \"thin\", color: { argb: \"FF9AA5B1\" } } };\n\t}\n\tif (totals.balance < 0) {\n\t\ttotalRow.getCell(8).font = { bold: true, color: { argb: \"FFC62828\" } };\n\t}\n\trowIndex += 1;\n\n\t// --- absences of the month ------------------------------------------------\n\tif (input.absences.length > 0) {\n\t\trowIndex += 2;\n\t\tconst section = sheet.getRow(rowIndex);\n\t\tsection.values = [labels.absences];\n\t\tsection.font = { bold: true };\n\n\t\tconst sectionHeader = sheet.getRow(rowIndex + 1);\n\t\tsectionHeader.values = [labels.absence, labels.from, labels.to, labels.portion, \"\"];\n\t\tsectionHeader.font = { bold: true, size: 10 };\n\n\t\tfor (const absence of input.absences) {\n\t\t\tconst row = sheet.getRow(rowIndex + 2 + input.absences.indexOf(absence));\n\t\t\tconst portion = absence.dayPortion === 0.5 ? labels.halfDay : labels.fullDay;\n\t\t\trow.values = [\n\t\t\t\t`${absence.typeCode} - ${absence.typeName}`,\n\t\t\t\tformatDate(absence.dateFrom, locale),\n\t\t\t\tformatDate(absence.dateTo, locale),\n\t\t\t\tportion,\n\t\t\t\tabsence.hours === null ? \"\" : `${absence.hours} h`,\n\t\t\t];\n\t\t}\n\t\trowIndex += 2 + input.absences.length;\n\t}\n\n\t// --- signatures -----------------------------------------------------------\n\trowIndex += 2;\n\tsheet.getCell(`A${rowIndex}`).value = `${labels.signatureEmployee}: ______________________`;\n\tsheet.getCell(`F${rowIndex}`).value = `${labels.signatureManager}: ______________________`;\n\tsheet.getRow(rowIndex).font = { size: 10 };\n\n\treturn Buffer.from(await workbook.xlsx.writeBuffer());\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["ExcelJS"]
7
+ }
@@ -0,0 +1,495 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var aggregation_exports = {};
20
+ __export(aggregation_exports, {
21
+ createAggregationService: () => createAggregationService,
22
+ mapDayRow: () => mapDayRow,
23
+ mapMonthRow: () => mapMonthRow,
24
+ mapYearRow: () => mapYearRow
25
+ });
26
+ module.exports = __toCommonJS(aggregation_exports);
27
+ var import_errors = require("../errors");
28
+ var import_calculation = require("../domain/calculation");
29
+ var import_entries = require("../db/repositories/entries");
30
+ var import_time = require("../util/time");
31
+ const DAY_COLUMNS = `user_id, local_date, worked_min, break_min, paid_break_min, target_min, balance_min, absence_code,
32
+ is_holiday, first_in_utc, last_out_utc, has_open_entry, updated_at`;
33
+ const MONTH_COLUMNS = `user_id, year, month, worked_min, target_min, balance_min, overtime_min, vacation_used,
34
+ updated_at`;
35
+ const YEAR_COLUMNS = `user_id, year, worked_min, target_min, overtime_min, overtime_start, vacation_days,
36
+ vacation_used, vacation_left, vacation_planned_days, settled, updated_at`;
37
+ function defaultProfile(userId) {
38
+ return {
39
+ userId,
40
+ percent: 0,
41
+ weeklyHours: 0,
42
+ workdays: "0;1;1;1;1;1;0",
43
+ startDate: null,
44
+ endDate: null,
45
+ overtimeCarryover: 0,
46
+ vorholzeitPerYear: 0,
47
+ vacationCarryover: 0,
48
+ vacationPerYear: 0,
49
+ overtimeModel: "monthly",
50
+ holidayFlags: null,
51
+ pausePaidMinutes: 0
52
+ };
53
+ }
54
+ function mapDayRow(row) {
55
+ return {
56
+ userId: row.user_id,
57
+ localDate: row.local_date,
58
+ workedMin: row.worked_min,
59
+ breakMin: row.break_min,
60
+ paidBreakMin: row.paid_break_min,
61
+ targetMin: row.target_min,
62
+ balanceMin: row.balance_min,
63
+ absenceCode: row.absence_code,
64
+ isHoliday: row.is_holiday !== 0,
65
+ firstInUtc: row.first_in_utc,
66
+ lastOutUtc: row.last_out_utc,
67
+ hasOpenEntry: row.has_open_entry !== 0,
68
+ updatedAt: row.updated_at
69
+ };
70
+ }
71
+ function mapMonthRow(row) {
72
+ return {
73
+ userId: row.user_id,
74
+ year: row.year,
75
+ month: row.month,
76
+ workedMin: row.worked_min,
77
+ targetMin: row.target_min,
78
+ balanceMin: row.balance_min,
79
+ overtimeMin: row.overtime_min,
80
+ vacationUsed: row.vacation_used,
81
+ updatedAt: row.updated_at
82
+ };
83
+ }
84
+ function mapYearRow(row) {
85
+ return {
86
+ userId: row.user_id,
87
+ year: row.year,
88
+ workedMin: row.worked_min,
89
+ targetMin: row.target_min,
90
+ overtimeMin: row.overtime_min,
91
+ overtimeStart: row.overtime_start,
92
+ vacationDays: row.vacation_days,
93
+ vacationUsed: row.vacation_used,
94
+ vacationLeft: row.vacation_left,
95
+ vacationPlannedDays: row.vacation_planned_days,
96
+ settled: row.settled !== 0,
97
+ updatedAt: row.updated_at
98
+ };
99
+ }
100
+ function createAggregationService(deps) {
101
+ const { db, users, entries, absences, holidays, rules, settings } = deps;
102
+ const selectDay = db.prepare(`SELECT ${DAY_COLUMNS} FROM day_aggregates WHERE user_id = ? AND local_date = ?`);
103
+ const selectDays = db.prepare(
104
+ `SELECT ${DAY_COLUMNS} FROM day_aggregates
105
+ WHERE user_id = ? AND local_date >= ? AND local_date <= ?
106
+ ORDER BY local_date`
107
+ );
108
+ const upsertDay = db.prepare(
109
+ `INSERT INTO day_aggregates (${DAY_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
110
+ ON CONFLICT(user_id, local_date) DO UPDATE SET
111
+ worked_min = excluded.worked_min,
112
+ break_min = excluded.break_min,
113
+ paid_break_min = excluded.paid_break_min,
114
+ target_min = excluded.target_min,
115
+ balance_min = excluded.balance_min,
116
+ absence_code = excluded.absence_code,
117
+ is_holiday = excluded.is_holiday,
118
+ first_in_utc = excluded.first_in_utc,
119
+ last_out_utc = excluded.last_out_utc,
120
+ has_open_entry = excluded.has_open_entry,
121
+ updated_at = excluded.updated_at`
122
+ );
123
+ const selectMonth = db.prepare(
124
+ `SELECT ${MONTH_COLUMNS} FROM month_aggregates WHERE user_id = ? AND year = ? AND month = ?`
125
+ );
126
+ const upsertMonth = db.prepare(
127
+ `INSERT INTO month_aggregates (${MONTH_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
128
+ ON CONFLICT(user_id, year, month) DO UPDATE SET
129
+ worked_min = excluded.worked_min,
130
+ target_min = excluded.target_min,
131
+ balance_min = excluded.balance_min,
132
+ overtime_min = excluded.overtime_min,
133
+ vacation_used = excluded.vacation_used,
134
+ updated_at = excluded.updated_at`
135
+ );
136
+ const selectYear = db.prepare(`SELECT ${YEAR_COLUMNS} FROM year_aggregates WHERE user_id = ? AND year = ?`);
137
+ const upsertYear = db.prepare(
138
+ `INSERT INTO year_aggregates (${YEAR_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
139
+ ON CONFLICT(user_id, year) DO UPDATE SET
140
+ worked_min = excluded.worked_min,
141
+ target_min = excluded.target_min,
142
+ overtime_min = excluded.overtime_min,
143
+ overtime_start = excluded.overtime_start,
144
+ vacation_days = excluded.vacation_days,
145
+ vacation_used = excluded.vacation_used,
146
+ vacation_left = excluded.vacation_left,
147
+ vacation_planned_days = excluded.vacation_planned_days,
148
+ settled = excluded.settled,
149
+ updated_at = excluded.updated_at`
150
+ );
151
+ const selectPayouts = db.prepare(
152
+ "SELECT COALESCE(SUM(minutes), 0) AS minutes FROM payouts WHERE user_id = ? AND year = ?"
153
+ );
154
+ const selectEntryCache = db.prepare("SELECT id, ts_utc AS tsUtc FROM time_entries WHERE user_id = ?");
155
+ const updateEntryCache = db.prepare("UPDATE time_entries SET ts_local = ?, local_date = ? WHERE id = ?");
156
+ const timeZoneOf = (userId) => {
157
+ var _a, _b;
158
+ return (_b = (_a = users.findById(userId)) == null ? void 0 : _a.timezone) != null ? _b : "UTC";
159
+ };
160
+ const holidayRegion = () => {
161
+ var _a;
162
+ return (_a = settings.get("holiday_country")) != null ? _a : "DE";
163
+ };
164
+ const pauseMode = () => {
165
+ const stored = settings.get("pause_mode");
166
+ return stored === "punched" || stored === "staffel" ? stored : "auto";
167
+ };
168
+ const profileOf = (userId) => {
169
+ var _a;
170
+ return (_a = users.getWorkProfile(userId)) != null ? _a : defaultProfile(userId);
171
+ };
172
+ const absenceFor = (userId, date) => {
173
+ const covering = absences.withTypesInRange(userId, date, date);
174
+ let best = null;
175
+ for (const absence of covering) {
176
+ const share = Math.max(0, Math.min(100, absence.factor)) / 100 * absence.dayPortion;
177
+ const bestShare = best ? Math.max(0, Math.min(100, best.factor)) / 100 * best.dayPortion : -1;
178
+ if (share > bestShare || share === bestShare && best !== null && absence.id < best.id) {
179
+ best = absence;
180
+ }
181
+ }
182
+ return best;
183
+ };
184
+ const payoutsOf = (userId, year) => selectPayouts.get(userId, year).minutes;
185
+ const activeMonthsInYear = (profile, year, upToMonth, timeZone) => {
186
+ const startDate = profile.startDate == null ? null : (0, import_time.localDate)(profile.startDate, timeZone);
187
+ const endDate = profile.endDate == null ? null : (0, import_time.localDate)(profile.endDate, timeZone);
188
+ const limit = Math.max(0, Math.min(12, upToMonth));
189
+ let months = 0;
190
+ for (let month = 1; month <= limit; month++) {
191
+ const key = `${year}-${String(month).padStart(2, "0")}`;
192
+ const first = `${key}-01`;
193
+ const last = `${key}-${String((0, import_time.daysInMonth)(year, month)).padStart(2, "0")}`;
194
+ if ((startDate === null || last >= startDate) && (endDate === null || first <= endDate)) {
195
+ months++;
196
+ }
197
+ }
198
+ return months;
199
+ };
200
+ const overtimeStartFor = (userId, year, carryover) => {
201
+ const previous = selectYear.get(userId, year - 1);
202
+ return previous ? previous.overtime_min : carryover;
203
+ };
204
+ const overtimeAfterMonth = (args) => {
205
+ var _a;
206
+ const balances = [];
207
+ for (let index = 1; index <= args.month; index++) {
208
+ if (index === args.month && args.monthBalance !== null) {
209
+ balances.push(args.monthBalance);
210
+ continue;
211
+ }
212
+ const row = selectMonth.get(args.userId, args.year, index);
213
+ balances.push((_a = row == null ? void 0 : row.balance_min) != null ? _a : 0);
214
+ }
215
+ return (0, import_calculation.overtimeAfterPeriod)({
216
+ model: args.profile.overtimeModel,
217
+ monthBalances: balances,
218
+ overtimeStartMinutes: overtimeStartFor(args.userId, args.year, args.profile.overtimeCarryover),
219
+ vorholzeitPerYearMinutes: args.profile.vorholzeitPerYear,
220
+ activeMonths: activeMonthsInYear(args.profile, args.year, args.month, args.timeZone),
221
+ payoutMinutes: payoutsOf(args.userId, args.year),
222
+ completeYear: args.completeYear
223
+ }).overtimeMinutes;
224
+ };
225
+ const vacationDaysInRange = (args) => {
226
+ const year = Number(args.to.slice(0, 4));
227
+ const holidayDates = holidays.dateSet(year, holidayRegion());
228
+ const overlapping = absences.withTypesInRange(args.userId, args.from, args.to);
229
+ let days = 0;
230
+ for (const absence of overlapping) {
231
+ if (!absence.reduceVacation || absence.status !== args.status) {
232
+ continue;
233
+ }
234
+ const from = absence.dateFrom < args.from ? args.from : absence.dateFrom;
235
+ const to = absence.dateTo > args.to ? args.to : absence.dateTo;
236
+ if (from > to) {
237
+ continue;
238
+ }
239
+ days += (0, import_calculation.absenceDaysInRange)({
240
+ from,
241
+ to,
242
+ workdays: args.profile.workdays,
243
+ timeZone: args.timeZone,
244
+ holidays: holidayDates,
245
+ dayPortion: absence.dayPortion
246
+ });
247
+ }
248
+ return Math.round(days * 100) / 100;
249
+ };
250
+ const isCompleteYear = (year, todayDate, settled) => settled === true || Number(todayDate.slice(0, 4)) > year;
251
+ const calculateDayFor = (userId, localDate, now) => {
252
+ var _a;
253
+ const timeZone = timeZoneOf(userId);
254
+ const absence = absenceFor(userId, localDate);
255
+ const isHoliday = holidays.isHoliday(localDate, holidayRegion());
256
+ const profile = profileOf(userId);
257
+ const day = (0, import_calculation.calculateDay)({
258
+ profile,
259
+ entries: (0, import_entries.toPunchEntries)(entries.listByDate(userId, localDate)),
260
+ pauseRules: rules.pauseRules(userId),
261
+ pauseMode: pauseMode(),
262
+ pausePaidMinutes: profile.pausePaidMinutes,
263
+ timeZone,
264
+ localDate,
265
+ isHoliday,
266
+ absence: absence ? { factor: absence.factor, dayPortion: absence.dayPortion } : null
267
+ });
268
+ return {
269
+ userId,
270
+ localDate,
271
+ workedMin: day.workedMinutes,
272
+ breakMin: day.breakMinutes,
273
+ paidBreakMin: day.paidPauseMinutes,
274
+ targetMin: day.targetMinutes,
275
+ balanceMin: day.balanceMinutes,
276
+ absenceCode: (_a = absence == null ? void 0 : absence.typeCode) != null ? _a : null,
277
+ isHoliday,
278
+ firstInUtc: day.firstInUtc,
279
+ lastOutUtc: day.lastOutUtc,
280
+ hasOpenEntry: day.hasOpenEntry,
281
+ updatedAt: now
282
+ };
283
+ };
284
+ return {
285
+ recalculateDay(userId, localDate, options) {
286
+ var _a;
287
+ const now = (_a = options == null ? void 0 : options.now) != null ? _a : Math.floor(Date.now() / 1e3);
288
+ const record = calculateDayFor(userId, localDate, now);
289
+ const run = db.transaction(() => {
290
+ upsertDay.run(
291
+ record.userId,
292
+ record.localDate,
293
+ record.workedMin,
294
+ record.breakMin,
295
+ record.paidBreakMin,
296
+ record.targetMin,
297
+ record.balanceMin,
298
+ record.absenceCode,
299
+ record.isHoliday ? 1 : 0,
300
+ record.firstInUtc,
301
+ record.lastOutUtc,
302
+ record.hasOpenEntry ? 1 : 0,
303
+ record.updatedAt
304
+ );
305
+ });
306
+ run();
307
+ return record;
308
+ },
309
+ recalculateRange(userId, from, to, options) {
310
+ const totals = {
311
+ days: 0,
312
+ openDays: 0,
313
+ workedMin: 0,
314
+ breakMin: 0,
315
+ paidBreakMin: 0,
316
+ targetMin: 0,
317
+ balanceMin: 0
318
+ };
319
+ const run = db.transaction(() => {
320
+ for (const date of (0, import_time.dateRange)(from, to)) {
321
+ const record = this.recalculateDay(userId, date, options);
322
+ totals.days++;
323
+ totals.workedMin += record.workedMin;
324
+ totals.breakMin += record.breakMin;
325
+ totals.paidBreakMin += record.paidBreakMin;
326
+ totals.targetMin += record.targetMin;
327
+ totals.balanceMin += record.balanceMin;
328
+ totals.openDays += record.hasOpenEntry ? 1 : 0;
329
+ }
330
+ });
331
+ run();
332
+ return totals;
333
+ },
334
+ recalculateMonth(userId, year, month, options) {
335
+ var _a, _b;
336
+ if (!Number.isInteger(month) || month < 1 || month > 12) {
337
+ throw new import_errors.ValidationError(`month must be between 1 and 12 (got ${month})`);
338
+ }
339
+ const now = (_a = options == null ? void 0 : options.now) != null ? _a : Math.floor(Date.now() / 1e3);
340
+ const timeZone = timeZoneOf(userId);
341
+ const profile = profileOf(userId);
342
+ const upToDate = (_b = options == null ? void 0 : options.upToDate) != null ? _b : (0, import_time.localDate)(now, timeZone);
343
+ const key = `${year}-${String(month).padStart(2, "0")}`;
344
+ const firstDay = `${key}-01`;
345
+ const lastDay = `${key}-${String((0, import_time.daysInMonth)(year, month)).padStart(2, "0")}`;
346
+ const endDate = (options == null ? void 0 : options.includeFuture) === true || lastDay <= upToDate ? lastDay : upToDate;
347
+ let workedMin = 0;
348
+ let targetMin = 0;
349
+ let balanceMin = 0;
350
+ const run = db.transaction(() => {
351
+ for (const date of (0, import_time.dateRange)(firstDay, endDate)) {
352
+ const record = this.recalculateDay(userId, date, options);
353
+ workedMin += record.workedMin;
354
+ targetMin += record.targetMin;
355
+ balanceMin += record.balanceMin;
356
+ }
357
+ const vacationUsed = vacationDaysInRange({
358
+ userId,
359
+ from: firstDay,
360
+ to: lastDay,
361
+ status: "taken",
362
+ profile,
363
+ timeZone
364
+ });
365
+ const overtimeMin = overtimeAfterMonth({
366
+ userId,
367
+ year,
368
+ month,
369
+ monthBalance: balanceMin,
370
+ profile,
371
+ timeZone,
372
+ completeYear: isCompleteYear(year, upToDate, options == null ? void 0 : options.settled)
373
+ });
374
+ upsertMonth.run(userId, year, month, workedMin, targetMin, balanceMin, overtimeMin, vacationUsed, now);
375
+ });
376
+ run();
377
+ const saved = selectMonth.get(userId, year, month);
378
+ if (!saved) {
379
+ throw new Error(`month aggregate ${key} disappeared right after the write`);
380
+ }
381
+ return mapMonthRow(saved);
382
+ },
383
+ recalculateYear(userId, year, options) {
384
+ var _a, _b;
385
+ const now = (_a = options == null ? void 0 : options.now) != null ? _a : Math.floor(Date.now() / 1e3);
386
+ const timeZone = timeZoneOf(userId);
387
+ const profile = profileOf(userId);
388
+ const upToDate = (_b = options == null ? void 0 : options.upToDate) != null ? _b : (0, import_time.localDate)(now, timeZone);
389
+ const settled = isCompleteYear(year, upToDate, options == null ? void 0 : options.settled);
390
+ const yearStart = `${year}-01-01`;
391
+ const yearEnd = `${year}-12-31`;
392
+ let workedMin = 0;
393
+ let targetMin = 0;
394
+ let overtimeMin = 0;
395
+ const run = db.transaction(() => {
396
+ for (let month = 1; month <= 12; month++) {
397
+ const row = this.recalculateMonth(userId, year, month, options);
398
+ workedMin += row.workedMin;
399
+ targetMin += row.targetMin;
400
+ }
401
+ overtimeMin = overtimeAfterMonth({
402
+ userId,
403
+ year,
404
+ month: 12,
405
+ monthBalance: null,
406
+ profile,
407
+ timeZone,
408
+ completeYear: settled
409
+ });
410
+ const vacationDays = profile.vacationCarryover + profile.vacationPerYear;
411
+ const vacationUsed = vacationDaysInRange({
412
+ userId,
413
+ from: yearStart,
414
+ to: yearEnd,
415
+ status: "taken",
416
+ profile,
417
+ timeZone
418
+ });
419
+ const vacationPlanned = vacationDaysInRange({
420
+ userId,
421
+ from: yearStart,
422
+ to: yearEnd,
423
+ status: "planned",
424
+ profile,
425
+ timeZone
426
+ });
427
+ upsertYear.run(
428
+ userId,
429
+ year,
430
+ workedMin,
431
+ targetMin,
432
+ overtimeMin,
433
+ overtimeStartFor(userId, year, profile.overtimeCarryover),
434
+ Math.round(vacationDays * 100) / 100,
435
+ vacationUsed,
436
+ Math.round((vacationDays - vacationUsed) * 100) / 100,
437
+ vacationPlanned,
438
+ settled ? 1 : 0,
439
+ now
440
+ );
441
+ });
442
+ run();
443
+ const saved = selectYear.get(userId, year);
444
+ if (!saved) {
445
+ throw new Error(`year aggregate ${year} disappeared right after the write`);
446
+ }
447
+ return mapYearRow(saved);
448
+ },
449
+ day(userId, localDate) {
450
+ const row = selectDay.get(userId, localDate);
451
+ return row ? mapDayRow(row) : null;
452
+ },
453
+ days(userId, from, to) {
454
+ return selectDays.all(userId, from, to).map(mapDayRow);
455
+ },
456
+ month(userId, year, month) {
457
+ const row = selectMonth.get(userId, year, month);
458
+ return row ? mapMonthRow(row) : null;
459
+ },
460
+ year(userId, year) {
461
+ const row = selectYear.get(userId, year);
462
+ return row ? mapYearRow(row) : null;
463
+ },
464
+ recalculateLocalCaches(userId) {
465
+ const timeZone = timeZoneOf(userId);
466
+ const rows = selectEntryCache.all(userId);
467
+ let changed = 0;
468
+ const run = db.transaction(() => {
469
+ for (const row of rows) {
470
+ const tsLocal = (0, import_time.utcToWallTime)(row.tsUtc, timeZone);
471
+ const date = (0, import_time.localDate)(row.tsUtc, timeZone);
472
+ if (cacheMatches(db, row.id, tsLocal, date)) {
473
+ continue;
474
+ }
475
+ updateEntryCache.run(tsLocal, date, row.id);
476
+ changed++;
477
+ }
478
+ });
479
+ run();
480
+ return changed;
481
+ }
482
+ };
483
+ }
484
+ function cacheMatches(db, entryId, tsLocal, localDate) {
485
+ const row = db.prepare("SELECT ts_local AS tsLocal, local_date AS localDate FROM time_entries WHERE id = ?").get(entryId);
486
+ return row !== void 0 && row.tsLocal === tsLocal && row.localDate === localDate;
487
+ }
488
+ // Annotate the CommonJS export names for ESM import in node:
489
+ 0 && (module.exports = {
490
+ createAggregationService,
491
+ mapDayRow,
492
+ mapMonthRow,
493
+ mapYearRow
494
+ });
495
+ //# sourceMappingURL=aggregation.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": ["resolveLocalDate"]
7
+ }