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.
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/admin/i18n/de.json +34 -0
- package/admin/i18n/en.json +1 -0
- package/admin/i18n/es.json +34 -0
- package/admin/i18n/fr.json +34 -0
- package/admin/i18n/it.json +34 -0
- package/admin/i18n/nl.json +34 -0
- package/admin/i18n/pl.json +34 -0
- package/admin/i18n/pt.json +34 -0
- package/admin/i18n/ru.json +34 -0
- package/admin/i18n/uk.json +34 -0
- package/admin/i18n/zh-cn.json +34 -0
- package/admin/jsonConfig.json +266 -0
- package/admin/time-tracker.png +0 -0
- package/build/lib/adapter/automation.js +100 -0
- package/build/lib/adapter/automation.js.map +7 -0
- package/build/lib/adapter/commands.js +160 -0
- package/build/lib/adapter/commands.js.map +7 -0
- package/build/lib/adapter/messages.js +207 -0
- package/build/lib/adapter/messages.js.map +7 -0
- package/build/lib/adapter/presence.js +129 -0
- package/build/lib/adapter/presence.js.map +7 -0
- package/build/lib/adapter/states.js +330 -0
- package/build/lib/adapter/states.js.map +7 -0
- package/build/lib/adapter/triggers.js +92 -0
- package/build/lib/adapter/triggers.js.map +7 -0
- package/build/lib/db/database.js +96 -0
- package/build/lib/db/database.js.map +7 -0
- package/build/lib/db/migrations.js +603 -0
- package/build/lib/db/migrations.js.map +7 -0
- package/build/lib/db/repositories/absences.js +432 -0
- package/build/lib/db/repositories/absences.js.map +7 -0
- package/build/lib/db/repositories/audit.js +90 -0
- package/build/lib/db/repositories/audit.js.map +7 -0
- package/build/lib/db/repositories/automations.js +326 -0
- package/build/lib/db/repositories/automations.js.map +7 -0
- package/build/lib/db/repositories/entries.js +335 -0
- package/build/lib/db/repositories/entries.js.map +7 -0
- package/build/lib/db/repositories/holidays.js +156 -0
- package/build/lib/db/repositories/holidays.js.map +7 -0
- package/build/lib/db/repositories/payouts.js +221 -0
- package/build/lib/db/repositories/payouts.js.map +7 -0
- package/build/lib/db/repositories/rfid.js +264 -0
- package/build/lib/db/repositories/rfid.js.map +7 -0
- package/build/lib/db/repositories/rules.js +356 -0
- package/build/lib/db/repositories/rules.js.map +7 -0
- package/build/lib/db/repositories/settings.js +91 -0
- package/build/lib/db/repositories/settings.js.map +7 -0
- package/build/lib/db/repositories/terminals.js +243 -0
- package/build/lib/db/repositories/terminals.js.map +7 -0
- package/build/lib/db/repositories/triggers.js +240 -0
- package/build/lib/db/repositories/triggers.js.map +7 -0
- package/build/lib/db/repositories/users.js +491 -0
- package/build/lib/db/repositories/users.js.map +7 -0
- package/build/lib/db/seed.js +180 -0
- package/build/lib/db/seed.js.map +7 -0
- package/build/lib/domain/avatar.js +63 -0
- package/build/lib/domain/avatar.js.map +7 -0
- package/build/lib/domain/breaks.js +70 -0
- package/build/lib/domain/breaks.js.map +7 -0
- package/build/lib/domain/calculation.js +150 -0
- package/build/lib/domain/calculation.js.map +7 -0
- package/build/lib/domain/holidays.js +113 -0
- package/build/lib/domain/holidays.js.map +7 -0
- package/build/lib/domain/punch.js +99 -0
- package/build/lib/domain/punch.js.map +7 -0
- package/build/lib/domain/target.js +89 -0
- package/build/lib/domain/target.js.map +7 -0
- package/build/lib/errors.js +56 -0
- package/build/lib/errors.js.map +7 -0
- package/build/lib/reports/labels.js +358 -0
- package/build/lib/reports/labels.js.map +7 -0
- package/build/lib/reports/pdf.js +308 -0
- package/build/lib/reports/pdf.js.map +7 -0
- package/build/lib/reports/types.js +32 -0
- package/build/lib/reports/types.js.map +7 -0
- package/build/lib/reports/xls.js +211 -0
- package/build/lib/reports/xls.js.map +7 -0
- package/build/lib/services/aggregation.js +495 -0
- package/build/lib/services/aggregation.js.map +7 -0
- package/build/lib/services/auth.js +381 -0
- package/build/lib/services/auth.js.map +7 -0
- package/build/lib/services/backup.js +372 -0
- package/build/lib/services/backup.js.map +7 -0
- package/build/lib/services/closing.js +106 -0
- package/build/lib/services/closing.js.map +7 -0
- package/build/lib/services/sessionSecret.js +100 -0
- package/build/lib/services/sessionSecret.js.map +7 -0
- package/build/lib/services/sync.js +278 -0
- package/build/lib/services/sync.js.map +7 -0
- package/build/lib/util/time.js +125 -0
- package/build/lib/util/time.js.map +7 -0
- package/build/lib/web/api.js +2589 -0
- package/build/lib/web/api.js.map +7 -0
- package/build/lib/web/cookies.js +80 -0
- package/build/lib/web/cookies.js.map +7 -0
- package/build/lib/web/events.js +50 -0
- package/build/lib/web/events.js.map +7 -0
- package/build/lib/web/pin-guard.js +86 -0
- package/build/lib/web/pin-guard.js.map +7 -0
- package/build/lib/web/problem.js +129 -0
- package/build/lib/web/problem.js.map +7 -0
- package/build/lib/web/rate-limit.js +65 -0
- package/build/lib/web/rate-limit.js.map +7 -0
- package/build/lib/web/router.js +274 -0
- package/build/lib/web/router.js.map +7 -0
- package/build/lib/web/server.js +189 -0
- package/build/lib/web/server.js.map +7 -0
- package/build/lib/web/static.js +151 -0
- package/build/lib/web/static.js.map +7 -0
- package/build/lib/web/stream.js +135 -0
- package/build/lib/web/stream.js.map +7 -0
- package/build/main.js +986 -0
- package/build/main.js.map +7 -0
- package/io-package.json +221 -0
- package/package.json +131 -0
- package/src-pwa/src/i18n/de.json +359 -0
- package/src-pwa/src/i18n/en.json +359 -0
- package/src-pwa/src/i18n/es.json +359 -0
- package/src-pwa/src/i18n/fr.json +359 -0
- package/src-pwa/src/i18n/index.ts +96 -0
- package/src-pwa/src/i18n/it.json +359 -0
- package/src-pwa/src/i18n/nl.json +359 -0
- package/src-pwa/src/i18n/pl.json +359 -0
- package/src-pwa/src/i18n/pt.json +359 -0
- package/src-pwa/src/i18n/ru.json +359 -0
- package/src-pwa/src/i18n/uk.json +359 -0
- package/src-pwa/src/i18n/zh-cn.json +359 -0
- package/www/assets/index-QI3wrEvW.js +119 -0
- package/www/assets/index-QI3wrEvW.js.map +1 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js +3 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js.map +1 -0
- package/www/favicon.svg +16 -0
- package/www/icon-192.png +0 -0
- package/www/icon-512.png +0 -0
- package/www/index.html +38 -0
- package/www/manifest.webmanifest +1 -0
- package/www/person.png +0 -0
- package/www/sw.js +3 -0
- package/www/sw.js.map +1 -0
- package/www/workbox-9c191d2f.js +3 -0
- package/www/workbox-9c191d2f.js.map +1 -0
|
@@ -0,0 +1,113 @@
|
|
|
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 holidays_exports = {};
|
|
20
|
+
__export(holidays_exports, {
|
|
21
|
+
addDays: () => addDays,
|
|
22
|
+
easterSunday: () => easterSunday,
|
|
23
|
+
formatDate: () => formatDate,
|
|
24
|
+
holidaysForYear: () => holidaysForYear
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(holidays_exports);
|
|
27
|
+
function easterSunday(year) {
|
|
28
|
+
const a = year % 19;
|
|
29
|
+
const b = Math.floor(year / 100);
|
|
30
|
+
const c = year % 100;
|
|
31
|
+
const d = Math.floor(b / 4);
|
|
32
|
+
const e = b % 4;
|
|
33
|
+
const f = Math.floor((b + 8) / 25);
|
|
34
|
+
const g = Math.floor((b - f + 1) / 3);
|
|
35
|
+
const h = (19 * a + b - d - g + 15) % 30;
|
|
36
|
+
const i = Math.floor(c / 4);
|
|
37
|
+
const k = c % 4;
|
|
38
|
+
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
|
39
|
+
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
|
40
|
+
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
|
41
|
+
const day = (h + l - 7 * m + 114) % 31 + 1;
|
|
42
|
+
return { year, month, day };
|
|
43
|
+
}
|
|
44
|
+
function formatDate(date) {
|
|
45
|
+
const month = String(date.month).padStart(2, "0");
|
|
46
|
+
const day = String(date.day).padStart(2, "0");
|
|
47
|
+
return `${date.year}-${month}-${day}`;
|
|
48
|
+
}
|
|
49
|
+
function addDays(date, days) {
|
|
50
|
+
const base = new Date(Date.UTC(date.year, date.month - 1, date.day));
|
|
51
|
+
base.setUTCDate(base.getUTCDate() + days);
|
|
52
|
+
return { year: base.getUTCFullYear(), month: base.getUTCMonth() + 1, day: base.getUTCDate() };
|
|
53
|
+
}
|
|
54
|
+
function holidaysForYear(year, country) {
|
|
55
|
+
const easter = easterSunday(year);
|
|
56
|
+
const movable = (offset, name, key) => ({
|
|
57
|
+
date: formatDate(addDays(easter, offset)),
|
|
58
|
+
name,
|
|
59
|
+
key
|
|
60
|
+
});
|
|
61
|
+
const fixed = [{ date: `${year}-01-01`, name: "New Year", key: "newYear" }];
|
|
62
|
+
const easterBased = [];
|
|
63
|
+
switch (country) {
|
|
64
|
+
case "CH":
|
|
65
|
+
easterBased.push(movable(-2, "Good Friday", "goodFriday"), movable(39, "Ascension Day", "ascension"));
|
|
66
|
+
fixed.push(
|
|
67
|
+
{ date: `${year}-08-01`, name: "National Day", key: "nationalDay" },
|
|
68
|
+
{ date: `${year}-12-25`, name: "Christmas Day", key: "christmas" }
|
|
69
|
+
);
|
|
70
|
+
break;
|
|
71
|
+
case "DE":
|
|
72
|
+
easterBased.push(
|
|
73
|
+
movable(-2, "Good Friday", "goodFriday"),
|
|
74
|
+
movable(1, "Easter Monday", "easterMonday"),
|
|
75
|
+
movable(39, "Ascension Day", "ascension"),
|
|
76
|
+
movable(50, "Whit Monday", "whitMonday")
|
|
77
|
+
);
|
|
78
|
+
fixed.push(
|
|
79
|
+
{ date: `${year}-05-01`, name: "Labour Day", key: "labourDay" },
|
|
80
|
+
{ date: `${year}-10-03`, name: "German Unity Day", key: "germanUnity" },
|
|
81
|
+
{ date: `${year}-12-25`, name: "Christmas Day", key: "christmas" },
|
|
82
|
+
{ date: `${year}-12-26`, name: "Boxing Day", key: "boxingDay" }
|
|
83
|
+
);
|
|
84
|
+
break;
|
|
85
|
+
case "AT":
|
|
86
|
+
easterBased.push(
|
|
87
|
+
movable(1, "Easter Monday", "easterMonday"),
|
|
88
|
+
movable(39, "Ascension Day", "ascension"),
|
|
89
|
+
movable(50, "Whit Monday", "whitMonday"),
|
|
90
|
+
movable(60, "Corpus Christi", "corpusChristi")
|
|
91
|
+
);
|
|
92
|
+
fixed.push(
|
|
93
|
+
{ date: `${year}-01-06`, name: "Epiphany", key: "epiphany" },
|
|
94
|
+
{ date: `${year}-05-01`, name: "Labour Day", key: "labourDay" },
|
|
95
|
+
{ date: `${year}-08-15`, name: "Assumption Day", key: "assumption" },
|
|
96
|
+
{ date: `${year}-10-26`, name: "National Day", key: "nationalDay" },
|
|
97
|
+
{ date: `${year}-11-01`, name: "All Saints' Day", key: "allSaints" },
|
|
98
|
+
{ date: `${year}-12-08`, name: "Immaculate Conception", key: "immaculateConception" },
|
|
99
|
+
{ date: `${year}-12-25`, name: "Christmas Day", key: "christmas" },
|
|
100
|
+
{ date: `${year}-12-26`, name: "Boxing Day", key: "boxingDay" }
|
|
101
|
+
);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
return [...fixed, ...easterBased].sort((a, b) => a.date.localeCompare(b.date));
|
|
105
|
+
}
|
|
106
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
107
|
+
0 && (module.exports = {
|
|
108
|
+
addDays,
|
|
109
|
+
easterSunday,
|
|
110
|
+
formatDate,
|
|
111
|
+
holidaysForYear
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=holidays.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/domain/holidays.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Public holidays for the supported countries.\n *\n * Date strings use the format `YYYY-MM-DD` (local calendar date), matching `holidays.date`\n * and `absences.date_from` in the database.\n *\n * Movable feasts are derived from Easter Sunday (Meeus/Jones/Butcher algorithm); fixed dates are\n * defined per country. Country-specific deviations (cantonal holidays in Switzerland, for example)\n * are configured later per user via `work_profiles.holiday_flags`.\n */\n\nexport type HolidayCountry = \"CH\" | \"DE\" | \"AT\";\n\n/**\n * A public holiday with its local calendar date.\n */\nexport interface Holiday {\n\t/** Local date, `YYYY-MM-DD` */\n\tdate: string;\n\t/** English display name (fallback for a day somebody added by hand) */\n\tname: string;\n\t/** Stable key for the translations, e.g. `newYear` */\n\tkey: string;\n}\n\n/**\n * A calendar date without time and without time zone.\n */\nexport interface CalendarDate {\n\t/** Four digit year */\n\tyear: number;\n\t/** Month, 1 to 12 */\n\tmonth: number;\n\t/** Day of month, 1 to 31 */\n\tday: number;\n}\n\n/**\n * Calculates Easter Sunday for a Gregorian year.\n *\n * @param year - four digit year\n * @returns date of Easter Sunday\n */\nexport function easterSunday(year: number): CalendarDate {\n\tconst a = year % 19;\n\tconst b = Math.floor(year / 100);\n\tconst c = year % 100;\n\tconst d = Math.floor(b / 4);\n\tconst e = b % 4;\n\tconst f = Math.floor((b + 8) / 25);\n\tconst g = Math.floor((b - f + 1) / 3);\n\tconst h = (19 * a + b - d - g + 15) % 30;\n\tconst i = Math.floor(c / 4);\n\tconst k = c % 4;\n\tconst l = (32 + 2 * e + 2 * i - h - k) % 7;\n\tconst m = Math.floor((a + 11 * h + 22 * l) / 451);\n\tconst month = Math.floor((h + l - 7 * m + 114) / 31);\n\tconst day = ((h + l - 7 * m + 114) % 31) + 1;\n\treturn { year, month, day };\n}\n\n/**\n * Formats a calendar date as `YYYY-MM-DD`.\n *\n * @param date - calendar date to format\n * @returns date string in `YYYY-MM-DD`\n */\nexport function formatDate(date: CalendarDate): string {\n\tconst month = String(date.month).padStart(2, \"0\");\n\tconst day = String(date.day).padStart(2, \"0\");\n\treturn `${date.year}-${month}-${day}`;\n}\n\n/**\n * Adds a number of days to a calendar date (UTC based, so no daylight saving surprises).\n *\n * @param date - starting date\n * @param days - number of days to add (may be negative)\n * @returns resulting date\n */\nexport function addDays(date: CalendarDate, days: number): CalendarDate {\n\tconst base = new Date(Date.UTC(date.year, date.month - 1, date.day));\n\tbase.setUTCDate(base.getUTCDate() + days);\n\treturn { year: base.getUTCFullYear(), month: base.getUTCMonth() + 1, day: base.getUTCDate() };\n}\n\n/**\n * Returns all public holidays of a year for the given country, sorted by date.\n *\n * @param year - four digit year\n * @param country - supported country code\n */\nexport function holidaysForYear(year: number, country: HolidayCountry): Holiday[] {\n\tconst easter = easterSunday(year);\n\tconst movable = (offset: number, name: string, key: string): Holiday => ({\n\t\tdate: formatDate(addDays(easter, offset)),\n\t\tname,\n\t\tkey,\n\t});\n\n\tconst fixed: Holiday[] = [{ date: `${year}-01-01`, name: \"New Year\", key: \"newYear\" }];\n\tconst easterBased: Holiday[] = [];\n\n\tswitch (country) {\n\t\tcase \"CH\":\n\t\t\teasterBased.push(movable(-2, \"Good Friday\", \"goodFriday\"), movable(39, \"Ascension Day\", \"ascension\"));\n\t\t\tfixed.push(\n\t\t\t\t{ date: `${year}-08-01`, name: \"National Day\", key: \"nationalDay\" },\n\t\t\t\t{ date: `${year}-12-25`, name: \"Christmas Day\", key: \"christmas\" },\n\t\t\t);\n\t\t\tbreak;\n\t\tcase \"DE\":\n\t\t\teasterBased.push(\n\t\t\t\tmovable(-2, \"Good Friday\", \"goodFriday\"),\n\t\t\t\tmovable(1, \"Easter Monday\", \"easterMonday\"),\n\t\t\t\tmovable(39, \"Ascension Day\", \"ascension\"),\n\t\t\t\tmovable(50, \"Whit Monday\", \"whitMonday\"),\n\t\t\t);\n\t\t\tfixed.push(\n\t\t\t\t{ date: `${year}-05-01`, name: \"Labour Day\", key: \"labourDay\" },\n\t\t\t\t{ date: `${year}-10-03`, name: \"German Unity Day\", key: \"germanUnity\" },\n\t\t\t\t{ date: `${year}-12-25`, name: \"Christmas Day\", key: \"christmas\" },\n\t\t\t\t{ date: `${year}-12-26`, name: \"Boxing Day\", key: \"boxingDay\" },\n\t\t\t);\n\t\t\tbreak;\n\t\tcase \"AT\":\n\t\t\teasterBased.push(\n\t\t\t\tmovable(1, \"Easter Monday\", \"easterMonday\"),\n\t\t\t\tmovable(39, \"Ascension Day\", \"ascension\"),\n\t\t\t\tmovable(50, \"Whit Monday\", \"whitMonday\"),\n\t\t\t\tmovable(60, \"Corpus Christi\", \"corpusChristi\"),\n\t\t\t);\n\t\t\tfixed.push(\n\t\t\t\t{ date: `${year}-01-06`, name: \"Epiphany\", key: \"epiphany\" },\n\t\t\t\t{ date: `${year}-05-01`, name: \"Labour Day\", key: \"labourDay\" },\n\t\t\t\t{ date: `${year}-08-15`, name: \"Assumption Day\", key: \"assumption\" },\n\t\t\t\t{ date: `${year}-10-26`, name: \"National Day\", key: \"nationalDay\" },\n\t\t\t\t{ date: `${year}-11-01`, name: \"All Saints' Day\", key: \"allSaints\" },\n\t\t\t\t{ date: `${year}-12-08`, name: \"Immaculate Conception\", key: \"immaculateConception\" },\n\t\t\t\t{ date: `${year}-12-25`, name: \"Christmas Day\", key: \"christmas\" },\n\t\t\t\t{ date: `${year}-12-26`, name: \"Boxing Day\", key: \"boxingDay\" },\n\t\t\t);\n\t\t\tbreak;\n\t}\n\n\treturn [...fixed, ...easterBased].sort((a, b) => a.date.localeCompare(b.date));\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 punch_exports = {};
|
|
20
|
+
__export(punch_exports, {
|
|
21
|
+
buildDayPunches: () => buildDayPunches,
|
|
22
|
+
checkEmploymentWindow: () => checkEmploymentWindow,
|
|
23
|
+
nextDirection: () => nextDirection,
|
|
24
|
+
roundToStep: () => roundToStep
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(punch_exports);
|
|
27
|
+
var import_luxon = require("luxon");
|
|
28
|
+
var import_time = require("../util/time");
|
|
29
|
+
const UTC = "utc";
|
|
30
|
+
function buildDayPunches(entries, options = {}) {
|
|
31
|
+
var _a, _b;
|
|
32
|
+
const minDistance = (_a = options.minDistanceSeconds) != null ? _a : 30;
|
|
33
|
+
const usable = entries.filter((entry) => {
|
|
34
|
+
var _a2;
|
|
35
|
+
return ((_a2 = entry.syncState) != null ? _a2 : "synced") === "synced";
|
|
36
|
+
}).slice().sort((a, b) => a.tsUtc - b.tsUtc || a.id - b.id);
|
|
37
|
+
const kept = [];
|
|
38
|
+
for (const entry of usable) {
|
|
39
|
+
const previous = kept[kept.length - 1];
|
|
40
|
+
if (previous && entry.tsUtc - previous.tsUtc < minDistance) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
kept.push(entry);
|
|
44
|
+
}
|
|
45
|
+
const pairs = [];
|
|
46
|
+
let totalSeconds = 0;
|
|
47
|
+
for (let index = 0; index < kept.length; index += 2) {
|
|
48
|
+
const inEntry = kept[index];
|
|
49
|
+
const outEntry = (_b = kept[index + 1]) != null ? _b : null;
|
|
50
|
+
const seconds = outEntry ? Math.max(0, outEntry.tsUtc - inEntry.tsUtc) : 0;
|
|
51
|
+
totalSeconds += seconds;
|
|
52
|
+
pairs.push({ inEntry, outEntry, minutes: Math.round(seconds / 60) });
|
|
53
|
+
}
|
|
54
|
+
const hasOpenEntry = kept.length % 2 === 1;
|
|
55
|
+
const spanSeconds = hasOpenEntry || kept.length === 0 ? totalSeconds : Math.max(0, kept[kept.length - 1].tsUtc - kept[0].tsUtc);
|
|
56
|
+
const workedMinutes = Math.round(totalSeconds / 60);
|
|
57
|
+
const spanMinutes = Math.round(spanSeconds / 60);
|
|
58
|
+
return {
|
|
59
|
+
pairs,
|
|
60
|
+
hasOpenEntry,
|
|
61
|
+
firstInUtc: kept.length > 0 ? kept[0].tsUtc : null,
|
|
62
|
+
lastOutUtc: kept.length > 0 && !hasOpenEntry ? kept[kept.length - 1].tsUtc : null,
|
|
63
|
+
workedMinutes,
|
|
64
|
+
spanMinutes,
|
|
65
|
+
gapMinutes: Math.max(0, spanMinutes - workedMinutes)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function nextDirection(day) {
|
|
69
|
+
return day.hasOpenEntry ? "out" : "in";
|
|
70
|
+
}
|
|
71
|
+
function checkEmploymentWindow(tsUtc, window) {
|
|
72
|
+
if (window.startDate != null && tsUtc < window.startDate) {
|
|
73
|
+
return "before_start_date";
|
|
74
|
+
}
|
|
75
|
+
if (window.endDate != null && tsUtc > window.endDate) {
|
|
76
|
+
return "after_end_date";
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function roundToStep(tsUtc, stepMinutes, timeZone) {
|
|
81
|
+
if (stepMinutes <= 0) {
|
|
82
|
+
return tsUtc;
|
|
83
|
+
}
|
|
84
|
+
const stepSeconds = stepMinutes * 60;
|
|
85
|
+
const local = import_luxon.DateTime.fromSeconds(tsUtc, { zone: UTC }).setZone(timeZone);
|
|
86
|
+
const secondsOfDay = local.hour * 3600 + local.minute * 60 + local.second;
|
|
87
|
+
const roundedSecondsOfDay = Math.round(secondsOfDay / stepSeconds) * stepSeconds;
|
|
88
|
+
const wallSeconds = (0, import_time.utcToWallTime)(tsUtc, timeZone);
|
|
89
|
+
const roundedWallSeconds = wallSeconds - secondsOfDay + roundedSecondsOfDay;
|
|
90
|
+
return (0, import_time.wallTimeToUtc)(roundedWallSeconds, timeZone).tsUtc;
|
|
91
|
+
}
|
|
92
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
93
|
+
0 && (module.exports = {
|
|
94
|
+
buildDayPunches,
|
|
95
|
+
checkEmploymentWindow,
|
|
96
|
+
nextDirection,
|
|
97
|
+
roundToStep
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=punch.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/domain/punch.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Punch (clock-in/out) logic: pairing, duplicate protection, employment window and quick rounding.\n *\n * Source of truth is the order of the punches by `(ts_utc, id)`; the stored `direction` is only a UI hint.\n * Punches that are not yet synchronised (`pending`) or flagged as conflict do not count.\n */\n\nimport { DateTime } from \"luxon\";\nimport { utcToWallTime, wallTimeToUtc } from \"../util/time\";\n\nconst UTC = \"utc\";\n\n/** Minimal punch information needed for pairing. */\nexport interface PunchEntry {\n\t/** Database id (used as tie breaker for identical timestamps) */\n\tid: number;\n\t/** Punch instant, UTC epoch seconds */\n\ttsUtc: number;\n\t/** Synchronisation state; `pending` and `conflict` punches are ignored */\n\tsyncState?: \"synced\" | \"pending\" | \"conflict\";\n}\n\n/** A pair of punches; `outEntry` is `null` while the punch is still open. */\nexport interface PunchPair {\n\t/** First punch of the pair (clock in) */\n\tinEntry: PunchEntry;\n\t/** Second punch of the pair (clock out), `null` while the punch is open */\n\toutEntry: PunchEntry | null;\n\t/** Duration of the pair in minutes (0 for an open pair) */\n\tminutes: number;\n}\n\n/** Result of pairing all punches of one day. */\nexport interface DayPunches {\n\t/** All 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/** Timestamp of the first punch of the day */\n\tfirstInUtc: number | null;\n\t/** Timestamp of the last completed clock-out, `null` while a punch is open */\n\tlastOutUtc: number | null;\n\t/** Sum of all complete pairs in minutes */\n\tworkedMinutes: number;\n\t/**\n\t * Minutes the employee was present: from the first punch to the last completed one.\n\t *\n\t * While a punch is open this equals the sum of the pairs \u2014 an open day has no final end yet.\n\t */\n\tspanMinutes: number;\n\t/**\n\t * Minutes between the pairs: the breaks the employee punched.\n\t *\n\t * A day that is still open reports `0`, because its breaks are not final yet.\n\t */\n\tgapMinutes: number;\n}\n\n/** Options for pairing. */\nexport interface PairingOptions {\n\t/** Punches closer than this to the previous kept punch are ignored (default 30 seconds) */\n\tminDistanceSeconds?: number;\n}\n\n/** Errors of the employment window. */\nexport type EmploymentError = \"before_start_date\" | \"after_end_date\";\n\n/**\n * Pairs the punches of one day (ascending by `ts_utc`, then `id`).\n *\n * @param entries - punches of a single local day\n * @param options - pairing options\n * @returns pairs, open state and worked minutes\n */\nexport function buildDayPunches(entries: PunchEntry[], options: PairingOptions = {}): DayPunches {\n\tconst minDistance = options.minDistanceSeconds ?? 30;\n\n\tconst usable = entries\n\t\t.filter(entry => (entry.syncState ?? \"synced\") === \"synced\")\n\t\t.slice()\n\t\t.sort((a, b) => a.tsUtc - b.tsUtc || a.id - b.id);\n\n\tconst kept: PunchEntry[] = [];\n\tfor (const entry of usable) {\n\t\tconst previous = kept[kept.length - 1];\n\t\tif (previous && entry.tsUtc - previous.tsUtc < minDistance) {\n\t\t\t// duplicate protection: keep the first punch, drop the immediate repetition\n\t\t\tcontinue;\n\t\t}\n\t\tkept.push(entry);\n\t}\n\n\tconst pairs: PunchPair[] = [];\n\tlet totalSeconds = 0;\n\tfor (let index = 0; index < kept.length; index += 2) {\n\t\tconst inEntry = kept[index];\n\t\tconst outEntry = kept[index + 1] ?? null;\n\t\tconst seconds = outEntry ? Math.max(0, outEntry.tsUtc - inEntry.tsUtc) : 0;\n\t\ttotalSeconds += seconds;\n\t\tpairs.push({ inEntry, outEntry, minutes: Math.round(seconds / 60) });\n\t}\n\n\tconst hasOpenEntry = kept.length % 2 === 1;\n\t// The presence reaches from the first punch to the last completed one; the difference to the pairs is exactly\n\t// the time between them \u2014 the breaks. A day with an open punch measures nothing of that. Both values are\n\t// rounded first and the break is their difference, so `span \u2212 break` is exactly the sum of the pairs.\n\tconst spanSeconds =\n\t\thasOpenEntry || kept.length === 0 ? totalSeconds : Math.max(0, kept[kept.length - 1].tsUtc - kept[0].tsUtc);\n\tconst workedMinutes = Math.round(totalSeconds / 60);\n\tconst spanMinutes = Math.round(spanSeconds / 60);\n\n\treturn {\n\t\tpairs,\n\t\thasOpenEntry,\n\t\tfirstInUtc: kept.length > 0 ? kept[0].tsUtc : null,\n\t\tlastOutUtc: kept.length > 0 && !hasOpenEntry ? kept[kept.length - 1].tsUtc : null,\n\t\tworkedMinutes,\n\t\tspanMinutes,\n\t\tgapMinutes: Math.max(0, spanMinutes - workedMinutes),\n\t};\n}\n\n/**\n * Direction the next punch would have (\u201Cout\u201D while a punch is open, otherwise \u201Cin\u201D).\n *\n * @param day - result of `buildDayPunches`\n * @returns expected direction\n */\nexport function nextDirection(day: DayPunches): \"in\" | \"out\" {\n\treturn day.hasOpenEntry ? \"out\" : \"in\";\n}\n\n/**\n * Checks whether a punch lies inside the employment window (`start_date`/`end_date`).\n *\n * @param tsUtc - punch instant, UTC epoch seconds\n * @param window - employment window (either bound may be missing)\n * @param window.startDate - first day the employee may punch (optional)\n * @param window.endDate - last day the employee may punch (optional)\n * @returns the violated bound or `null` when the punch is allowed\n */\nexport function checkEmploymentWindow(\n\ttsUtc: number,\n\twindow: { startDate?: number | null; endDate?: number | null },\n): EmploymentError | null {\n\tif (window.startDate != null && tsUtc < window.startDate) {\n\t\treturn \"before_start_date\";\n\t}\n\tif (window.endDate != null && tsUtc > window.endDate) {\n\t\treturn \"after_end_date\";\n\t}\n\treturn null;\n}\n\n/**\n * Rounds an instant to the nearest multiple of a step, based on the local wall clock time.\n *\n * @param tsUtc - instant to round, UTC epoch seconds\n * @param stepMinutes - step in minutes (`0` disables rounding)\n * @param timeZone - time zone used for the wall clock\n * @returns rounded instant, UTC epoch seconds\n */\nexport function roundToStep(tsUtc: number, stepMinutes: number, timeZone: string): number {\n\tif (stepMinutes <= 0) {\n\t\treturn tsUtc;\n\t}\n\n\tconst stepSeconds = stepMinutes * 60;\n\tconst local = DateTime.fromSeconds(tsUtc, { zone: UTC }).setZone(timeZone);\n\tconst secondsOfDay = local.hour * 3600 + local.minute * 60 + local.second;\n\tconst roundedSecondsOfDay = Math.round(secondsOfDay / stepSeconds) * stepSeconds;\n\n\tconst wallSeconds = utcToWallTime(tsUtc, timeZone);\n\tconst roundedWallSeconds = wallSeconds - secondsOfDay + roundedSecondsOfDay;\n\treturn wallTimeToUtc(roundedWallSeconds, timeZone).tsUtc;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,mBAAyB;AACzB,kBAA6C;AAE7C,MAAM,MAAM;AAgEL,SAAS,gBAAgB,SAAuB,UAA0B,CAAC,GAAe;AA1EjG;AA2EC,QAAM,eAAc,aAAQ,uBAAR,YAA8B;AAElD,QAAM,SAAS,QACb,OAAO,WAAM;AA9EhB,QAAAA;AA8EoB,aAAAA,MAAA,MAAM,cAAN,OAAAA,MAAmB,cAAc;AAAA,GAAQ,EAC1D,MAAM,EACN,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE;AAEjD,QAAM,OAAqB,CAAC;AAC5B,aAAW,SAAS,QAAQ;AAC3B,UAAM,WAAW,KAAK,KAAK,SAAS,CAAC;AACrC,QAAI,YAAY,MAAM,QAAQ,SAAS,QAAQ,aAAa;AAE3D;AAAA,IACD;AACA,SAAK,KAAK,KAAK;AAAA,EAChB;AAEA,QAAM,QAAqB,CAAC;AAC5B,MAAI,eAAe;AACnB,WAAS,QAAQ,GAAG,QAAQ,KAAK,QAAQ,SAAS,GAAG;AACpD,UAAM,UAAU,KAAK,KAAK;AAC1B,UAAM,YAAW,UAAK,QAAQ,CAAC,MAAd,YAAmB;AACpC,UAAM,UAAU,WAAW,KAAK,IAAI,GAAG,SAAS,QAAQ,QAAQ,KAAK,IAAI;AACzE,oBAAgB;AAChB,UAAM,KAAK,EAAE,SAAS,UAAU,SAAS,KAAK,MAAM,UAAU,EAAE,EAAE,CAAC;AAAA,EACpE;AAEA,QAAM,eAAe,KAAK,SAAS,MAAM;AAIzC,QAAM,cACL,gBAAgB,KAAK,WAAW,IAAI,eAAe,KAAK,IAAI,GAAG,KAAK,KAAK,SAAS,CAAC,EAAE,QAAQ,KAAK,CAAC,EAAE,KAAK;AAC3G,QAAM,gBAAgB,KAAK,MAAM,eAAe,EAAE;AAClD,QAAM,cAAc,KAAK,MAAM,cAAc,EAAE;AAE/C,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA,YAAY,KAAK,SAAS,IAAI,KAAK,CAAC,EAAE,QAAQ;AAAA,IAC9C,YAAY,KAAK,SAAS,KAAK,CAAC,eAAe,KAAK,KAAK,SAAS,CAAC,EAAE,QAAQ;AAAA,IAC7E;AAAA,IACA;AAAA,IACA,YAAY,KAAK,IAAI,GAAG,cAAc,aAAa;AAAA,EACpD;AACD;AAQO,SAAS,cAAc,KAA+B;AAC5D,SAAO,IAAI,eAAe,QAAQ;AACnC;AAWO,SAAS,sBACf,OACA,QACyB;AACzB,MAAI,OAAO,aAAa,QAAQ,QAAQ,OAAO,WAAW;AACzD,WAAO;AAAA,EACR;AACA,MAAI,OAAO,WAAW,QAAQ,QAAQ,OAAO,SAAS;AACrD,WAAO;AAAA,EACR;AACA,SAAO;AACR;AAUO,SAAS,YAAY,OAAe,aAAqB,UAA0B;AACzF,MAAI,eAAe,GAAG;AACrB,WAAO;AAAA,EACR;AAEA,QAAM,cAAc,cAAc;AAClC,QAAM,QAAQ,sBAAS,YAAY,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,QAAQ,QAAQ;AACzE,QAAM,eAAe,MAAM,OAAO,OAAO,MAAM,SAAS,KAAK,MAAM;AACnE,QAAM,sBAAsB,KAAK,MAAM,eAAe,WAAW,IAAI;AAErE,QAAM,kBAAc,2BAAc,OAAO,QAAQ;AACjD,QAAM,qBAAqB,cAAc,eAAe;AACxD,aAAO,2BAAc,oBAAoB,QAAQ,EAAE;AACpD;",
|
|
6
|
+
"names": ["_a"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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 target_exports = {};
|
|
20
|
+
__export(target_exports, {
|
|
21
|
+
activeWorkdayCount: () => activeWorkdayCount,
|
|
22
|
+
dailyTargetMinutes: () => dailyTargetMinutes,
|
|
23
|
+
isWorkdayOn: () => isWorkdayOn,
|
|
24
|
+
parseWorkdays: () => parseWorkdays,
|
|
25
|
+
targetForDay: () => targetForDay,
|
|
26
|
+
weeklyTargetMinutes: () => weeklyTargetMinutes
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(target_exports);
|
|
29
|
+
function parseWorkdays(value) {
|
|
30
|
+
var _a;
|
|
31
|
+
const parts = value.split(";");
|
|
32
|
+
const days = [];
|
|
33
|
+
for (let index = 0; index < 7; index++) {
|
|
34
|
+
days.push(((_a = parts[index]) == null ? void 0 : _a.trim()) === "1");
|
|
35
|
+
}
|
|
36
|
+
return days;
|
|
37
|
+
}
|
|
38
|
+
function activeWorkdayCount(value) {
|
|
39
|
+
return parseWorkdays(value).filter(Boolean).length;
|
|
40
|
+
}
|
|
41
|
+
function isWorkdayOn(value, weekday) {
|
|
42
|
+
return parseWorkdays(value)[weekday] === true;
|
|
43
|
+
}
|
|
44
|
+
function weeklyTargetMinutes(profile) {
|
|
45
|
+
const weeklyHours = profile.weeklyHours * profile.percent / 100;
|
|
46
|
+
return Math.round(weeklyHours * 60);
|
|
47
|
+
}
|
|
48
|
+
function dailyTargetMinutes(profile) {
|
|
49
|
+
const activeDays = activeWorkdayCount(profile.workdays);
|
|
50
|
+
if (activeDays === 0) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const weeklyHours = profile.weeklyHours * profile.percent / 100;
|
|
54
|
+
const dailyHours = Math.round(weeklyHours / activeDays * 100) / 100;
|
|
55
|
+
return Math.round(dailyHours * 60);
|
|
56
|
+
}
|
|
57
|
+
function targetForDay(args) {
|
|
58
|
+
if (args.isBeforeStart || args.isAfterEnd) {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
if (!isWorkdayOn(args.profile.workdays, args.weekday)) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
if (args.isHoliday) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const base = dailyTargetMinutes(args.profile);
|
|
68
|
+
if (!args.absence) {
|
|
69
|
+
return base;
|
|
70
|
+
}
|
|
71
|
+
const credited = Math.max(0, Math.min(100, args.absence.factor)) / 100 * clampPortion(args.absence.dayPortion);
|
|
72
|
+
return Math.round(base * (1 - credited));
|
|
73
|
+
}
|
|
74
|
+
function clampPortion(portion) {
|
|
75
|
+
if (!Number.isFinite(portion) || portion <= 0) {
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
return Math.min(1, portion);
|
|
79
|
+
}
|
|
80
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
81
|
+
0 && (module.exports = {
|
|
82
|
+
activeWorkdayCount,
|
|
83
|
+
dailyTargetMinutes,
|
|
84
|
+
isWorkdayOn,
|
|
85
|
+
parseWorkdays,
|
|
86
|
+
targetForDay,
|
|
87
|
+
weeklyTargetMinutes
|
|
88
|
+
});
|
|
89
|
+
//# sourceMappingURL=target.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/domain/target.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Target working time (daily target).\n *\n * Formula:\n * weekly target = weekly_hours * percent / 100\n * daily target = round(weekly target / number of active working days, 2) (in hours)\n *\n * The value is stored in minutes; the two-decimal rounding is kept so that existing balances keep their\n * value (rounding to whole minutes afterwards).\n */\n\n/** Employment parameters relevant for the target time. */\nexport interface WorkProfile {\n\t/** Employment level in percent */\n\tpercent: number;\n\t/** Contracted hours per week at 100 % */\n\tweeklyHours: number;\n\t/** Active working days, `0;1;\u2026` for Sunday\u2026Saturday */\n\tworkdays: string;\n\t/** First day of the employment (UTC epoch seconds) */\n\tstartDate?: number | null;\n\t/** Last day of the employment (UTC epoch seconds) */\n\tendDate?: number | null;\n}\n\n/** Impact of an absence on the target of one day. */\nexport interface AbsenceImpact {\n\t/** Percentage of the working time credited (100 = fully credited) */\n\tfactor: number;\n\t/** Portion of the day (1 = whole day, 0.5 = half day) */\n\tdayPortion: number;\n}\n\n/**\n * Parses the `workdays` field into seven booleans, index 0 = Sunday.\n *\n * @param value - `0;1;\u2026` with seven entries\n * @returns flags for Sunday to Saturday\n */\nexport function parseWorkdays(value: string): boolean[] {\n\tconst parts = value.split(\";\");\n\tconst days: boolean[] = [];\n\tfor (let index = 0; index < 7; index++) {\n\t\tdays.push(parts[index]?.trim() === \"1\");\n\t}\n\treturn days;\n}\n\n/**\n * Counts the active working days.\n *\n * @param value - `workdays` field\n * @returns number of active days (0 to 7)\n */\nexport function activeWorkdayCount(value: string): number {\n\treturn parseWorkdays(value).filter(Boolean).length;\n}\n\n/**\n * Checks whether a weekday is an active working day.\n *\n * @param value - `workdays` field\n * @param weekday - 0 = Sunday \u2026 6 = Saturday\n * @returns true when the day is an active working day\n */\nexport function isWorkdayOn(value: string, weekday: number): boolean {\n\treturn parseWorkdays(value)[weekday] === true;\n}\n\n/**\n * Weekly target in minutes.\n *\n * @param profile - employment parameters\n * @returns weekly target in minutes, rounded\n */\nexport function weeklyTargetMinutes(profile: WorkProfile): number {\n\tconst weeklyHours = (profile.weeklyHours * profile.percent) / 100;\n\treturn Math.round(weeklyHours * 60);\n}\n\n/**\n * Daily target in minutes for a full working day (independent of holidays and absences).\n *\n * @param profile - employment parameters\n * @returns daily target in minutes, 0 when no working day is configured\n */\nexport function dailyTargetMinutes(profile: WorkProfile): number {\n\tconst activeDays = activeWorkdayCount(profile.workdays);\n\tif (activeDays === 0) {\n\t\treturn 0;\n\t}\n\n\tconst weeklyHours = (profile.weeklyHours * profile.percent) / 100;\n\tconst dailyHours = Math.round((weeklyHours / activeDays) * 100) / 100;\n\treturn Math.round(dailyHours * 60);\n}\n\n/**\n * Target minutes of a single day, considering holidays, absences and the employment window.\n *\n * @param args - target calculation input\n * @param args.profile - employment parameters\n * @param args.weekday - weekday of the day, 0 = Sunday\n * @param args.isHoliday - true when the day is a public holiday\n * @param args.absence - absence covering the day, if any\n * @param args.isBeforeStart - true when the day lies before the employment start\n * @param args.isAfterEnd - true when the day lies after the employment end\n * @returns target minutes for that day\n */\nexport function targetForDay(args: {\n\tprofile: WorkProfile;\n\tweekday: number;\n\tisHoliday?: boolean;\n\tabsence?: AbsenceImpact | null;\n\tisBeforeStart?: boolean;\n\tisAfterEnd?: boolean;\n}): number {\n\tif (args.isBeforeStart || args.isAfterEnd) {\n\t\treturn 0;\n\t}\n\tif (!isWorkdayOn(args.profile.workdays, args.weekday)) {\n\t\treturn 0;\n\t}\n\tif (args.isHoliday) {\n\t\treturn 0;\n\t}\n\n\tconst base = dailyTargetMinutes(args.profile);\n\tif (!args.absence) {\n\t\treturn base;\n\t}\n\n\tconst credited = (Math.max(0, Math.min(100, args.absence.factor)) / 100) * clampPortion(args.absence.dayPortion);\n\treturn Math.round(base * (1 - credited));\n}\n\n/**\n * Clamps a day portion into (0, 1].\n *\n * @param portion - raw portion\n * @returns clamped portion\n */\nfunction clampPortion(portion: number): number {\n\tif (!Number.isFinite(portion) || portion <= 0) {\n\t\treturn 0;\n\t}\n\treturn Math.min(1, portion);\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuCO,SAAS,cAAc,OAA0B;AAvCxD;AAwCC,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,QAAM,OAAkB,CAAC;AACzB,WAAS,QAAQ,GAAG,QAAQ,GAAG,SAAS;AACvC,SAAK,OAAK,WAAM,KAAK,MAAX,mBAAc,YAAW,GAAG;AAAA,EACvC;AACA,SAAO;AACR;AAQO,SAAS,mBAAmB,OAAuB;AACzD,SAAO,cAAc,KAAK,EAAE,OAAO,OAAO,EAAE;AAC7C;AASO,SAAS,YAAY,OAAe,SAA0B;AACpE,SAAO,cAAc,KAAK,EAAE,OAAO,MAAM;AAC1C;AAQO,SAAS,oBAAoB,SAA8B;AACjE,QAAM,cAAe,QAAQ,cAAc,QAAQ,UAAW;AAC9D,SAAO,KAAK,MAAM,cAAc,EAAE;AACnC;AAQO,SAAS,mBAAmB,SAA8B;AAChE,QAAM,aAAa,mBAAmB,QAAQ,QAAQ;AACtD,MAAI,eAAe,GAAG;AACrB,WAAO;AAAA,EACR;AAEA,QAAM,cAAe,QAAQ,cAAc,QAAQ,UAAW;AAC9D,QAAM,aAAa,KAAK,MAAO,cAAc,aAAc,GAAG,IAAI;AAClE,SAAO,KAAK,MAAM,aAAa,EAAE;AAClC;AAcO,SAAS,aAAa,MAOlB;AACV,MAAI,KAAK,iBAAiB,KAAK,YAAY;AAC1C,WAAO;AAAA,EACR;AACA,MAAI,CAAC,YAAY,KAAK,QAAQ,UAAU,KAAK,OAAO,GAAG;AACtD,WAAO;AAAA,EACR;AACA,MAAI,KAAK,WAAW;AACnB,WAAO;AAAA,EACR;AAEA,QAAM,OAAO,mBAAmB,KAAK,OAAO;AAC5C,MAAI,CAAC,KAAK,SAAS;AAClB,WAAO;AAAA,EACR;AAEA,QAAM,WAAY,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,QAAQ,MAAM,CAAC,IAAI,MAAO,aAAa,KAAK,QAAQ,UAAU;AAC/G,SAAO,KAAK,MAAM,QAAQ,IAAI,SAAS;AACxC;AAQA,SAAS,aAAa,SAAyB;AAC9C,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,GAAG;AAC9C,WAAO;AAAA,EACR;AACA,SAAO,KAAK,IAAI,GAAG,OAAO;AAC3B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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 errors_exports = {};
|
|
20
|
+
__export(errors_exports, {
|
|
21
|
+
NotFoundError: () => NotFoundError,
|
|
22
|
+
ValidationError: () => ValidationError
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(errors_exports);
|
|
25
|
+
class ValidationError extends Error {
|
|
26
|
+
/** Issues per field, empty when the message alone describes the problem */
|
|
27
|
+
fields;
|
|
28
|
+
/**
|
|
29
|
+
* Creates the error.
|
|
30
|
+
*
|
|
31
|
+
* @param message - explanation that is safe to show to the caller
|
|
32
|
+
* @param fields - optional list of invalid fields (RFC 9457 `errors[]`)
|
|
33
|
+
*/
|
|
34
|
+
constructor(message, fields = []) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "ValidationError";
|
|
37
|
+
this.fields = fields;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
class NotFoundError extends Error {
|
|
41
|
+
/**
|
|
42
|
+
* Creates the error.
|
|
43
|
+
*
|
|
44
|
+
* @param message - explanation that is safe to show to the caller
|
|
45
|
+
*/
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "NotFoundError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
52
|
+
0 && (module.exports = {
|
|
53
|
+
NotFoundError,
|
|
54
|
+
ValidationError
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/errors.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Error types shared by the domain, the storage layer and the web layer.\n *\n * The web layer maps them onto HTTP status codes, so a caller always gets the right status without guessing\n * from a message. Anything that is **not** one of these types is treated as an internal error (`500`) and its\n * message is never exposed.\n */\n\n/** One field that the caller has to correct. */\nexport interface FieldIssue {\n\t/** Path of the field, e.g. `percent` or `entries[2].tsUtc` */\n\tpath: string;\n\t/** What is wrong with it */\n\tmessage: string;\n}\n\n/** Input that the caller can correct (missing field, wrong format, value out of range). */\nexport class ValidationError extends Error {\n\t/** Issues per field, empty when the message alone describes the problem */\n\tpublic readonly fields: FieldIssue[];\n\n\t/**\n\t * Creates the error.\n\t *\n\t * @param message - explanation that is safe to show to the caller\n\t * @param fields - optional list of invalid fields (RFC 9457 `errors[]`)\n\t */\n\tconstructor(message: string, fields: FieldIssue[] = []) {\n\t\tsuper(message);\n\t\tthis.name = \"ValidationError\";\n\t\tthis.fields = fields;\n\t}\n}\n\n/** A record the caller referred to does not exist (anymore). */\nexport class NotFoundError extends Error {\n\t/**\n\t * Creates the error.\n\t *\n\t * @param message - explanation that is safe to show to the caller\n\t */\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"NotFoundError\";\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBO,MAAM,wBAAwB,MAAM;AAAA;AAAA,EAE1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQhB,YAAY,SAAiB,SAAuB,CAAC,GAAG;AACvD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EACf;AACD;AAGO,MAAM,sBAAsB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMxC,YAAY,SAAiB;AAC5B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACb;AACD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|