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,180 @@
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 seed_exports = {};
20
+ __export(seed_exports, {
21
+ ABSENCE_TYPES: () => ABSENCE_TYPES,
22
+ ALL_PERMISSIONS: () => ALL_PERMISSIONS,
23
+ PERMISSIONS: () => PERMISSIONS,
24
+ ROLE_PERMISSIONS: () => ROLE_PERMISSIONS,
25
+ SETTING_DEFAULTS: () => SETTING_DEFAULTS,
26
+ ensureHolidaysForYear: () => ensureHolidaysForYear,
27
+ seed: () => seed
28
+ });
29
+ module.exports = __toCommonJS(seed_exports);
30
+ var import_holidays = require("../domain/holidays");
31
+ const PERMISSIONS = {
32
+ time: ["time.punch", "time.edit_own", "time.edit_other", "time.delete", "time.import", "time.resolve_conflict"],
33
+ absence: ["absence.request", "absence.approve", "absence.edit_other", "absence.manage_types"],
34
+ user: ["user.view", "user.create", "user.edit", "user.deactivate", "user.manage_roles"],
35
+ report: ["report.view_own", "report.view_other", "report.export", "report.statistics", "report.print_locked"],
36
+ payout: ["payout.view", "payout.create"],
37
+ system: [
38
+ "settings.view",
39
+ "settings.edit",
40
+ "rfid.manage",
41
+ "terminal.manage",
42
+ "holiday.manage",
43
+ "import.run",
44
+ "audit.view",
45
+ "backup.run"
46
+ ],
47
+ command: ["command.punch", "command.close_month", "command.recalc"]
48
+ };
49
+ const ALL_PERMISSIONS = Object.values(PERMISSIONS).flat();
50
+ const ROLE_PERMISSIONS = {
51
+ admin: ALL_PERMISSIONS,
52
+ manager: [
53
+ "time.punch",
54
+ "time.edit_own",
55
+ "time.edit_other",
56
+ "time.delete",
57
+ "time.resolve_conflict",
58
+ "absence.request",
59
+ "absence.approve",
60
+ "absence.edit_other",
61
+ "absence.manage_types",
62
+ "user.view",
63
+ "report.view_own",
64
+ "report.view_other",
65
+ "report.export",
66
+ "report.statistics",
67
+ "report.print_locked",
68
+ "payout.view",
69
+ "settings.view",
70
+ "holiday.manage",
71
+ "audit.view",
72
+ "command.punch",
73
+ "command.close_month",
74
+ "command.recalc"
75
+ ],
76
+ employee: ["time.punch", "time.edit_own", "absence.request", "report.view_own"]
77
+ };
78
+ const ABSENCE_TYPES = [
79
+ { code: "F", name: "Ferien", factor: 100, reduceVacation: true },
80
+ { code: "K", name: "Krankheit", factor: 100, reduceVacation: false },
81
+ { code: "U", name: "Unfall", factor: 100, reduceVacation: false },
82
+ { code: "M", name: "Milit\xE4r", factor: 100, reduceVacation: false },
83
+ { code: "I", name: "Intern", factor: 100, reduceVacation: false },
84
+ { code: "W", name: "Weiterbildung", factor: 50, reduceVacation: false },
85
+ { code: "E", name: "Extern", factor: 50, reduceVacation: false }
86
+ ];
87
+ const SETTING_DEFAULTS = {
88
+ edit_window_days: "7",
89
+ quick_round_minutes: "0",
90
+ absence_calc_until_today: "1",
91
+ absence_deduct_worktime: "0",
92
+ print_limit_days: "0",
93
+ holiday_country: "DE",
94
+ default_language: "de",
95
+ session_ttl_minutes: "720",
96
+ backup_retention_days: "30",
97
+ attendance_list_visible: "0",
98
+ // how the pause of a day is determined: `auto` = the punched break, otherwise the graduated rules
99
+ pause_mode: "auto",
100
+ // path of a Unicode font for the PDF statement; empty uses the built-in Latin fonts
101
+ report_font_path: "",
102
+ // branding of the installation: logo and background as data URLs (empty = not set), accent colour as hex
103
+ brand_logo: "",
104
+ brand_background: "",
105
+ brand_color: ""
106
+ };
107
+ function seed(db, options = {}) {
108
+ var _a, _b;
109
+ const country = (_a = options.holidayCountry) != null ? _a : "DE";
110
+ const currentYear = (/* @__PURE__ */ new Date()).getUTCFullYear();
111
+ const years = (_b = options.holidayYears) != null ? _b : [currentYear, currentYear + 1];
112
+ const run = db.transaction(() => {
113
+ const now = Math.floor(Date.now() / 1e3);
114
+ const insertRole = db.prepare("INSERT OR IGNORE INTO roles (key, name) VALUES (?, ?)");
115
+ insertRole.run("admin", "Administrator");
116
+ insertRole.run("manager", "Manager");
117
+ insertRole.run("employee", "Employee");
118
+ const insertPermission = db.prepare("INSERT OR IGNORE INTO permissions (key) VALUES (?)");
119
+ for (const permission of ALL_PERMISSIONS) {
120
+ insertPermission.run(permission);
121
+ }
122
+ const findRole = db.prepare("SELECT id FROM roles WHERE key = ?");
123
+ const findPermission = db.prepare("SELECT id FROM permissions WHERE key = ?");
124
+ const linkRole = db.prepare("INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)");
125
+ for (const [roleKey, permissions] of Object.entries(ROLE_PERMISSIONS)) {
126
+ const role = findRole.get(roleKey);
127
+ if (!role) {
128
+ continue;
129
+ }
130
+ for (const permission of permissions) {
131
+ const row = findPermission.get(permission);
132
+ if (row) {
133
+ linkRole.run(role.id, row.id);
134
+ }
135
+ }
136
+ }
137
+ const insertAbsenceType = db.prepare(
138
+ `INSERT OR IGNORE INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active)
139
+ VALUES (NULL, ?, ?, 1, ?, ?, 1)`
140
+ );
141
+ for (const type of ABSENCE_TYPES) {
142
+ insertAbsenceType.run(type.code, type.name, type.factor, type.reduceVacation ? 1 : 0);
143
+ }
144
+ const insertSetting = db.prepare(
145
+ "INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)"
146
+ );
147
+ for (const [key, value] of Object.entries(SETTING_DEFAULTS)) {
148
+ insertSetting.run(key, key === "holiday_country" ? country : value, now);
149
+ }
150
+ });
151
+ run();
152
+ let holidayCount = 0;
153
+ for (const year of years) {
154
+ holidayCount += ensureHolidaysForYear(db, country, year);
155
+ }
156
+ const permissionCount = db.prepare("SELECT COUNT(*) AS c FROM permissions").get().c;
157
+ return { permissions: permissionCount, holidays: holidayCount };
158
+ }
159
+ function ensureHolidaysForYear(db, country, year) {
160
+ const insert = db.prepare("INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)");
161
+ let inserted = 0;
162
+ const run = db.transaction(() => {
163
+ for (const holiday of (0, import_holidays.holidaysForYear)(year, country)) {
164
+ inserted += insert.run(country, year, holiday.date, holiday.name, holiday.key).changes;
165
+ }
166
+ });
167
+ run();
168
+ return inserted;
169
+ }
170
+ // Annotate the CommonJS export names for ESM import in node:
171
+ 0 && (module.exports = {
172
+ ABSENCE_TYPES,
173
+ ALL_PERMISSIONS,
174
+ PERMISSIONS,
175
+ ROLE_PERMISSIONS,
176
+ SETTING_DEFAULTS,
177
+ ensureHolidaysForYear,
178
+ seed
179
+ });
180
+ //# sourceMappingURL=seed.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/db/seed.ts"],
4
+ "sourcesContent": ["/**\n * Initial data (seeds): roles, permission catalogue, absence types, instance settings and holidays.\n *\n * All seed operations are idempotent (`INSERT OR IGNORE`), so `seed()` can run on every adapter start.\n */\n\nimport type { Db } from \"./database\";\nimport { holidaysForYear, type HolidayCountry } from \"../domain/holidays\";\n\n/** Permission catalogue (keys are stored in `permissions.key`). */\nexport const PERMISSIONS = {\n\ttime: [\"time.punch\", \"time.edit_own\", \"time.edit_other\", \"time.delete\", \"time.import\", \"time.resolve_conflict\"],\n\tabsence: [\"absence.request\", \"absence.approve\", \"absence.edit_other\", \"absence.manage_types\"],\n\tuser: [\"user.view\", \"user.create\", \"user.edit\", \"user.deactivate\", \"user.manage_roles\"],\n\treport: [\"report.view_own\", \"report.view_other\", \"report.export\", \"report.statistics\", \"report.print_locked\"],\n\tpayout: [\"payout.view\", \"payout.create\"],\n\tsystem: [\n\t\t\"settings.view\",\n\t\t\"settings.edit\",\n\t\t\"rfid.manage\",\n\t\t\"terminal.manage\",\n\t\t\"holiday.manage\",\n\t\t\"import.run\",\n\t\t\"audit.view\",\n\t\t\"backup.run\",\n\t],\n\tcommand: [\"command.punch\", \"command.close_month\", \"command.recalc\"],\n} as const;\n\nexport const ALL_PERMISSIONS: string[] = Object.values(PERMISSIONS).flat();\n\n/** Role \u2192 permissions (specification section 4.8). */\nexport const ROLE_PERMISSIONS: Record<string, string[]> = {\n\tadmin: ALL_PERMISSIONS,\n\tmanager: [\n\t\t\"time.punch\",\n\t\t\"time.edit_own\",\n\t\t\"time.edit_other\",\n\t\t\"time.delete\",\n\t\t\"time.resolve_conflict\",\n\t\t\"absence.request\",\n\t\t\"absence.approve\",\n\t\t\"absence.edit_other\",\n\t\t\"absence.manage_types\",\n\t\t\"user.view\",\n\t\t\"report.view_own\",\n\t\t\"report.view_other\",\n\t\t\"report.export\",\n\t\t\"report.statistics\",\n\t\t\"report.print_locked\",\n\t\t\"payout.view\",\n\t\t\"settings.view\",\n\t\t\"holiday.manage\",\n\t\t\"audit.view\",\n\t\t\"command.punch\",\n\t\t\"command.close_month\",\n\t\t\"command.recalc\",\n\t],\n\temployee: [\"time.punch\", \"time.edit_own\", \"absence.request\", \"report.view_own\"],\n};\n\n/** Default absence types of the time tracking: code, name, factor, reduces vacation. */\nexport const ABSENCE_TYPES: { code: string; name: string; factor: number; reduceVacation: boolean }[] = [\n\t{ code: \"F\", name: \"Ferien\", factor: 100, reduceVacation: true },\n\t{ code: \"K\", name: \"Krankheit\", factor: 100, reduceVacation: false },\n\t{ code: \"U\", name: \"Unfall\", factor: 100, reduceVacation: false },\n\t{ code: \"M\", name: \"Milit\u00E4r\", factor: 100, reduceVacation: false },\n\t{ code: \"I\", name: \"Intern\", factor: 100, reduceVacation: false },\n\t{ code: \"W\", name: \"Weiterbildung\", factor: 50, reduceVacation: false },\n\t{ code: \"E\", name: \"Extern\", factor: 50, reduceVacation: false },\n];\n\n/** Default instance settings (specification section 2.9.9); existing values are never overwritten. */\nexport const SETTING_DEFAULTS: Record<string, string> = {\n\tedit_window_days: \"7\",\n\tquick_round_minutes: \"0\",\n\tabsence_calc_until_today: \"1\",\n\tabsence_deduct_worktime: \"0\",\n\tprint_limit_days: \"0\",\n\tholiday_country: \"DE\",\n\tdefault_language: \"de\",\n\tsession_ttl_minutes: \"720\",\n\tbackup_retention_days: \"30\",\n\tattendance_list_visible: \"0\",\n\t// how the pause of a day is determined: `auto` = the punched break, otherwise the graduated rules\n\tpause_mode: \"auto\",\n\t// path of a Unicode font for the PDF statement; empty uses the built-in Latin fonts\n\treport_font_path: \"\",\n\t// branding of the installation: logo and background as data URLs (empty = not set), accent colour as hex\n\tbrand_logo: \"\",\n\tbrand_background: \"\",\n\tbrand_color: \"\",\n};\n\n/**\n * Options controlling which initial data is written.\n */\nexport interface SeedOptions {\n\t/** Country used to generate public holidays (default `CH`) */\n\tholidayCountry?: HolidayCountry;\n\t/** Years for which holidays are generated (default: current and next year) */\n\tholidayYears?: number[];\n}\n\n/**\n * Writes all initial data. Idempotent: repeated calls change nothing and never overwrite\n * user-modified settings.\n *\n * @param db - open database handle\n * @param options - seed options\n * @returns summary of what is now present\n */\nexport function seed(db: Db, options: SeedOptions = {}): { permissions: number; holidays: number } {\n\tconst country: HolidayCountry = options.holidayCountry ?? \"DE\";\n\tconst currentYear = new Date().getUTCFullYear();\n\tconst years = options.holidayYears ?? [currentYear, currentYear + 1];\n\n\tconst run = db.transaction((): void => {\n\t\tconst now = Math.floor(Date.now() / 1000);\n\n\t\tconst insertRole = db.prepare(\"INSERT OR IGNORE INTO roles (key, name) VALUES (?, ?)\");\n\t\tinsertRole.run(\"admin\", \"Administrator\");\n\t\tinsertRole.run(\"manager\", \"Manager\");\n\t\tinsertRole.run(\"employee\", \"Employee\");\n\n\t\tconst insertPermission = db.prepare(\"INSERT OR IGNORE INTO permissions (key) VALUES (?)\");\n\t\tfor (const permission of ALL_PERMISSIONS) {\n\t\t\tinsertPermission.run(permission);\n\t\t}\n\n\t\tconst findRole = db.prepare(\"SELECT id FROM roles WHERE key = ?\");\n\t\tconst findPermission = db.prepare(\"SELECT id FROM permissions WHERE key = ?\");\n\t\tconst linkRole = db.prepare(\"INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)\");\n\t\tfor (const [roleKey, permissions] of Object.entries(ROLE_PERMISSIONS)) {\n\t\t\tconst role = findRole.get(roleKey) as { id: number } | undefined;\n\t\t\tif (!role) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const permission of permissions) {\n\t\t\t\tconst row = findPermission.get(permission) as { id: number } | undefined;\n\t\t\t\tif (row) {\n\t\t\t\t\tlinkRole.run(role.id, row.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst insertAbsenceType = db.prepare(\n\t\t\t`INSERT OR IGNORE INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active)\n\t\t\t VALUES (NULL, ?, ?, 1, ?, ?, 1)`,\n\t\t);\n\t\tfor (const type of ABSENCE_TYPES) {\n\t\t\tinsertAbsenceType.run(type.code, type.name, type.factor, type.reduceVacation ? 1 : 0);\n\t\t}\n\n\t\tconst insertSetting = db.prepare(\n\t\t\t\"INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)\",\n\t\t);\n\t\tfor (const [key, value] of Object.entries(SETTING_DEFAULTS)) {\n\t\t\tinsertSetting.run(key, key === \"holiday_country\" ? country : value, now);\n\t\t}\n\t});\n\trun();\n\n\tlet holidayCount = 0;\n\tfor (const year of years) {\n\t\tholidayCount += ensureHolidaysForYear(db, country, year);\n\t}\n\n\tconst permissionCount = (db.prepare(\"SELECT COUNT(*) AS c FROM permissions\").get() as { c: number }).c;\n\treturn { permissions: permissionCount, holidays: holidayCount };\n}\n\n/**\n * Generates and stores the public holidays of one year for a country (idempotent).\n *\n * @param db - open database handle\n * @param country - supported country code\n * @param year - four digit year\n * @returns number of inserted holiday rows\n */\nexport function ensureHolidaysForYear(db: Db, country: HolidayCountry, year: number): number {\n\tconst insert = db.prepare(\"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\");\n\tlet inserted = 0;\n\tconst run = db.transaction((): void => {\n\t\tfor (const holiday of holidaysForYear(year, country)) {\n\t\t\tinserted += insert.run(country, year, holiday.date, holiday.name, holiday.key).changes;\n\t\t}\n\t});\n\trun();\n\treturn inserted;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,63 @@
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 avatar_exports = {};
20
+ __export(avatar_exports, {
21
+ AVATAR_TYPES: () => AVATAR_TYPES,
22
+ MAX_AVATAR_BYTES: () => MAX_AVATAR_BYTES,
23
+ MAX_BRANDING_BYTES: () => MAX_BRANDING_BYTES,
24
+ parseAvatarDataUrl: () => parseAvatarDataUrl,
25
+ readAvatar: () => readAvatar
26
+ });
27
+ module.exports = __toCommonJS(avatar_exports);
28
+ const AVATAR_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
29
+ const MAX_AVATAR_BYTES = 256 * 1024;
30
+ const MAX_BRANDING_BYTES = 512 * 1024;
31
+ const DATA_URL = /^data:([a-z]+\/[a-z0-9.+-]+);base64,([A-Za-z0-9+/=\r\n]+)$/i;
32
+ function parseAvatarDataUrl(value, maxBytes = MAX_AVATAR_BYTES) {
33
+ const match = DATA_URL.exec(value.trim());
34
+ if (!match) {
35
+ return null;
36
+ }
37
+ const contentType = match[1].toLowerCase();
38
+ if (!AVATAR_TYPES.includes(contentType)) {
39
+ return null;
40
+ }
41
+ const base64 = match[2].replace(/[\r\n]/g, "");
42
+ const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
43
+ const bytes = Math.floor(base64.length * 3 / 4) - padding;
44
+ if (bytes <= 0 || bytes > maxBytes) {
45
+ return null;
46
+ }
47
+ return { contentType, base64, bytes };
48
+ }
49
+ function readAvatar(stored) {
50
+ if (!stored) {
51
+ return null;
52
+ }
53
+ return parseAvatarDataUrl(stored);
54
+ }
55
+ // Annotate the CommonJS export names for ESM import in node:
56
+ 0 && (module.exports = {
57
+ AVATAR_TYPES,
58
+ MAX_AVATAR_BYTES,
59
+ MAX_BRANDING_BYTES,
60
+ parseAvatarDataUrl,
61
+ readAvatar
62
+ });
63
+ //# sourceMappingURL=avatar.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/domain/avatar.ts"],
4
+ "sourcesContent": ["/**\n * Picture of an employee (`users.avatar`).\n *\n * The picture is stored as a data URL (`data:image/png;base64,\u2026`) because it belongs to the account: it travels with\n * a backup and with the synchronisation between two hosts, and no file has to be kept in step with the database.\n * Only the API turns it into an image again (`GET /users/<id>/avatar`), so the JSON payloads stay small.\n *\n * A picture the agent can draw is not required: where none is stored, the web app shows the placeholder image.\n */\n\n/** Content types a picture may have. */\nexport const AVATAR_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\n\n/** Largest accepted picture (bytes of the decoded image). */\nexport const MAX_AVATAR_BYTES = 256 * 1024;\n\n/**\n * Largest accepted logo or background of the installation.\n *\n * A branding picture sits in a settings row instead of a user account and is delivered by its own route, so it may\n * be a little larger than the picture of an employee.\n */\nexport const MAX_BRANDING_BYTES = 512 * 1024;\n\n/** A checked picture. */\nexport interface AvatarImage {\n\t/** Content type of the image */\n\tcontentType: (typeof AVATAR_TYPES)[number];\n\t/** Base64 payload without the prefix, exactly as the data URL carries it */\n\tbase64: string;\n\t/** Size of the decoded image in bytes */\n\tbytes: number;\n}\n\n/** Matches a data URL of an image with a base64 payload. */\nconst DATA_URL = /^data:([a-z]+\\/[a-z0-9.+-]+);base64,([A-Za-z0-9+/=\\r\\n]+)$/i;\n\n/**\n * Checks a data URL that should become the picture of an employee.\n *\n * @param value - value as the administration sent it\n * @param maxBytes - largest accepted size of the decoded image (default: the picture of an employee)\n * @returns the checked image, or `null` when the value is not a usable picture\n */\nexport function parseAvatarDataUrl(value: string, maxBytes: number = MAX_AVATAR_BYTES): AvatarImage | null {\n\tconst match = DATA_URL.exec(value.trim());\n\tif (!match) {\n\t\treturn null;\n\t}\n\n\tconst contentType = match[1].toLowerCase();\n\tif (!(AVATAR_TYPES as readonly string[]).includes(contentType)) {\n\t\treturn null;\n\t}\n\n\t// the payload is the decoded image: 4 base64 characters carry 3 bytes, and padding shrinks it\n\tconst base64 = match[2].replace(/[\\r\\n]/g, \"\");\n\tconst padding = base64.endsWith(\"==\") ? 2 : base64.endsWith(\"=\") ? 1 : 0;\n\tconst bytes = Math.floor((base64.length * 3) / 4) - padding;\n\tif (bytes <= 0 || bytes > maxBytes) {\n\t\treturn null;\n\t}\n\n\treturn { contentType: contentType as AvatarImage[\"contentType\"], base64, bytes };\n}\n\n/**\n * Turns a stored data URL into an image and its content type.\n *\n * @param stored - value as it stands in the database\n * @returns the image, or `null` when nothing usable is stored\n */\nexport function readAvatar(stored: string | null | undefined): AvatarImage | null {\n\tif (!stored) {\n\t\treturn null;\n\t}\n\treturn parseAvatarDataUrl(stored);\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWO,MAAM,eAAe,CAAC,aAAa,cAAc,cAAc,WAAW;AAG1E,MAAM,mBAAmB,MAAM;AAQ/B,MAAM,qBAAqB,MAAM;AAaxC,MAAM,WAAW;AASV,SAAS,mBAAmB,OAAe,WAAmB,kBAAsC;AAC1G,QAAM,QAAQ,SAAS,KAAK,MAAM,KAAK,CAAC;AACxC,MAAI,CAAC,OAAO;AACX,WAAO;AAAA,EACR;AAEA,QAAM,cAAc,MAAM,CAAC,EAAE,YAAY;AACzC,MAAI,CAAE,aAAmC,SAAS,WAAW,GAAG;AAC/D,WAAO;AAAA,EACR;AAGA,QAAM,SAAS,MAAM,CAAC,EAAE,QAAQ,WAAW,EAAE;AAC7C,QAAM,UAAU,OAAO,SAAS,IAAI,IAAI,IAAI,OAAO,SAAS,GAAG,IAAI,IAAI;AACvE,QAAM,QAAQ,KAAK,MAAO,OAAO,SAAS,IAAK,CAAC,IAAI;AACpD,MAAI,SAAS,KAAK,QAAQ,UAAU;AACnC,WAAO;AAAA,EACR;AAEA,SAAO,EAAE,aAAwD,QAAQ,MAAM;AAChF;AAQO,SAAS,WAAW,QAAuD;AACjF,MAAI,CAAC,QAAQ;AACZ,WAAO;AAAA,EACR;AACA,SAAO,mBAAmB,MAAM;AACjC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,70 @@
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 breaks_exports = {};
20
+ __export(breaks_exports, {
21
+ applyPauses: () => applyPauses,
22
+ effectivePause: () => effectivePause,
23
+ pauseRuleForPair: () => pauseRuleForPair
24
+ });
25
+ module.exports = __toCommonJS(breaks_exports);
26
+ function pauseRuleForPair(pairMinutes, rules) {
27
+ const active = rules.filter((rule) => rule.isActive !== false && rule.pauseMin > 0).slice().sort((a, b) => a.fromMin - b.fromMin);
28
+ for (const rule of active) {
29
+ const withinStart = pairMinutes >= rule.fromMin;
30
+ const withinEnd = rule.toMin == null || pairMinutes < rule.toMin;
31
+ if (withinStart && withinEnd) {
32
+ return rule;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ function applyPauses(pairs, rules) {
38
+ const perPair = pairs.map((pair, index) => {
39
+ const rule = pauseRuleForPair(pair.minutes, rules);
40
+ const pauseMin = rule ? Math.min(rule.pauseMin, pair.minutes) : 0;
41
+ return { pairIndex: index, pairMinutes: pair.minutes, pauseMin, rule };
42
+ });
43
+ const grossMinutes = perPair.reduce((sum, entry) => sum + entry.pairMinutes, 0);
44
+ const breakMinutes = perPair.reduce((sum, entry) => sum + entry.pauseMin, 0);
45
+ return {
46
+ perPair,
47
+ breakMinutes,
48
+ grossMinutes,
49
+ workedMinutes: grossMinutes - breakMinutes
50
+ };
51
+ }
52
+ function effectivePause(input) {
53
+ const punched = input.gapMinutes > 0 && input.hasOpenEntry !== true;
54
+ if (punched) {
55
+ if (input.mode === "punched" || input.mode === "auto") {
56
+ return { pauseMinutes: input.gapMinutes, source: "punched" };
57
+ }
58
+ }
59
+ if (input.mode === "punched") {
60
+ return { pauseMinutes: 0, source: "none" };
61
+ }
62
+ return input.staffelMinutes > 0 ? { pauseMinutes: input.staffelMinutes, source: "staffel" } : { pauseMinutes: 0, source: "none" };
63
+ }
64
+ // Annotate the CommonJS export names for ESM import in node:
65
+ 0 && (module.exports = {
66
+ applyPauses,
67
+ effectivePause,
68
+ pauseRuleForPair
69
+ });
70
+ //# sourceMappingURL=breaks.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/domain/breaks.ts"],
4
+ "sourcesContent": ["/**\n * Break rules (graduated pauses).\n *\n * A break is deducted **per punch pair**, not once per day: every complete pair is\n * checked against the graduated rules, so a day with several pairs can trigger several deductions.\n */\n\n/** A graduated break rule: from a pair duration of `fromMin` minutes, `pauseMin` minutes are deducted. */\nexport interface PauseRule {\n\t/** Rule applies from this pair duration in minutes (inclusive) */\n\tfromMin: number;\n\t/** Rule applies below this pair duration in minutes (exclusive); `null` = open end */\n\ttoMin?: number | null;\n\t/** Minutes deducted from the pair */\n\tpauseMin: number;\n\t/** Inactive rules are ignored */\n\tisActive?: boolean;\n}\n\n/** Break deduction of a single pair. */\nexport interface PairBreak {\n\t/** Index of the pair inside the day */\n\tpairIndex: number;\n\t/** Gross duration of the pair in minutes */\n\tpairMinutes: number;\n\t/** Deducted break minutes */\n\tpauseMin: number;\n\t/** Rule that matched, `null` when no rule applied */\n\trule: PauseRule | null;\n}\n\n/** Result of applying the break rules to all pairs of a day. */\nexport interface BreakResult {\n\t/** Deduction per pair in chronological order */\n\tperPair: PairBreak[];\n\t/** Total deducted minutes */\n\tbreakMinutes: number;\n\t/** Sum of the pair durations before deductions */\n\tgrossMinutes: number;\n\t/** Sum of the pair durations after deductions */\n\tworkedMinutes: number;\n}\n\n/**\n * Finds the first matching active rule for a pair duration (rules are sorted by `fromMin`).\n *\n * @param pairMinutes - gross duration of the pair in minutes\n * @param rules - graduated rules\n * @returns matching rule or `null`\n */\nexport function pauseRuleForPair(pairMinutes: number, rules: PauseRule[]): PauseRule | null {\n\tconst active = rules\n\t\t.filter(rule => rule.isActive !== false && rule.pauseMin > 0)\n\t\t.slice()\n\t\t.sort((a, b) => a.fromMin - b.fromMin);\n\n\tfor (const rule of active) {\n\t\tconst withinStart = pairMinutes >= rule.fromMin;\n\t\tconst withinEnd = rule.toMin == null || pairMinutes < rule.toMin;\n\t\tif (withinStart && withinEnd) {\n\t\t\treturn rule;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Applies the break rules to all pairs of one day.\n *\n * @param pairs - pairs with their gross duration in minutes\n * @param rules - graduated rules\n * @returns per-pair deductions and the totals\n */\nexport function applyPauses(pairs: { minutes: number }[], rules: PauseRule[]): BreakResult {\n\tconst perPair: PairBreak[] = pairs.map((pair, index) => {\n\t\tconst rule = pauseRuleForPair(pair.minutes, rules);\n\t\t// never deduct more than the pair itself\n\t\tconst pauseMin = rule ? Math.min(rule.pauseMin, pair.minutes) : 0;\n\t\treturn { pairIndex: index, pairMinutes: pair.minutes, pauseMin, rule };\n\t});\n\n\tconst grossMinutes = perPair.reduce((sum, entry) => sum + entry.pairMinutes, 0);\n\tconst breakMinutes = perPair.reduce((sum, entry) => sum + entry.pauseMin, 0);\n\n\treturn {\n\t\tperPair,\n\t\tbreakMinutes,\n\t\tgrossMinutes,\n\t\tworkedMinutes: grossMinutes - breakMinutes,\n\t};\n}\n\n/** How the pause of a day is determined. */\nexport type PauseMode = \"auto\" | \"punched\" | \"staffel\";\n\n/** Where the pause of a day came from. */\nexport type PauseSource = \"punched\" | \"staffel\" | \"none\";\n\n/**\n * Decides which pause a day carries.\n *\n * - `punched`: the breaks the employee punched (the time between two pairs of punches)\n * - `staffel`: the graduated rules, deducted per pair\n * - `auto`: the punched breaks as soon as there are any, otherwise the rules \u2014 so a day without a punched break\n * still gets its statutory deduction\n *\n * A day with an open punch never counts as measured: its breaks are not final yet, so the rules apply.\n *\n * @param input - mode, measured break and rule deduction of one day\n * @param input.mode - chosen mode of the instance\n * @param input.gapMinutes - minutes between the pairs of the day\n * @param input.staffelMinutes - minutes the graduated rules deduct\n * @param input.hasOpenEntry - true while the day has an open punch\n * @returns the pause of the day and where it came from\n */\nexport function effectivePause(input: {\n\tmode: PauseMode;\n\tgapMinutes: number;\n\tstaffelMinutes: number;\n\thasOpenEntry?: boolean;\n}): { pauseMinutes: number; source: PauseSource } {\n\tconst punched = input.gapMinutes > 0 && input.hasOpenEntry !== true;\n\tif (punched) {\n\t\tif (input.mode === \"punched\" || input.mode === \"auto\") {\n\t\t\treturn { pauseMinutes: input.gapMinutes, source: \"punched\" };\n\t\t}\n\t}\n\tif (input.mode === \"punched\") {\n\t\t// measured breaks only: a day without them carries no pause at all\n\t\treturn { pauseMinutes: 0, source: \"none\" };\n\t}\n\treturn input.staffelMinutes > 0\n\t\t? { pauseMinutes: input.staffelMinutes, source: \"staffel\" }\n\t\t: { pauseMinutes: 0, source: \"none\" };\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkDO,SAAS,iBAAiB,aAAqB,OAAsC;AAC3F,QAAM,SAAS,MACb,OAAO,UAAQ,KAAK,aAAa,SAAS,KAAK,WAAW,CAAC,EAC3D,MAAM,EACN,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO;AAEtC,aAAW,QAAQ,QAAQ;AAC1B,UAAM,cAAc,eAAe,KAAK;AACxC,UAAM,YAAY,KAAK,SAAS,QAAQ,cAAc,KAAK;AAC3D,QAAI,eAAe,WAAW;AAC7B,aAAO;AAAA,IACR;AAAA,EACD;AACA,SAAO;AACR;AASO,SAAS,YAAY,OAA8B,OAAiC;AAC1F,QAAM,UAAuB,MAAM,IAAI,CAAC,MAAM,UAAU;AACvD,UAAM,OAAO,iBAAiB,KAAK,SAAS,KAAK;AAEjD,UAAM,WAAW,OAAO,KAAK,IAAI,KAAK,UAAU,KAAK,OAAO,IAAI;AAChE,WAAO,EAAE,WAAW,OAAO,aAAa,KAAK,SAAS,UAAU,KAAK;AAAA,EACtE,CAAC;AAED,QAAM,eAAe,QAAQ,OAAO,CAAC,KAAK,UAAU,MAAM,MAAM,aAAa,CAAC;AAC9E,QAAM,eAAe,QAAQ,OAAO,CAAC,KAAK,UAAU,MAAM,MAAM,UAAU,CAAC;AAE3E,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,eAAe;AAAA,EAC/B;AACD;AAyBO,SAAS,eAAe,OAKmB;AACjD,QAAM,UAAU,MAAM,aAAa,KAAK,MAAM,iBAAiB;AAC/D,MAAI,SAAS;AACZ,QAAI,MAAM,SAAS,aAAa,MAAM,SAAS,QAAQ;AACtD,aAAO,EAAE,cAAc,MAAM,YAAY,QAAQ,UAAU;AAAA,IAC5D;AAAA,EACD;AACA,MAAI,MAAM,SAAS,WAAW;AAE7B,WAAO,EAAE,cAAc,GAAG,QAAQ,OAAO;AAAA,EAC1C;AACA,SAAO,MAAM,iBAAiB,IAC3B,EAAE,cAAc,MAAM,gBAAgB,QAAQ,UAAU,IACxD,EAAE,cAAc,GAAG,QAAQ,OAAO;AACtC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,150 @@
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 calculation_exports = {};
20
+ __export(calculation_exports, {
21
+ absenceDaysInRange: () => absenceDaysInRange,
22
+ calculateDay: () => calculateDay,
23
+ overtimeAfterPeriod: () => overtimeAfterPeriod,
24
+ overtimeOfPeriod: () => overtimeOfPeriod,
25
+ sumPeriod: () => sumPeriod,
26
+ vacationBalance: () => vacationBalance,
27
+ vorholzeitForActiveMonths: () => vorholzeitForActiveMonths
28
+ });
29
+ module.exports = __toCommonJS(calculation_exports);
30
+ var import_breaks = require("./breaks");
31
+ var import_punch = require("./punch");
32
+ var import_target = require("./target");
33
+ var import_time = require("../util/time");
34
+ function calculateDay(input) {
35
+ var _a, _b, _c;
36
+ const day = (0, import_punch.buildDayPunches)(input.entries, input.pairing);
37
+ const breaks = (0, import_breaks.applyPauses)(day.pairs, (_a = input.pauseRules) != null ? _a : []);
38
+ const weekday = (0, import_time.dayOfWeek)(input.localDate, input.timeZone);
39
+ const startDate = input.profile.startDate == null ? null : (0, import_time.localDate)(input.profile.startDate, input.timeZone);
40
+ const endDate = input.profile.endDate == null ? null : (0, import_time.localDate)(input.profile.endDate, input.timeZone);
41
+ const targetMinutes = (0, import_target.targetForDay)({
42
+ profile: input.profile,
43
+ weekday,
44
+ isHoliday: input.isHoliday,
45
+ absence: input.absence,
46
+ isBeforeStart: startDate !== null && input.localDate < startDate,
47
+ isAfterEnd: endDate !== null && input.localDate > endDate
48
+ });
49
+ const pause = (0, import_breaks.effectivePause)({
50
+ mode: (_b = input.pauseMode) != null ? _b : "auto",
51
+ gapMinutes: day.gapMinutes,
52
+ staffelMinutes: breaks.breakMinutes,
53
+ hasOpenEntry: day.hasOpenEntry
54
+ });
55
+ const paidPauseMinutes = Math.min(pause.pauseMinutes, Math.max(0, (_c = input.pausePaidMinutes) != null ? _c : 0));
56
+ const workedMinutes = day.spanMinutes - pause.pauseMinutes + paidPauseMinutes;
57
+ return {
58
+ localDate: input.localDate,
59
+ weekday,
60
+ pairs: day.pairs,
61
+ hasOpenEntry: day.hasOpenEntry,
62
+ firstInUtc: day.firstInUtc,
63
+ lastOutUtc: day.lastOutUtc,
64
+ spanMinutes: day.spanMinutes,
65
+ grossMinutes: breaks.grossMinutes,
66
+ breakMinutes: pause.pauseMinutes,
67
+ paidPauseMinutes,
68
+ pauseSource: pause.source,
69
+ workedMinutes,
70
+ targetMinutes,
71
+ balanceMinutes: workedMinutes - targetMinutes,
72
+ breaks
73
+ };
74
+ }
75
+ function sumPeriod(days) {
76
+ return days.reduce(
77
+ (totals, day) => ({
78
+ workedMinutes: totals.workedMinutes + day.workedMinutes,
79
+ targetMinutes: totals.targetMinutes + day.targetMinutes,
80
+ balanceMinutes: totals.balanceMinutes + day.balanceMinutes,
81
+ days: totals.days + 1,
82
+ openDays: totals.openDays + (day.hasOpenEntry ? 1 : 0)
83
+ }),
84
+ { workedMinutes: 0, targetMinutes: 0, balanceMinutes: 0, days: 0, openDays: 0 }
85
+ );
86
+ }
87
+ function overtimeOfPeriod(monthBalances) {
88
+ return monthBalances.reduce((sum, balance) => sum + balance, 0);
89
+ }
90
+ function vorholzeitForActiveMonths(vorholzeitPerYearMinutes, activeMonths) {
91
+ const months = Math.max(0, Math.min(12, activeMonths));
92
+ return Math.round(vorholzeitPerYearMinutes / 12 * months);
93
+ }
94
+ function overtimeAfterPeriod(input) {
95
+ var _a, _b, _c;
96
+ const monthOvertimeMinutes = overtimeOfPeriod(input.monthBalances);
97
+ const vorholzeitMinutes = vorholzeitForActiveMonths(
98
+ (_a = input.vorholzeitPerYearMinutes) != null ? _a : 0,
99
+ (_b = input.activeMonths) != null ? _b : input.monthBalances.length
100
+ );
101
+ if (input.model === "yearly" && input.completeYear !== true) {
102
+ return {
103
+ monthOvertimeMinutes,
104
+ vorholzeitMinutes: 0,
105
+ overtimeMinutes: input.overtimeStartMinutes,
106
+ pending: true
107
+ };
108
+ }
109
+ const overtimeMinutes = input.overtimeStartMinutes + monthOvertimeMinutes - vorholzeitMinutes - ((_c = input.payoutMinutes) != null ? _c : 0);
110
+ return { monthOvertimeMinutes, vorholzeitMinutes, overtimeMinutes, pending: false };
111
+ }
112
+ function vacationBalance(input) {
113
+ var _a;
114
+ const entitlementDays = roundDays(input.carryoverDays + input.perYearDays);
115
+ const leftDays = roundDays(entitlementDays - input.usedDays);
116
+ const plannedDays = roundDays((_a = input.plannedDays) != null ? _a : 0);
117
+ return { entitlementDays, leftDays, availableDays: roundDays(leftDays - plannedDays), plannedDays };
118
+ }
119
+ function absenceDaysInRange(input) {
120
+ var _a, _b;
121
+ const portion = (_a = input.dayPortion) != null ? _a : 1;
122
+ if (!(portion > 0)) {
123
+ return 0;
124
+ }
125
+ let days = 0;
126
+ for (const date of (0, import_time.dateRange)(input.from, input.to)) {
127
+ if (!(0, import_target.isWorkdayOn)(input.workdays, (0, import_time.dayOfWeek)(date, input.timeZone))) {
128
+ continue;
129
+ }
130
+ if ((_b = input.holidays) == null ? void 0 : _b.has(date)) {
131
+ continue;
132
+ }
133
+ days += portion;
134
+ }
135
+ return roundDays(days);
136
+ }
137
+ function roundDays(value) {
138
+ return Math.round(value * 100) / 100;
139
+ }
140
+ // Annotate the CommonJS export names for ESM import in node:
141
+ 0 && (module.exports = {
142
+ absenceDaysInRange,
143
+ calculateDay,
144
+ overtimeAfterPeriod,
145
+ overtimeOfPeriod,
146
+ sumPeriod,
147
+ vacationBalance,
148
+ vorholzeitForActiveMonths
149
+ });
150
+ //# sourceMappingURL=calculation.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/domain/calculation.ts"],
4
+ "sourcesContent": ["/**\n * Calculation service: day balance, monthly sums, overtime models and vacation.\n *\n * The formulas follow the internal specification (section 3.3\u20133.6): balance = worked \u2212 break \u2212 target,\n * overtime is carried per month (or yearly), the annual \u201CVorholzeit\u201D is credited monthly, payouts reduce\n * the overtime balance.\n */\n\nimport {\n\tapplyPauses,\n\teffectivePause,\n\ttype BreakResult,\n\ttype PauseMode,\n\ttype PauseRule,\n\ttype PauseSource,\n} from \"./breaks\";\nimport { buildDayPunches, type PairingOptions, type PunchEntry, type PunchPair } from \"./punch\";\nimport { isWorkdayOn, targetForDay, type AbsenceImpact, type WorkProfile } from \"./target\";\nimport { dateRange, dayOfWeek, localDate } from \"../util/time\";\n\n/** Supported overtime models (`work_profiles.overtime_model`). */\nexport type OvertimeModel = \"cumulative\" | \"yearly\" | \"monthly\";\n\n/** Input for the calculation of one day. */\nexport interface DayCalculationInput {\n\t/** Employment parameters */\n\tprofile: WorkProfile;\n\t/** Punches of that day */\n\tentries: PunchEntry[];\n\t/** Graduated break rules */\n\tpauseRules?: PauseRule[];\n\t/** How the pause is determined (default `auto`: punched break, otherwise the rules) */\n\tpauseMode?: PauseMode;\n\t/** Minutes of the pause per day that are credited as working time (0 = the pause is not paid) */\n\tpausePaidMinutes?: number;\n\t/** Time zone of the employee */\n\ttimeZone: string;\n\t/** Local date to calculate */\n\tlocalDate: string;\n\t/** True when the day is a public holiday */\n\tisHoliday?: boolean;\n\t/** Absence covering the day, if any */\n\tabsence?: AbsenceImpact | null;\n\t/** Pairing options (duplicate protection) */\n\tpairing?: PairingOptions;\n}\n\n/** Result of the calculation of one day. */\nexport interface DayCalculation {\n\t/** Local date the calculation belongs to */\n\tlocalDate: string;\n\t/** Weekday, 0 = Sunday */\n\tweekday: number;\n\t/** Pairs of the day in chronological order */\n\tpairs: PunchPair[];\n\t/** True when the last punch has no counterpart yet */\n\thasOpenEntry: boolean;\n\t/** Instant of the first punch of the day */\n\tfirstInUtc: number | null;\n\t/** Instant of the last completed clock-out, `null` while a punch is open */\n\tlastOutUtc: number | null;\n\t/** Minutes the employee was present (first punch to the last completed one) */\n\tspanMinutes: number;\n\t/** Pair durations before break deduction */\n\tgrossMinutes: number;\n\t/** Pause of the day (punched break or rule deduction) */\n\tbreakMinutes: number;\n\t/** Part of the pause that is paid and credited as working time */\n\tpaidPauseMinutes: number;\n\t/** Where the pause came from */\n\tpauseSource: PauseSource;\n\t/** Net working time */\n\tworkedMinutes: number;\n\t/** Target time of the day */\n\ttargetMinutes: number;\n\t/** workedMinutes \u2212 targetMinutes */\n\tbalanceMinutes: number;\n\t/** Detailed break calculation */\n\tbreaks: BreakResult;\n}\n\n/** Aggregated values of a period (month or year). */\nexport interface PeriodTotals {\n\t/** Net working time of the period */\n\tworkedMinutes: number;\n\t/** Target time of the period */\n\ttargetMinutes: number;\n\t/** workedMinutes \u2212 targetMinutes */\n\tbalanceMinutes: number;\n\t/** Number of days in the period */\n\tdays: number;\n\t/** Number of days with an open punch */\n\topenDays: number;\n}\n\n/**\n * Calculates the balance of one day.\n *\n * @param input - day calculation input\n * @returns day calculation result\n */\nexport function calculateDay(input: DayCalculationInput): DayCalculation {\n\tconst day = buildDayPunches(input.entries, input.pairing);\n\tconst breaks = applyPauses(day.pairs, input.pauseRules ?? []);\n\tconst weekday = dayOfWeek(input.localDate, input.timeZone);\n\n\tconst startDate = input.profile.startDate == null ? null : localDate(input.profile.startDate, input.timeZone);\n\tconst endDate = input.profile.endDate == null ? null : localDate(input.profile.endDate, input.timeZone);\n\n\tconst targetMinutes = targetForDay({\n\t\tprofile: input.profile,\n\t\tweekday,\n\t\tisHoliday: input.isHoliday,\n\t\tabsence: input.absence,\n\t\tisBeforeStart: startDate !== null && input.localDate < startDate,\n\t\tisAfterEnd: endDate !== null && input.localDate > endDate,\n\t});\n\n\t// Which pause the day carries: the punched break or the graduated rules (see `effectivePause`). The presence\n\t// carries that pause \u2014 and the part of it that is paid is credited, so it stays part of the working time.\n\tconst pause = effectivePause({\n\t\tmode: input.pauseMode ?? \"auto\",\n\t\tgapMinutes: day.gapMinutes,\n\t\tstaffelMinutes: breaks.breakMinutes,\n\t\thasOpenEntry: day.hasOpenEntry,\n\t});\n\tconst paidPauseMinutes = Math.min(pause.pauseMinutes, Math.max(0, input.pausePaidMinutes ?? 0));\n\tconst workedMinutes = day.spanMinutes - pause.pauseMinutes + paidPauseMinutes;\n\n\treturn {\n\t\tlocalDate: input.localDate,\n\t\tweekday,\n\t\tpairs: day.pairs,\n\t\thasOpenEntry: day.hasOpenEntry,\n\t\tfirstInUtc: day.firstInUtc,\n\t\tlastOutUtc: day.lastOutUtc,\n\t\tspanMinutes: day.spanMinutes,\n\t\tgrossMinutes: breaks.grossMinutes,\n\t\tbreakMinutes: pause.pauseMinutes,\n\t\tpaidPauseMinutes,\n\t\tpauseSource: pause.source,\n\t\tworkedMinutes,\n\t\ttargetMinutes,\n\t\tbalanceMinutes: workedMinutes - targetMinutes,\n\t\tbreaks,\n\t};\n}\n\n/**\n * Sums up the days of a period.\n *\n * @param days - calculated days\n * @returns totals of the period\n */\nexport function sumPeriod(days: DayCalculation[]): PeriodTotals {\n\treturn days.reduce<PeriodTotals>(\n\t\t(totals, day) => ({\n\t\t\tworkedMinutes: totals.workedMinutes + day.workedMinutes,\n\t\t\ttargetMinutes: totals.targetMinutes + day.targetMinutes,\n\t\t\tbalanceMinutes: totals.balanceMinutes + day.balanceMinutes,\n\t\t\tdays: totals.days + 1,\n\t\t\topenDays: totals.openDays + (day.hasOpenEntry ? 1 : 0),\n\t\t}),\n\t\t{ workedMinutes: 0, targetMinutes: 0, balanceMinutes: 0, days: 0, openDays: 0 },\n\t);\n}\n\n/**\n * Sums the monthly balances to the overtime of the period.\n *\n * @param monthBalances - balance per month in minutes\n * @returns overtime of the period in minutes\n */\nexport function overtimeOfPeriod(monthBalances: number[]): number {\n\treturn monthBalances.reduce((sum, balance) => sum + balance, 0);\n}\n\n/**\n * Annual \u201CVorholzeit\u201D credited for the active months.\n *\n * @param vorholzeitPerYearMinutes - annual Vorholzeit in minutes\n * @param activeMonths - number of active months in the period\n * @returns credited minutes (rounded)\n */\nexport function vorholzeitForActiveMonths(vorholzeitPerYearMinutes: number, activeMonths: number): number {\n\tconst months = Math.max(0, Math.min(12, activeMonths));\n\treturn Math.round((vorholzeitPerYearMinutes / 12) * months);\n}\n\n/** Input of the overtime calculation. */\nexport interface OvertimeInput {\n\t/** Overtime model of the employee */\n\tmodel: OvertimeModel;\n\t/** Balance per month in minutes (index 0 = January, gaps are `0`) */\n\tmonthBalances: number[];\n\t/** Carryover at the beginning of the period (from the previous year) */\n\tovertimeStartMinutes: number;\n\t/** Annual \u201CVorholzeit\u201D in minutes (`work_profiles.vorholzeit_per_year`) */\n\tvorholzeitPerYearMinutes?: number;\n\t/** Active months inside the period (defaults to the number of given months) */\n\tactiveMonths?: number;\n\t/** Overtime paid out in the period (reduces the balance) */\n\tpayoutMinutes?: number;\n\t/** True when the period is a complete calendar year (January to December) */\n\tcompleteYear?: boolean;\n}\n\n/** Result of the overtime calculation. */\nexport interface OvertimeResult {\n\t/** Sum of the monthly balances of the period */\n\tmonthOvertimeMinutes: number;\n\t/** Credited \u201CVorholzeit\u201D of the period */\n\tvorholzeitMinutes: number;\n\t/** Carried overtime after the period */\n\tovertimeMinutes: number;\n\t/** True when the yearly model has not reached its calculation point yet */\n\tpending: boolean;\n}\n\n/**\n * Carried overtime after a period.\n *\n * - `monthly`: balance is carried month by month.\n * - `cumulative`: identical to `monthly`, but the caller passes the running value as `overtimeStartMinutes`\n * so the balance survives the year boundary.\n * - `yearly`: the balance is only computed at the end of the calendar year; until then the carryover is\n * reported unchanged (`pending = true`).\n *\n * @param input - overtime calculation input\n * @returns carried overtime plus the intermediate values\n */\nexport function overtimeAfterPeriod(input: OvertimeInput): OvertimeResult {\n\tconst monthOvertimeMinutes = overtimeOfPeriod(input.monthBalances);\n\tconst vorholzeitMinutes = vorholzeitForActiveMonths(\n\t\tinput.vorholzeitPerYearMinutes ?? 0,\n\t\tinput.activeMonths ?? input.monthBalances.length,\n\t);\n\n\tif (input.model === \"yearly\" && input.completeYear !== true) {\n\t\treturn {\n\t\t\tmonthOvertimeMinutes,\n\t\t\tvorholzeitMinutes: 0,\n\t\t\tovertimeMinutes: input.overtimeStartMinutes,\n\t\t\tpending: true,\n\t\t};\n\t}\n\n\tconst overtimeMinutes =\n\t\tinput.overtimeStartMinutes + monthOvertimeMinutes - vorholzeitMinutes - (input.payoutMinutes ?? 0);\n\n\treturn { monthOvertimeMinutes, vorholzeitMinutes, overtimeMinutes, pending: false };\n}\n\n/** Input of the vacation (holiday) balance. */\nexport interface VacationInput {\n\t/** Days carried over from the previous year */\n\tcarryoverDays: number;\n\t/** Annual entitlement in days */\n\tperYearDays: number;\n\t/** Days already taken */\n\tusedDays: number;\n\t/** Days approved but not taken yet (reported separately) */\n\tplannedDays?: number;\n}\n\n/** Vacation balance of a year. */\nexport interface VacationResult {\n\t/** Entitlement including the carryover */\n\tentitlementDays: number;\n\t/** Remaining days after the taken days */\n\tleftDays: number;\n\t/** Remaining days after the taken **and** planned days */\n\tavailableDays: number;\n\t/** Approved but not yet taken days */\n\tplannedDays: number;\n}\n\n/**\n * Vacation balance of a year (`carryover + entitlement \u2212 used`).\n *\n * Planned but not yet taken days are reported separately so the UI can show \u201Cleft\u201D and \u201Cafter planned\u201D.\n *\n * @param input - vacation input\n * @returns vacation balance\n */\nexport function vacationBalance(input: VacationInput): VacationResult {\n\tconst entitlementDays = roundDays(input.carryoverDays + input.perYearDays);\n\tconst leftDays = roundDays(entitlementDays - input.usedDays);\n\tconst plannedDays = roundDays(input.plannedDays ?? 0);\n\treturn { entitlementDays, leftDays, availableDays: roundDays(leftDays - plannedDays), plannedDays };\n}\n\n/**\n * Vacation/absence days inside a date range.\n *\n * Only active working days are counted (weekends and holidays are free), weighted by the day portion\n * (0.5 for a half day).\n *\n * @param input - range, portion and calendar data\n * @param input.from - first local date, `YYYY-MM-DD`\n * @param input.to - last local date, `YYYY-MM-DD`\n * @param input.workdays - `workdays` field of the employee\n * @param input.timeZone - time zone used to resolve the weekdays\n * @param input.holidays - local dates of the public holidays of the period\n * @param input.dayPortion - portion of each day (1 = whole day)\n * @returns counted days, rounded to two decimals\n */\nexport function absenceDaysInRange(input: {\n\tfrom: string;\n\tto: string;\n\tworkdays: string;\n\ttimeZone: string;\n\tholidays?: ReadonlySet<string>;\n\tdayPortion?: number;\n}): number {\n\tconst portion = input.dayPortion ?? 1;\n\tif (!(portion > 0)) {\n\t\treturn 0;\n\t}\n\n\tlet days = 0;\n\tfor (const date of dateRange(input.from, input.to)) {\n\t\tif (!isWorkdayOn(input.workdays, dayOfWeek(date, input.timeZone))) {\n\t\t\tcontinue;\n\t\t}\n\t\tif (input.holidays?.has(date)) {\n\t\t\tcontinue;\n\t\t}\n\t\tdays += portion;\n\t}\n\treturn roundDays(days);\n}\n\n/**\n * Rounds a day value to two decimals (avoids float noise like 24.999999).\n *\n * @param value - raw day value\n * @returns rounded value\n */\nfunction roundDays(value: number): number {\n\treturn Math.round(value * 100) / 100;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }