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,160 @@
|
|
|
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 commands_exports = {};
|
|
20
|
+
__export(commands_exports, {
|
|
21
|
+
handleCommand: () => handleCommand,
|
|
22
|
+
punchEmployee: () => punchEmployee
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(commands_exports);
|
|
25
|
+
var import_errors = require("../errors");
|
|
26
|
+
var import_punch = require("../domain/punch");
|
|
27
|
+
var import_time = require("../util/time");
|
|
28
|
+
var import_states = require("./states");
|
|
29
|
+
function parsePeriod(value) {
|
|
30
|
+
const monthMatch = /^(\d{4})-(\d{1,2})$/.exec(value.trim());
|
|
31
|
+
if (monthMatch) {
|
|
32
|
+
const month = Number(monthMatch[2]);
|
|
33
|
+
if (month < 1 || month > 12) {
|
|
34
|
+
throw new import_errors.ValidationError(`month must be between 1 and 12 (got ${value})`);
|
|
35
|
+
}
|
|
36
|
+
return { year: Number(monthMatch[1]), month };
|
|
37
|
+
}
|
|
38
|
+
const yearMatch = /^(\d{4})$/.exec(value.trim());
|
|
39
|
+
if (yearMatch) {
|
|
40
|
+
return { year: Number(yearMatch[1]), month: null };
|
|
41
|
+
}
|
|
42
|
+
throw new import_errors.ValidationError(`expected YYYY-MM or YYYY (got "${value}")`);
|
|
43
|
+
}
|
|
44
|
+
function targetUser(deps) {
|
|
45
|
+
var _a;
|
|
46
|
+
const configured = Number((_a = deps.settings.get("command_punch_user_id")) != null ? _a : Number.NaN);
|
|
47
|
+
const explicit = Number.isInteger(configured) ? deps.users.findById(configured) : null;
|
|
48
|
+
if (explicit) {
|
|
49
|
+
return explicit;
|
|
50
|
+
}
|
|
51
|
+
const candidates = deps.users.list();
|
|
52
|
+
if (candidates.length === 1) {
|
|
53
|
+
return candidates[0];
|
|
54
|
+
}
|
|
55
|
+
if (candidates.length === 0) {
|
|
56
|
+
throw new import_errors.ValidationError("no employee exists yet");
|
|
57
|
+
}
|
|
58
|
+
throw new import_errors.ValidationError("several employees exist - set command_punch_user_id or write users.<id> commands");
|
|
59
|
+
}
|
|
60
|
+
function punchEmployee(deps, input) {
|
|
61
|
+
var _a, _b, _c;
|
|
62
|
+
const target = deps.users.findById(input.userId);
|
|
63
|
+
if (!target) {
|
|
64
|
+
throw new import_errors.ValidationError(`employee ${input.userId} does not exist`);
|
|
65
|
+
}
|
|
66
|
+
if (!target.isActive) {
|
|
67
|
+
throw new import_errors.ValidationError(`${target.displayName} is deactivated, no punch was written`);
|
|
68
|
+
}
|
|
69
|
+
const now = (_a = deps.now) != null ? _a : (() => Math.floor(Date.now() / 1e3));
|
|
70
|
+
const timestamp = (_b = input.now) != null ? _b : now();
|
|
71
|
+
const quickRound = input.quick === true ? deps.settings.getNumber("quick_round_minutes", 0) : 0;
|
|
72
|
+
const tsUtc = quickRound > 0 ? (0, import_punch.roundToStep)(timestamp, quickRound, target.timezone) : timestamp;
|
|
73
|
+
const today = (0, import_time.localDate)(tsUtc, target.timezone);
|
|
74
|
+
const existing = deps.entries.listByDate(target.id, today).map((entry) => ({ id: entry.id, tsUtc: entry.tsUtc, syncState: entry.syncState }));
|
|
75
|
+
const direction = (0, import_punch.nextDirection)((0, import_punch.buildDayPunches)(existing));
|
|
76
|
+
const stored = deps.entries.insert({
|
|
77
|
+
userId: target.id,
|
|
78
|
+
tsUtc,
|
|
79
|
+
timeZone: target.timezone,
|
|
80
|
+
source: "api",
|
|
81
|
+
direction,
|
|
82
|
+
note: (_c = input.note) != null ? _c : null,
|
|
83
|
+
now: timestamp
|
|
84
|
+
});
|
|
85
|
+
const day = deps.aggregation.recalculateDay(target.id, stored.entry.localDate, { now: timestamp });
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
message: `${target.displayName} punched ${direction} (${day.workedMin} min today, open: ${day.hasOpenEntry})`,
|
|
89
|
+
recalculated: [stored.entry.localDate]
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function handleCommand(deps, id, value) {
|
|
93
|
+
var _a;
|
|
94
|
+
const now = (_a = deps.now) != null ? _a : (() => Math.floor(Date.now() / 1e3));
|
|
95
|
+
const timestamp = now();
|
|
96
|
+
if (id === import_states.COMMAND_IDS.closeMonth || id === import_states.COMMAND_IDS.recalc) {
|
|
97
|
+
const period = parsePeriod(String(value != null ? value : ""));
|
|
98
|
+
const target = targetUser(deps);
|
|
99
|
+
if (id === import_states.COMMAND_IDS.recalc) {
|
|
100
|
+
if (period.month === null) {
|
|
101
|
+
deps.aggregation.recalculateYear(target.id, period.year, { now: timestamp });
|
|
102
|
+
return { ok: true, message: `year ${period.year} recalculated`, recalculated: [] };
|
|
103
|
+
}
|
|
104
|
+
deps.aggregation.recalculateMonth(target.id, period.year, period.month, { now: timestamp });
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
message: `month ${period.year}-${String(period.month).padStart(2, "0")} recalculated`,
|
|
108
|
+
recalculated: []
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (period.month === null) {
|
|
112
|
+
throw new import_errors.ValidationError("closing a month needs YYYY-MM");
|
|
113
|
+
}
|
|
114
|
+
const closed = deps.closing.closeMonth({
|
|
115
|
+
userId: target.id,
|
|
116
|
+
year: period.year,
|
|
117
|
+
month: period.month,
|
|
118
|
+
actorId: 0,
|
|
119
|
+
now: timestamp
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
message: `month ${period.year}-${String(period.month).padStart(2, "0")} closed for ${target.displayName} (balance ${closed.month.balanceMin} min, overtime ${closed.month.overtimeMin} min)`,
|
|
124
|
+
recalculated: []
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (id === import_states.COMMAND_IDS.punch || id === import_states.COMMAND_IDS.quickPunch) {
|
|
128
|
+
if (value !== true) {
|
|
129
|
+
return { ok: false, message: "ignored (buttons act on true)", recalculated: [] };
|
|
130
|
+
}
|
|
131
|
+
const target = targetUser(deps);
|
|
132
|
+
return punchEmployee(deps, {
|
|
133
|
+
userId: target.id,
|
|
134
|
+
quick: id === import_states.COMMAND_IDS.quickPunch,
|
|
135
|
+
note: "command.punch",
|
|
136
|
+
now: timestamp
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (id === import_states.COMMAND_IDS.backup) {
|
|
140
|
+
if (value !== true) {
|
|
141
|
+
return { ok: false, message: "ignored (buttons act on true)", recalculated: [] };
|
|
142
|
+
}
|
|
143
|
+
if (!deps.backup) {
|
|
144
|
+
throw new import_errors.ValidationError("this instance has no backup service");
|
|
145
|
+
}
|
|
146
|
+
const created = deps.backup.create({ actorId: 0, reason: "command state" });
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
message: `backup ${created.backup.name} written (${created.backup.sizeBytes} bytes, ${created.backup.entries} punches), ${created.removed.length} old file(s) removed`,
|
|
150
|
+
recalculated: []
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
throw new import_errors.ValidationError(`unknown command state ${id}`);
|
|
154
|
+
}
|
|
155
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
156
|
+
0 && (module.exports = {
|
|
157
|
+
handleCommand,
|
|
158
|
+
punchEmployee
|
|
159
|
+
});
|
|
160
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/adapter/commands.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Command states of the adapter.\n *\n * The adapter reacts to the states below, so a script, a dashboard or another adapter can punch, close a month\n * or trigger a recalculation without the REST API. Commands are triggered from inside ioBroker, not from a\n * browser, so no session is involved; the audit trail records them with `actorId: 0` (system).\n */\n\nimport type { Db } from \"../db/database\";\nimport type { EntriesRepository } from \"../db/repositories/entries\";\nimport type { SettingsRepository } from \"../db/repositories/settings\";\nimport type { UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport type { ClosingService } from \"../services/closing\";\nimport type { BackupService } from \"../services/backup\";\nimport { ValidationError } from \"../errors\";\nimport { buildDayPunches, nextDirection, roundToStep, type PunchEntry } from \"../domain/punch\";\nimport { localDate as resolveLocalDate } from \"../util/time\";\nimport { COMMAND_IDS } from \"./states\";\n\n/** Data sources of the command handler. */\nexport interface CommandDeps {\n\t/** Open database handle */\n\tdb: Db;\n\t/** Punch storage */\n\tentries: EntriesRepository;\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Instance settings (quick rounding, target employee) */\n\tsettings: SettingsRepository;\n\t/** Aggregation service */\n\taggregation: Pick<AggregationService, \"recalculateDay\" | \"recalculateMonth\" | \"recalculateYear\">;\n\t/** Closing service (month close) */\n\tclosing: Pick<ClosingService, \"closeMonth\">;\n\t/** Backup service (button `commands.backup`), optional */\n\tbackup?: Pick<BackupService, \"create\">;\n\t/** Instant source, defaults to the system clock */\n\tnow?: () => number;\n}\n\n/** Result of a command. */\nexport interface CommandResult {\n\t/** True when the command was executed */\n\tok: boolean;\n\t/** Human readable summary for the log */\n\tmessage: string;\n\t/** Local dates that were recalculated */\n\trecalculated: string[];\n}\n\n/** A period taken from a command value. */\ninterface Period {\n\t/** Four digit year */\n\tyear: number;\n\t/** Month (1 to 12) or `null` for a whole year */\n\tmonth: number | null;\n}\n\n/**\n * Parses a period string.\n *\n * @param value - `YYYY-MM` or `YYYY`\n * @returns the parsed period\n */\nfunction parsePeriod(value: string): Period {\n\tconst monthMatch = /^(\\d{4})-(\\d{1,2})$/.exec(value.trim());\n\tif (monthMatch) {\n\t\tconst month = Number(monthMatch[2]);\n\t\tif (month < 1 || month > 12) {\n\t\t\tthrow new ValidationError(`month must be between 1 and 12 (got ${value})`);\n\t\t}\n\t\treturn { year: Number(monthMatch[1]), month };\n\t}\n\n\tconst yearMatch = /^(\\d{4})$/.exec(value.trim());\n\tif (yearMatch) {\n\t\treturn { year: Number(yearMatch[1]), month: null };\n\t}\n\tthrow new ValidationError(`expected YYYY-MM or YYYY (got \"${value}\")`);\n}\n\n/**\n * Resolves the employee a command applies to: the only one, or the configured one.\n *\n * @param deps - data sources\n * @returns the employee\n */\nfunction targetUser(deps: CommandDeps): { id: number; displayName: string; timezone: string } {\n\tconst configured = Number(deps.settings.get(\"command_punch_user_id\") ?? Number.NaN);\n\tconst explicit = Number.isInteger(configured) ? deps.users.findById(configured) : null;\n\tif (explicit) {\n\t\treturn explicit;\n\t}\n\n\tconst candidates = deps.users.list();\n\tif (candidates.length === 1) {\n\t\treturn candidates[0];\n\t}\n\tif (candidates.length === 0) {\n\t\tthrow new ValidationError(\"no employee exists yet\");\n\t}\n\tthrow new ValidationError(\"several employees exist - set command_punch_user_id or write users.<id> commands\");\n}\n\n/** Data a punch needs: where to write, how to round and how to recalculate. */\nexport type PunchDeps = Pick<CommandDeps, \"entries\" | \"users\" | \"settings\" | \"aggregation\" | \"now\">;\n\n/**\n * Punches in or out for one employee.\n *\n * This is the single punch path of the adapter: the `commands.punch` button, a trigger rule and a `sendTo`\n * message all end up here, so direction, rounding, note and recalculation behave the same everywhere.\n *\n * @param deps - data sources\n * @param input - employee, rounding and note\n * @param input.userId - employee the punch belongs to\n * @param input.quick - true to round the instant with the configured quick rounding\n * @param input.note - note stored with the punch\n * @param input.now - instant of the punch, defaults to the system clock\n * @returns result of the punch\n */\nexport function punchEmployee(\n\tdeps: PunchDeps,\n\tinput: { userId: number; quick?: boolean; note?: string; now?: number },\n): CommandResult {\n\tconst target = deps.users.findById(input.userId);\n\tif (!target) {\n\t\tthrow new ValidationError(`employee ${input.userId} does not exist`);\n\t}\n\tif (!target.isActive) {\n\t\tthrow new ValidationError(`${target.displayName} is deactivated, no punch was written`);\n\t}\n\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\tconst timestamp = input.now ?? now();\n\tconst quickRound = input.quick === true ? deps.settings.getNumber(\"quick_round_minutes\", 0) : 0;\n\tconst tsUtc = quickRound > 0 ? roundToStep(timestamp, quickRound, target.timezone) : timestamp;\n\tconst today = resolveLocalDate(tsUtc, target.timezone);\n\n\tconst existing: PunchEntry[] = deps.entries\n\t\t.listByDate(target.id, today)\n\t\t.map(entry => ({ id: entry.id, tsUtc: entry.tsUtc, syncState: entry.syncState }));\n\tconst direction = nextDirection(buildDayPunches(existing));\n\n\tconst stored = deps.entries.insert({\n\t\tuserId: target.id,\n\t\ttsUtc,\n\t\ttimeZone: target.timezone,\n\t\tsource: \"api\",\n\t\tdirection,\n\t\tnote: input.note ?? null,\n\t\tnow: timestamp,\n\t});\n\tconst day = deps.aggregation.recalculateDay(target.id, stored.entry.localDate, { now: timestamp });\n\n\treturn {\n\t\tok: true,\n\t\tmessage: `${target.displayName} punched ${direction} (${day.workedMin} min today, open: ${day.hasOpenEntry})`,\n\t\trecalculated: [stored.entry.localDate],\n\t};\n}\n\n/**\n * Handles a command state.\n *\n * @param deps - data sources\n * @param id - state id that was written (without the instance prefix)\n * @param value - value that was written\n * @returns result of the command\n */\nexport function handleCommand(deps: CommandDeps, id: string, value: ioBroker.StateValue): CommandResult {\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\tconst timestamp = now();\n\n\tif (id === COMMAND_IDS.closeMonth || id === COMMAND_IDS.recalc) {\n\t\tconst period = parsePeriod(String(value ?? \"\"));\n\t\tconst target = targetUser(deps);\n\n\t\tif (id === COMMAND_IDS.recalc) {\n\t\t\tif (period.month === null) {\n\t\t\t\tdeps.aggregation.recalculateYear(target.id, period.year, { now: timestamp });\n\t\t\t\treturn { ok: true, message: `year ${period.year} recalculated`, recalculated: [] };\n\t\t\t}\n\t\t\tdeps.aggregation.recalculateMonth(target.id, period.year, period.month, { now: timestamp });\n\t\t\treturn {\n\t\t\t\tok: true,\n\t\t\t\tmessage: `month ${period.year}-${String(period.month).padStart(2, \"0\")} recalculated`,\n\t\t\t\trecalculated: [],\n\t\t\t};\n\t\t}\n\n\t\tif (period.month === null) {\n\t\t\tthrow new ValidationError(\"closing a month needs YYYY-MM\");\n\t\t}\n\t\tconst closed = deps.closing.closeMonth({\n\t\t\tuserId: target.id,\n\t\t\tyear: period.year,\n\t\t\tmonth: period.month,\n\t\t\tactorId: 0,\n\t\t\tnow: timestamp,\n\t\t});\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `month ${period.year}-${String(period.month).padStart(2, \"0\")} closed for ${target.displayName} (balance ${closed.month.balanceMin} min, overtime ${closed.month.overtimeMin} min)`,\n\t\t\trecalculated: [],\n\t\t};\n\t}\n\n\tif (id === COMMAND_IDS.punch || id === COMMAND_IDS.quickPunch) {\n\t\tif (value !== true) {\n\t\t\t// buttons only act on `true`\n\t\t\treturn { ok: false, message: \"ignored (buttons act on true)\", recalculated: [] };\n\t\t}\n\n\t\tconst target = targetUser(deps);\n\t\treturn punchEmployee(deps, {\n\t\t\tuserId: target.id,\n\t\t\tquick: id === COMMAND_IDS.quickPunch,\n\t\t\tnote: \"command.punch\",\n\t\t\tnow: timestamp,\n\t\t});\n\t}\n\n\tif (id === COMMAND_IDS.backup) {\n\t\tif (value !== true) {\n\t\t\t// buttons only act on `true`\n\t\t\treturn { ok: false, message: \"ignored (buttons act on true)\", recalculated: [] };\n\t\t}\n\t\tif (!deps.backup) {\n\t\t\tthrow new ValidationError(\"this instance has no backup service\");\n\t\t}\n\n\t\tconst created = deps.backup.create({ actorId: 0, reason: \"command state\" });\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `backup ${created.backup.name} written (${created.backup.sizeBytes} bytes, ${created.backup.entries} punches), ${created.removed.length} old file(s) removed`,\n\t\t\trecalculated: [],\n\t\t};\n\t}\n\n\tthrow new ValidationError(`unknown command state ${id}`);\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,oBAAgC;AAChC,mBAA6E;AAC7E,kBAA8C;AAC9C,oBAA4B;AA8C5B,SAAS,YAAY,OAAuB;AAC3C,QAAM,aAAa,sBAAsB,KAAK,MAAM,KAAK,CAAC;AAC1D,MAAI,YAAY;AACf,UAAM,QAAQ,OAAO,WAAW,CAAC,CAAC;AAClC,QAAI,QAAQ,KAAK,QAAQ,IAAI;AAC5B,YAAM,IAAI,8BAAgB,uCAAuC,KAAK,GAAG;AAAA,IAC1E;AACA,WAAO,EAAE,MAAM,OAAO,WAAW,CAAC,CAAC,GAAG,MAAM;AAAA,EAC7C;AAEA,QAAM,YAAY,YAAY,KAAK,MAAM,KAAK,CAAC;AAC/C,MAAI,WAAW;AACd,WAAO,EAAE,MAAM,OAAO,UAAU,CAAC,CAAC,GAAG,OAAO,KAAK;AAAA,EAClD;AACA,QAAM,IAAI,8BAAgB,kCAAkC,KAAK,IAAI;AACtE;AAQA,SAAS,WAAW,MAA0E;AAvF9F;AAwFC,QAAM,aAAa,QAAO,UAAK,SAAS,IAAI,uBAAuB,MAAzC,YAA8C,OAAO,GAAG;AAClF,QAAM,WAAW,OAAO,UAAU,UAAU,IAAI,KAAK,MAAM,SAAS,UAAU,IAAI;AAClF,MAAI,UAAU;AACb,WAAO;AAAA,EACR;AAEA,QAAM,aAAa,KAAK,MAAM,KAAK;AACnC,MAAI,WAAW,WAAW,GAAG;AAC5B,WAAO,WAAW,CAAC;AAAA,EACpB;AACA,MAAI,WAAW,WAAW,GAAG;AAC5B,UAAM,IAAI,8BAAgB,wBAAwB;AAAA,EACnD;AACA,QAAM,IAAI,8BAAgB,kFAAkF;AAC7G;AAmBO,SAAS,cACf,MACA,OACgB;AA5HjB;AA6HC,QAAM,SAAS,KAAK,MAAM,SAAS,MAAM,MAAM;AAC/C,MAAI,CAAC,QAAQ;AACZ,UAAM,IAAI,8BAAgB,YAAY,MAAM,MAAM,iBAAiB;AAAA,EACpE;AACA,MAAI,CAAC,OAAO,UAAU;AACrB,UAAM,IAAI,8BAAgB,GAAG,OAAO,WAAW,uCAAuC;AAAA,EACvF;AAEA,QAAM,OAAM,UAAK,QAAL,aAAa,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC3D,QAAM,aAAY,WAAM,QAAN,YAAa,IAAI;AACnC,QAAM,aAAa,MAAM,UAAU,OAAO,KAAK,SAAS,UAAU,uBAAuB,CAAC,IAAI;AAC9F,QAAM,QAAQ,aAAa,QAAI,0BAAY,WAAW,YAAY,OAAO,QAAQ,IAAI;AACrF,QAAM,YAAQ,YAAAA,WAAiB,OAAO,OAAO,QAAQ;AAErD,QAAM,WAAyB,KAAK,QAClC,WAAW,OAAO,IAAI,KAAK,EAC3B,IAAI,YAAU,EAAE,IAAI,MAAM,IAAI,OAAO,MAAM,OAAO,WAAW,MAAM,UAAU,EAAE;AACjF,QAAM,gBAAY,gCAAc,8BAAgB,QAAQ,CAAC;AAEzD,QAAM,SAAS,KAAK,QAAQ,OAAO;AAAA,IAClC,QAAQ,OAAO;AAAA,IACf;AAAA,IACA,UAAU,OAAO;AAAA,IACjB,QAAQ;AAAA,IACR;AAAA,IACA,OAAM,WAAM,SAAN,YAAc;AAAA,IACpB,KAAK;AAAA,EACN,CAAC;AACD,QAAM,MAAM,KAAK,YAAY,eAAe,OAAO,IAAI,OAAO,MAAM,WAAW,EAAE,KAAK,UAAU,CAAC;AAEjG,SAAO;AAAA,IACN,IAAI;AAAA,IACJ,SAAS,GAAG,OAAO,WAAW,YAAY,SAAS,KAAK,IAAI,SAAS,qBAAqB,IAAI,YAAY;AAAA,IAC1G,cAAc,CAAC,OAAO,MAAM,SAAS;AAAA,EACtC;AACD;AAUO,SAAS,cAAc,MAAmB,IAAY,OAA2C;AA1KxG;AA2KC,QAAM,OAAM,UAAK,QAAL,aAAa,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC3D,QAAM,YAAY,IAAI;AAEtB,MAAI,OAAO,0BAAY,cAAc,OAAO,0BAAY,QAAQ;AAC/D,UAAM,SAAS,YAAY,OAAO,wBAAS,EAAE,CAAC;AAC9C,UAAM,SAAS,WAAW,IAAI;AAE9B,QAAI,OAAO,0BAAY,QAAQ;AAC9B,UAAI,OAAO,UAAU,MAAM;AAC1B,aAAK,YAAY,gBAAgB,OAAO,IAAI,OAAO,MAAM,EAAE,KAAK,UAAU,CAAC;AAC3E,eAAO,EAAE,IAAI,MAAM,SAAS,QAAQ,OAAO,IAAI,iBAAiB,cAAc,CAAC,EAAE;AAAA,MAClF;AACA,WAAK,YAAY,iBAAiB,OAAO,IAAI,OAAO,MAAM,OAAO,OAAO,EAAE,KAAK,UAAU,CAAC;AAC1F,aAAO;AAAA,QACN,IAAI;AAAA,QACJ,SAAS,SAAS,OAAO,IAAI,IAAI,OAAO,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,QACtE,cAAc,CAAC;AAAA,MAChB;AAAA,IACD;AAEA,QAAI,OAAO,UAAU,MAAM;AAC1B,YAAM,IAAI,8BAAgB,+BAA+B;AAAA,IAC1D;AACA,UAAM,SAAS,KAAK,QAAQ,WAAW;AAAA,MACtC,QAAQ,OAAO;AAAA,MACf,MAAM,OAAO;AAAA,MACb,OAAO,OAAO;AAAA,MACd,SAAS;AAAA,MACT,KAAK;AAAA,IACN,CAAC;AACD,WAAO;AAAA,MACN,IAAI;AAAA,MACJ,SAAS,SAAS,OAAO,IAAI,IAAI,OAAO,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,eAAe,OAAO,WAAW,aAAa,OAAO,MAAM,UAAU,kBAAkB,OAAO,MAAM,WAAW;AAAA,MACrL,cAAc,CAAC;AAAA,IAChB;AAAA,EACD;AAEA,MAAI,OAAO,0BAAY,SAAS,OAAO,0BAAY,YAAY;AAC9D,QAAI,UAAU,MAAM;AAEnB,aAAO,EAAE,IAAI,OAAO,SAAS,iCAAiC,cAAc,CAAC,EAAE;AAAA,IAChF;AAEA,UAAM,SAAS,WAAW,IAAI;AAC9B,WAAO,cAAc,MAAM;AAAA,MAC1B,QAAQ,OAAO;AAAA,MACf,OAAO,OAAO,0BAAY;AAAA,MAC1B,MAAM;AAAA,MACN,KAAK;AAAA,IACN,CAAC;AAAA,EACF;AAEA,MAAI,OAAO,0BAAY,QAAQ;AAC9B,QAAI,UAAU,MAAM;AAEnB,aAAO,EAAE,IAAI,OAAO,SAAS,iCAAiC,cAAc,CAAC,EAAE;AAAA,IAChF;AACA,QAAI,CAAC,KAAK,QAAQ;AACjB,YAAM,IAAI,8BAAgB,qCAAqC;AAAA,IAChE;AAEA,UAAM,UAAU,KAAK,OAAO,OAAO,EAAE,SAAS,GAAG,QAAQ,gBAAgB,CAAC;AAC1E,WAAO;AAAA,MACN,IAAI;AAAA,MACJ,SAAS,UAAU,QAAQ,OAAO,IAAI,aAAa,QAAQ,OAAO,SAAS,WAAW,QAAQ,OAAO,OAAO,cAAc,QAAQ,QAAQ,MAAM;AAAA,MAChJ,cAAc,CAAC;AAAA,IAChB;AAAA,EACD;AAEA,QAAM,IAAI,8BAAgB,yBAAyB,EAAE,EAAE;AACxD;",
|
|
6
|
+
"names": ["resolveLocalDate"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
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 messages_exports = {};
|
|
20
|
+
__export(messages_exports, {
|
|
21
|
+
handleMessage: () => handleMessage
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(messages_exports);
|
|
24
|
+
var import_errors = require("../errors");
|
|
25
|
+
var import_pdf = require("../reports/pdf");
|
|
26
|
+
var import_labels = require("../reports/labels");
|
|
27
|
+
var import_types = require("../reports/types");
|
|
28
|
+
var import_xls = require("../reports/xls");
|
|
29
|
+
var import_time = require("../util/time");
|
|
30
|
+
var import_commands = require("./commands");
|
|
31
|
+
var import_presence = require("./presence");
|
|
32
|
+
var import_states = require("./states");
|
|
33
|
+
var import_triggers = require("./triggers");
|
|
34
|
+
function textField(payload, field) {
|
|
35
|
+
const value = payload[field];
|
|
36
|
+
if (value === void 0 || value === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
if (typeof value !== "string") {
|
|
43
|
+
throw new import_errors.ValidationError(`${field} must be a string (got ${typeof value})`);
|
|
44
|
+
}
|
|
45
|
+
return value.trim() || null;
|
|
46
|
+
}
|
|
47
|
+
function booleanField(payload, field) {
|
|
48
|
+
const value = payload[field];
|
|
49
|
+
if (value === void 0 || value === null) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === "boolean") {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === "number") {
|
|
56
|
+
return value !== 0;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
const normalized = value.trim().toLowerCase();
|
|
60
|
+
if (["true", "1", "yes", "on"].includes(normalized)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (["false", "0", "no", "off", ""].includes(normalized)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw new import_errors.ValidationError(`${field} must be a boolean (got ${JSON.stringify(value)})`);
|
|
68
|
+
}
|
|
69
|
+
function targetUser(payload, users) {
|
|
70
|
+
var _a;
|
|
71
|
+
const raw = (_a = payload.user) != null ? _a : payload.userId;
|
|
72
|
+
if (raw === void 0 || raw === null || raw === "") {
|
|
73
|
+
throw new import_errors.ValidationError("user is required: pass a user id, a login or the shown name");
|
|
74
|
+
}
|
|
75
|
+
const asText = typeof raw === "string" || typeof raw === "number" ? String(raw) : JSON.stringify(raw);
|
|
76
|
+
const user = (0, import_triggers.resolveTriggerUser)(typeof raw === "number" ? raw : asText, users);
|
|
77
|
+
if (!user) {
|
|
78
|
+
throw new import_errors.ValidationError(`no employee matches ${asText}`);
|
|
79
|
+
}
|
|
80
|
+
return user;
|
|
81
|
+
}
|
|
82
|
+
function statementInput(deps, user, year, month) {
|
|
83
|
+
var _a, _b;
|
|
84
|
+
const timestamp = ((_a = deps.now) != null ? _a : (() => Math.floor(Date.now() / 1e3)))();
|
|
85
|
+
const prefix = `${year}-${String(month).padStart(2, "0")}`;
|
|
86
|
+
const first = `${prefix}-01`;
|
|
87
|
+
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
88
|
+
const last = `${prefix}-${String(lastDay).padStart(2, "0")}`;
|
|
89
|
+
deps.aggregation.recalculateRange(user.id, first, last, { now: timestamp });
|
|
90
|
+
const { labels, language } = (0, import_labels.reportLabels)(user.locale);
|
|
91
|
+
return {
|
|
92
|
+
user: { displayName: user.displayName, login: user.login, timezone: user.timezone },
|
|
93
|
+
labels,
|
|
94
|
+
language,
|
|
95
|
+
locale: user.locale || "en",
|
|
96
|
+
year,
|
|
97
|
+
month,
|
|
98
|
+
days: deps.aggregation.days(user.id, first, last),
|
|
99
|
+
absences: deps.absences.withTypesInRange(user.id, first, last).map((absence) => ({
|
|
100
|
+
typeCode: absence.typeCode,
|
|
101
|
+
typeName: absence.typeName,
|
|
102
|
+
dateFrom: absence.dateFrom,
|
|
103
|
+
dateTo: absence.dateTo,
|
|
104
|
+
dayPortion: absence.dayPortion,
|
|
105
|
+
hours: absence.hours
|
|
106
|
+
})),
|
|
107
|
+
generatedAt: timestamp,
|
|
108
|
+
generator: `time-tracker ${(_b = deps.version) != null ? _b : ""}`.trim()
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function handleMessage(deps, command, payload) {
|
|
112
|
+
var _a, _b, _c, _d, _e;
|
|
113
|
+
const body = payload != null ? payload : {};
|
|
114
|
+
const now = (_a = deps.now) != null ? _a : (() => Math.floor(Date.now() / 1e3));
|
|
115
|
+
if (command === "punch") {
|
|
116
|
+
const user = targetUser(body, deps.users);
|
|
117
|
+
const result = (0, import_commands.punchEmployee)(
|
|
118
|
+
{ entries: deps.entries, users: deps.users, settings: deps.settings, aggregation: deps.aggregation },
|
|
119
|
+
{
|
|
120
|
+
userId: user.id,
|
|
121
|
+
quick: booleanField(body, "quick") === true,
|
|
122
|
+
note: (_b = textField(body, "note")) != null ? _b : "sendTo.punch",
|
|
123
|
+
now: now()
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
return { ok: true, message: result.message, data: { userId: user.id, message: result.message } };
|
|
127
|
+
}
|
|
128
|
+
if (command === "present") {
|
|
129
|
+
const user = targetUser(body, deps.users);
|
|
130
|
+
const desired = booleanField(body, "present");
|
|
131
|
+
if (desired === null) {
|
|
132
|
+
throw new import_errors.ValidationError("present is required: pass true or false");
|
|
133
|
+
}
|
|
134
|
+
const result = (0, import_presence.handlePresenceState)(
|
|
135
|
+
{ entries: deps.entries, users: deps.users, aggregation: deps.aggregation, now },
|
|
136
|
+
`users.${user.id}.present`,
|
|
137
|
+
desired
|
|
138
|
+
);
|
|
139
|
+
return {
|
|
140
|
+
ok: result.ok,
|
|
141
|
+
message: result.message,
|
|
142
|
+
data: { userId: user.id, present: result.present, changed: result.changed }
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (command === "status") {
|
|
146
|
+
const user = targetUser(body, deps.users);
|
|
147
|
+
const snapshot = (0, import_states.readUserSnapshot)({
|
|
148
|
+
aggregation: deps.aggregation,
|
|
149
|
+
users: deps.users,
|
|
150
|
+
sync: deps.sync,
|
|
151
|
+
userId: user.id,
|
|
152
|
+
now: now()
|
|
153
|
+
});
|
|
154
|
+
if (!snapshot) {
|
|
155
|
+
throw new import_errors.ValidationError(`employee ${user.id} disappeared`);
|
|
156
|
+
}
|
|
157
|
+
return { ok: true, message: `figures of ${user.displayName}`, data: { ...snapshot } };
|
|
158
|
+
}
|
|
159
|
+
if (command === "report") {
|
|
160
|
+
const user = targetUser(body, deps.users);
|
|
161
|
+
const format = ((_c = textField(body, "format")) != null ? _c : "pdf").toLowerCase();
|
|
162
|
+
if (format !== "pdf" && format !== "xlsx") {
|
|
163
|
+
throw new import_errors.ValidationError(`format must be pdf or xlsx (got ${format})`);
|
|
164
|
+
}
|
|
165
|
+
const period = (_d = textField(body, "period")) != null ? _d : (0, import_time.localDate)(now(), user.timezone).slice(0, 7);
|
|
166
|
+
const match = /^(\d{4})-(\d{2})$/.exec(period);
|
|
167
|
+
if (!match) {
|
|
168
|
+
throw new import_errors.ValidationError(`period must be YYYY-MM (got "${period}")`);
|
|
169
|
+
}
|
|
170
|
+
const year = Number(match[1]);
|
|
171
|
+
const month = Number(match[2]);
|
|
172
|
+
if (month < 1 || month > 12) {
|
|
173
|
+
throw new import_errors.ValidationError(`month must be between 1 and 12 (got ${month})`);
|
|
174
|
+
}
|
|
175
|
+
const input = statementInput(deps, user, year, month);
|
|
176
|
+
const file = format === "pdf" ? await (0, import_pdf.buildMonthStatement)({ ...input, fontPath: (_e = deps.settings.get("report_font_path")) != null ? _e : "" }) : await (0, import_xls.buildMonthReport)(input);
|
|
177
|
+
const fileName = (0, import_types.reportFileName)(user.login, year, month, format === "pdf" ? "pdf" : "xlsx");
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
message: `${fileName} (${file.length} bytes)`,
|
|
181
|
+
data: {
|
|
182
|
+
userId: user.id,
|
|
183
|
+
fileName,
|
|
184
|
+
mimeType: format === "pdf" ? "application/pdf" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
185
|
+
bytes: file.length,
|
|
186
|
+
base64: file.toString("base64")
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (command === "backup") {
|
|
191
|
+
if (!deps.backup) {
|
|
192
|
+
throw new import_errors.ValidationError("this instance has no backup service");
|
|
193
|
+
}
|
|
194
|
+
const created = deps.backup.create({ actorId: 0, reason: "sendTo" });
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
message: `backup ${created.backup.name} written (${created.backup.sizeBytes} bytes)`,
|
|
198
|
+
data: { name: created.backup.name, sizeBytes: created.backup.sizeBytes, removed: created.removed }
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
throw new import_errors.ValidationError(`unknown command "${command}": use punch, present, status, report or backup`);
|
|
202
|
+
}
|
|
203
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
204
|
+
0 && (module.exports = {
|
|
205
|
+
handleMessage
|
|
206
|
+
});
|
|
207
|
+
//# sourceMappingURL=messages.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/adapter/messages.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Messages of the adapter (`sendTo`).\n *\n * A script adapter, Blockly or another adapter can drive the time tracking without HTTP and without a session:\n * `sendTo(\"time-tracker.0\", \"punch\", { user: \"anna\" }, callback)`. Every command answers with an object, so the\n * caller can react on the outcome.\n *\n * | Command | Payload | Answer |\n * | --------- | ----------------------------------------- | ------------------------------------- |\n * | `punch` | `{ user, quick?, note? }` | `{ userId, message }` |\n * | `present` | `{ user, present }` | `{ userId, present, changed }` |\n * | `status` | `{ user }` | figures of the employee |\n * | `report` | `{ user, period?, format? }` | `{ fileName, mimeType, base64 }` |\n * | `backup` | \u2014 | `{ name, sizeBytes }` |\n *\n * `user` is a user id, a login or the shown name \u2014 the same resolution the trigger rules use.\n */\n\nimport type { AbsencesRepository } from \"../db/repositories/absences\";\nimport type { EntriesRepository } from \"../db/repositories/entries\";\nimport type { SettingsRepository } from \"../db/repositories/settings\";\nimport type { UserRecord, UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport type { BackupService } from \"../services/backup\";\nimport type { SyncService } from \"../services/sync\";\nimport { ValidationError } from \"../errors\";\nimport { buildMonthStatement } from \"../reports/pdf\";\nimport { reportLabels } from \"../reports/labels\";\nimport { reportFileName, type ReportInput } from \"../reports/types\";\nimport { buildMonthReport } from \"../reports/xls\";\nimport { localDate } from \"../util/time\";\nimport { punchEmployee } from \"./commands\";\nimport { handlePresenceState } from \"./presence\";\nimport { readUserSnapshot } from \"./states\";\nimport { resolveTriggerUser } from \"./triggers\";\n\n/** Data a message needs. */\nexport interface MessageDeps {\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Punch storage */\n\tentries: EntriesRepository;\n\t/** Absences (the monthly statement shows them) */\n\tabsences: AbsencesRepository;\n\t/** Instance settings (quick rounding, font of the statements) */\n\tsettings: SettingsRepository;\n\t/** Aggregation service */\n\taggregation: AggregationService;\n\t/** Synchronisation service (open conflicts) */\n\tsync: SyncService;\n\t/** Backup service, optional: without it the command is refused */\n\tbackup?: Pick<BackupService, \"create\">;\n\t/** Version printed into a generated statement */\n\tversion?: string;\n\t/** Instant source, defaults to the system clock */\n\tnow?: () => number;\n}\n\n/** Answer of a message. */\nexport interface MessageResult {\n\t/** True when the command ran */\n\tok: boolean;\n\t/** Human readable summary */\n\tmessage: string;\n\t/** Command specific answer for the caller of `sendTo` */\n\tdata?: Record<string, unknown>;\n}\n\n/**\n * Reads a text field of a message payload.\n *\n * @param payload - payload of the message\n * @param field - field name\n * @returns the trimmed text or `null`\n */\nfunction textField(payload: Record<string, unknown>, field: string): string | null {\n\tconst value = payload[field];\n\tif (value === undefined || value === null) {\n\t\treturn null;\n\t}\n\tif (typeof value === \"number\" || typeof value === \"boolean\") {\n\t\treturn String(value);\n\t}\n\tif (typeof value !== \"string\") {\n\t\tthrow new ValidationError(`${field} must be a string (got ${typeof value})`);\n\t}\n\treturn value.trim() || null;\n}\n\n/**\n * Reads a boolean field of a message payload.\n *\n * @param payload - payload of the message\n * @param field - field name\n * @returns the boolean, or `null` when the field is missing\n */\nfunction booleanField(payload: Record<string, unknown>, field: string): boolean | null {\n\tconst value = payload[field];\n\tif (value === undefined || value === null) {\n\t\treturn null;\n\t}\n\tif (typeof value === \"boolean\") {\n\t\treturn value;\n\t}\n\tif (typeof value === \"number\") {\n\t\treturn value !== 0;\n\t}\n\tif (typeof value === \"string\") {\n\t\tconst normalized = value.trim().toLowerCase();\n\t\tif ([\"true\", \"1\", \"yes\", \"on\"].includes(normalized)) {\n\t\t\treturn true;\n\t\t}\n\t\tif ([\"false\", \"0\", \"no\", \"off\", \"\"].includes(normalized)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\tthrow new ValidationError(`${field} must be a boolean (got ${JSON.stringify(value)})`);\n}\n\n/**\n * Resolves the employee a message names.\n *\n * @param payload - payload of the message\n * @param users - user storage\n * @returns the employee\n */\nfunction targetUser(payload: Record<string, unknown>, users: UsersRepository): UserRecord {\n\tconst raw = payload.user ?? payload.userId;\n\tif (raw === undefined || raw === null || raw === \"\") {\n\t\tthrow new ValidationError(\"user is required: pass a user id, a login or the shown name\");\n\t}\n\t// a value of another type cannot name an employee; it is only reported back\n\tconst asText = typeof raw === \"string\" || typeof raw === \"number\" ? String(raw) : JSON.stringify(raw);\n\tconst user = resolveTriggerUser(typeof raw === \"number\" ? raw : asText, users);\n\tif (!user) {\n\t\tthrow new ValidationError(`no employee matches ${asText}`);\n\t}\n\treturn user;\n}\n\n/**\n * Builds the input of a monthly statement.\n *\n * @param deps - data sources\n * @param user - employee the statement belongs to\n * @param year - four digit year\n * @param month - month, 1 to 12\n * @returns the report input including the generator line\n */\nfunction statementInput(\n\tdeps: MessageDeps,\n\tuser: UserRecord,\n\tyear: number,\n\tmonth: number,\n): ReportInput & { generator: string } {\n\tconst timestamp = (deps.now ?? (() => Math.floor(Date.now() / 1000)))();\n\tconst prefix = `${year}-${String(month).padStart(2, \"0\")}`;\n\tconst first = `${prefix}-01`;\n\tconst lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();\n\tconst last = `${prefix}-${String(lastDay).padStart(2, \"0\")}`;\n\t// a statement has to show the current state of the month, so the range is recalculated first\n\tdeps.aggregation.recalculateRange(user.id, first, last, { now: timestamp });\n\tconst { labels, language } = reportLabels(user.locale);\n\n\treturn {\n\t\tuser: { displayName: user.displayName, login: user.login, timezone: user.timezone },\n\t\tlabels,\n\t\tlanguage,\n\t\tlocale: user.locale || \"en\",\n\t\tyear,\n\t\tmonth,\n\t\tdays: deps.aggregation.days(user.id, first, last),\n\t\tabsences: deps.absences.withTypesInRange(user.id, first, last).map(absence => ({\n\t\t\ttypeCode: absence.typeCode,\n\t\t\ttypeName: absence.typeName,\n\t\t\tdateFrom: absence.dateFrom,\n\t\t\tdateTo: absence.dateTo,\n\t\t\tdayPortion: absence.dayPortion,\n\t\t\thours: absence.hours,\n\t\t})),\n\t\tgeneratedAt: timestamp,\n\t\tgenerator: `time-tracker ${deps.version ?? \"\"}`.trim(),\n\t};\n}\n\n/**\n * Applies a message of a script, a Blockly block or another adapter.\n *\n * @param deps - data sources\n * @param command - name of the command\n * @param payload - payload of the message\n * @returns the answer for the caller\n */\nexport async function handleMessage(deps: MessageDeps, command: string, payload: unknown): Promise<MessageResult> {\n\tconst body = (payload ?? {}) as Record<string, unknown>;\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\n\tif (command === \"punch\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst result = punchEmployee(\n\t\t\t{ entries: deps.entries, users: deps.users, settings: deps.settings, aggregation: deps.aggregation },\n\t\t\t{\n\t\t\t\tuserId: user.id,\n\t\t\t\tquick: booleanField(body, \"quick\") === true,\n\t\t\t\tnote: textField(body, \"note\") ?? \"sendTo.punch\",\n\t\t\t\tnow: now(),\n\t\t\t},\n\t\t);\n\t\treturn { ok: true, message: result.message, data: { userId: user.id, message: result.message } };\n\t}\n\n\tif (command === \"present\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst desired = booleanField(body, \"present\");\n\t\tif (desired === null) {\n\t\t\tthrow new ValidationError(\"present is required: pass true or false\");\n\t\t}\n\t\tconst result = handlePresenceState(\n\t\t\t{ entries: deps.entries, users: deps.users, aggregation: deps.aggregation, now },\n\t\t\t`users.${user.id}.present`,\n\t\t\tdesired,\n\t\t);\n\t\treturn {\n\t\t\tok: result.ok,\n\t\t\tmessage: result.message,\n\t\t\tdata: { userId: user.id, present: result.present, changed: result.changed },\n\t\t};\n\t}\n\n\tif (command === \"status\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst snapshot = readUserSnapshot({\n\t\t\taggregation: deps.aggregation,\n\t\t\tusers: deps.users,\n\t\t\tsync: deps.sync,\n\t\t\tuserId: user.id,\n\t\t\tnow: now(),\n\t\t});\n\t\tif (!snapshot) {\n\t\t\tthrow new ValidationError(`employee ${user.id} disappeared`);\n\t\t}\n\t\treturn { ok: true, message: `figures of ${user.displayName}`, data: { ...snapshot } };\n\t}\n\n\tif (command === \"report\") {\n\t\tconst user = targetUser(body, deps.users);\n\t\tconst format = (textField(body, \"format\") ?? \"pdf\").toLowerCase();\n\t\tif (format !== \"pdf\" && format !== \"xlsx\") {\n\t\t\tthrow new ValidationError(`format must be pdf or xlsx (got ${format})`);\n\t\t}\n\t\tconst period = textField(body, \"period\") ?? localDate(now(), user.timezone).slice(0, 7);\n\t\tconst match = /^(\\d{4})-(\\d{2})$/.exec(period);\n\t\tif (!match) {\n\t\t\tthrow new ValidationError(`period must be YYYY-MM (got \"${period}\")`);\n\t\t}\n\t\tconst year = Number(match[1]);\n\t\tconst month = Number(match[2]);\n\t\tif (month < 1 || month > 12) {\n\t\t\tthrow new ValidationError(`month must be between 1 and 12 (got ${month})`);\n\t\t}\n\n\t\tconst input = statementInput(deps, user, year, month);\n\t\tconst file =\n\t\t\tformat === \"pdf\"\n\t\t\t\t? await buildMonthStatement({ ...input, fontPath: deps.settings.get(\"report_font_path\") ?? \"\" })\n\t\t\t\t: await buildMonthReport(input);\n\t\tconst fileName = reportFileName(user.login, year, month, format === \"pdf\" ? \"pdf\" : \"xlsx\");\n\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `${fileName} (${file.length} bytes)`,\n\t\t\tdata: {\n\t\t\t\tuserId: user.id,\n\t\t\t\tfileName,\n\t\t\t\tmimeType:\n\t\t\t\t\tformat === \"pdf\"\n\t\t\t\t\t\t? \"application/pdf\"\n\t\t\t\t\t\t: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\t\t\t\tbytes: file.length,\n\t\t\t\tbase64: file.toString(\"base64\"),\n\t\t\t},\n\t\t};\n\t}\n\n\tif (command === \"backup\") {\n\t\tif (!deps.backup) {\n\t\t\tthrow new ValidationError(\"this instance has no backup service\");\n\t\t}\n\t\tconst created = deps.backup.create({ actorId: 0, reason: \"sendTo\" });\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `backup ${created.backup.name} written (${created.backup.sizeBytes} bytes)`,\n\t\t\tdata: { name: created.backup.name, sizeBytes: created.backup.sizeBytes, removed: created.removed },\n\t\t};\n\t}\n\n\tthrow new ValidationError(`unknown command \"${command}\": use punch, present, status, report or backup`);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
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 presence_exports = {};
|
|
20
|
+
__export(presence_exports, {
|
|
21
|
+
PRESENCE_SUFFIX: () => PRESENCE_SUFFIX,
|
|
22
|
+
handlePresenceState: () => handlePresenceState,
|
|
23
|
+
parsePresenceStateId: () => parsePresenceStateId,
|
|
24
|
+
readPresenceValue: () => readPresenceValue
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(presence_exports);
|
|
27
|
+
var import_punch = require("../domain/punch");
|
|
28
|
+
var import_time = require("../util/time");
|
|
29
|
+
const PRESENCE_SUFFIX = "present";
|
|
30
|
+
function parsePresenceStateId(localId) {
|
|
31
|
+
const match = /^users\.(\d+)\.present$/.exec(localId.trim());
|
|
32
|
+
return match ? Number(match[1]) : null;
|
|
33
|
+
}
|
|
34
|
+
function readPresenceValue(value) {
|
|
35
|
+
if (typeof value === "boolean") {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "number") {
|
|
39
|
+
return value === 1 ? true : value === 0 ? false : null;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
const normalized = value.trim().toLowerCase();
|
|
43
|
+
if (normalized === "true" || normalized === "1" || normalized === "on") {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (normalized === "false" || normalized === "0" || normalized === "off" || normalized === "") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function handlePresenceState(deps, localId, value) {
|
|
53
|
+
var _a;
|
|
54
|
+
const userId = parsePresenceStateId(localId);
|
|
55
|
+
if (userId === null) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
message: `"${localId}" is not a presence state`,
|
|
59
|
+
userId: null,
|
|
60
|
+
present: false,
|
|
61
|
+
changed: false
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const desired = readPresenceValue(value);
|
|
65
|
+
if (desired === null) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
message: `write true or false (got ${JSON.stringify(value)})`,
|
|
69
|
+
userId,
|
|
70
|
+
present: false,
|
|
71
|
+
changed: false
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const user = deps.users.findById(userId);
|
|
75
|
+
if (!user) {
|
|
76
|
+
return { ok: false, message: `employee ${userId} does not exist`, userId, present: false, changed: false };
|
|
77
|
+
}
|
|
78
|
+
if (!user.isActive) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
message: `${user.displayName} is deactivated, no punch was written`,
|
|
82
|
+
userId,
|
|
83
|
+
present: false,
|
|
84
|
+
changed: false
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const now = (_a = deps.now) != null ? _a : (() => Math.floor(Date.now() / 1e3));
|
|
88
|
+
const timestamp = now();
|
|
89
|
+
const today = (0, import_time.localDate)(timestamp, user.timezone);
|
|
90
|
+
const existing = deps.entries.listByDate(user.id, today).map((entry) => ({ id: entry.id, tsUtc: entry.tsUtc, syncState: entry.syncState }));
|
|
91
|
+
const present = (0, import_punch.nextDirection)((0, import_punch.buildDayPunches)(existing)) === "out";
|
|
92
|
+
if (present === desired) {
|
|
93
|
+
return {
|
|
94
|
+
ok: true,
|
|
95
|
+
message: `${user.displayName} is already ${desired ? "present" : "absent"} (no punch written)`,
|
|
96
|
+
userId: user.id,
|
|
97
|
+
present: desired,
|
|
98
|
+
changed: false,
|
|
99
|
+
localDate: today
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const direction = desired ? "in" : "out";
|
|
103
|
+
const stored = deps.entries.insert({
|
|
104
|
+
userId: user.id,
|
|
105
|
+
tsUtc: timestamp,
|
|
106
|
+
timeZone: user.timezone,
|
|
107
|
+
source: "api",
|
|
108
|
+
direction,
|
|
109
|
+
note: "state.present",
|
|
110
|
+
now: timestamp
|
|
111
|
+
});
|
|
112
|
+
const day = deps.aggregation.recalculateDay(user.id, stored.entry.localDate, { now: timestamp });
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
message: `${user.displayName} punched ${direction} from the presence state (${day.workedMin} min today, present: ${day.hasOpenEntry})`,
|
|
116
|
+
userId: user.id,
|
|
117
|
+
present: day.hasOpenEntry,
|
|
118
|
+
changed: true,
|
|
119
|
+
localDate: stored.entry.localDate
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
123
|
+
0 && (module.exports = {
|
|
124
|
+
PRESENCE_SUFFIX,
|
|
125
|
+
handlePresenceState,
|
|
126
|
+
parsePresenceStateId,
|
|
127
|
+
readPresenceValue
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=presence.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/adapter/presence.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Presence state of an employee (`users.<userId>.present`).\n *\n * `present` is the writable twin of `users.<userId>.hasOpenEntry`: writing `true` opens an entry \u2014 paid working\n * time starts \u2014 and writing `false` closes it. A fingerprint reader, an RFID bridge, a dashboard or a plain script\n * can therefore tell the adapter that somebody arrived or left, without speaking to the REST API.\n *\n * The write is idempotent: `true` while the day is already open does not create a second punch, so a reader that\n * fires twice is harmless. Punches written this way are recorded like any other one (`source: \"api\"`, note\n * `state.present`), so they appear in the entry list and in the audit trail, just like `commands.*`.\n */\n\nimport type { EntriesRepository } from \"../db/repositories/entries\";\nimport type { UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport { buildDayPunches, nextDirection, type PunchEntry } from \"../domain/punch\";\nimport { localDate as resolveLocalDate } from \"../util/time\";\n\n/** Suffix of the writable presence state. */\nexport const PRESENCE_SUFFIX = \"present\";\n\n/** Data sources of the presence handler. */\nexport interface PresenceDeps {\n\t/** Punch storage */\n\tentries: EntriesRepository;\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Aggregation service */\n\taggregation: Pick<AggregationService, \"recalculateDay\">;\n\t/** Instant source, defaults to the system clock */\n\tnow?: () => number;\n}\n\n/** Result of a write on a presence state. */\nexport interface PresenceResult {\n\t/** True when the write was understood, even when nothing changed */\n\tok: boolean;\n\t/** Human readable summary for the log */\n\tmessage: string;\n\t/** Employee the write was meant for, `null` when the id was not a presence state */\n\tuserId: number | null;\n\t/** Whether the employee is present after the write */\n\tpresent: boolean;\n\t/** True when a punch was written */\n\tchanged: boolean;\n\t/** Local date the punch belongs to */\n\tlocalDate?: string;\n}\n\n/**\n * Reads the employee id from a presence state id.\n *\n * @param localId - state id without the instance prefix\n * @returns the employee id, or `null` when the id is not a presence state\n */\nexport function parsePresenceStateId(localId: string): number | null {\n\tconst match = /^users\\.(\\d+)\\.present$/.exec(localId.trim());\n\treturn match ? Number(match[1]) : null;\n}\n\n/**\n * Reads a boolean from a written state value.\n *\n * Dashboards and script adapters deliver `true`, `\"true\"`, `1` or `\"1\"` \u2014 and the opposites.\n *\n * @param value - value that was written\n * @returns the boolean, or `null` when the value is not boolean-like\n */\nexport function readPresenceValue(value: ioBroker.StateValue): boolean | null {\n\tif (typeof value === \"boolean\") {\n\t\treturn value;\n\t}\n\tif (typeof value === \"number\") {\n\t\treturn value === 1 ? true : value === 0 ? false : null;\n\t}\n\tif (typeof value === \"string\") {\n\t\tconst normalized = value.trim().toLowerCase();\n\t\tif (normalized === \"true\" || normalized === \"1\" || normalized === \"on\") {\n\t\t\treturn true;\n\t\t}\n\t\tif (normalized === \"false\" || normalized === \"0\" || normalized === \"off\" || normalized === \"\") {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Applies a write on `users.<id>.present`.\n *\n * @param deps - data sources\n * @param localId - state id without the instance prefix\n * @param value - value that was written\n * @returns what happened, so the caller can log it and republish the figures\n */\nexport function handlePresenceState(deps: PresenceDeps, localId: string, value: ioBroker.StateValue): PresenceResult {\n\tconst userId = parsePresenceStateId(localId);\n\tif (userId === null) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `\"${localId}\" is not a presence state`,\n\t\t\tuserId: null,\n\t\t\tpresent: false,\n\t\t\tchanged: false,\n\t\t};\n\t}\n\n\tconst desired = readPresenceValue(value);\n\tif (desired === null) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `write true or false (got ${JSON.stringify(value)})`,\n\t\t\tuserId,\n\t\t\tpresent: false,\n\t\t\tchanged: false,\n\t\t};\n\t}\n\n\tconst user = deps.users.findById(userId);\n\tif (!user) {\n\t\treturn { ok: false, message: `employee ${userId} does not exist`, userId, present: false, changed: false };\n\t}\n\tif (!user.isActive) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tmessage: `${user.displayName} is deactivated, no punch was written`,\n\t\t\tuserId,\n\t\t\tpresent: false,\n\t\t\tchanged: false,\n\t\t};\n\t}\n\n\tconst now = deps.now ?? (() => Math.floor(Date.now() / 1000));\n\tconst timestamp = now();\n\tconst today = resolveLocalDate(timestamp, user.timezone);\n\tconst existing: PunchEntry[] = deps.entries\n\t\t.listByDate(user.id, today)\n\t\t.map(entry => ({ id: entry.id, tsUtc: entry.tsUtc, syncState: entry.syncState }));\n\tconst present = nextDirection(buildDayPunches(existing)) === \"out\";\n\n\tif (present === desired) {\n\t\t// the reader fired twice, the dashboard was refreshed or a script repeats itself: nothing to store\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmessage: `${user.displayName} is already ${desired ? \"present\" : \"absent\"} (no punch written)`,\n\t\t\tuserId: user.id,\n\t\t\tpresent: desired,\n\t\t\tchanged: false,\n\t\t\tlocalDate: today,\n\t\t};\n\t}\n\n\tconst direction = desired ? \"in\" : \"out\";\n\tconst stored = deps.entries.insert({\n\t\tuserId: user.id,\n\t\ttsUtc: timestamp,\n\t\ttimeZone: user.timezone,\n\t\tsource: \"api\",\n\t\tdirection,\n\t\tnote: \"state.present\",\n\t\tnow: timestamp,\n\t});\n\tconst day = deps.aggregation.recalculateDay(user.id, stored.entry.localDate, { now: timestamp });\n\n\treturn {\n\t\tok: true,\n\t\tmessage: `${user.displayName} punched ${direction} from the presence state (${day.workedMin} min today, present: ${day.hasOpenEntry})`,\n\t\tuserId: user.id,\n\t\tpresent: day.hasOpenEntry,\n\t\tchanged: true,\n\t\tlocalDate: stored.entry.localDate,\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["resolveLocalDate"]
|
|
7
|
+
}
|