iobroker.time-tracker 0.2.0 → 0.2.2
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/README.md +14 -26
- package/build/lib/adapter/stateNames.js +520 -0
- package/build/lib/adapter/stateNames.js.map +7 -0
- package/build/lib/adapter/states.js +70 -114
- package/build/lib/adapter/states.js.map +2 -2
- package/io-package.json +27 -79
- package/package.json +1 -1
- package/www/assets/index-QI3wrEvW.js.map +1 -1
- package/www/sw.js.map +1 -1
|
@@ -32,6 +32,7 @@ __export(states_exports, {
|
|
|
32
32
|
readUserSnapshot: () => readUserSnapshot
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(states_exports);
|
|
35
|
+
var import_stateNames = require("./stateNames");
|
|
35
36
|
var import_time = require("../util/time");
|
|
36
37
|
const COMMAND_IDS = {
|
|
37
38
|
/** Employee the commands apply to */
|
|
@@ -64,6 +65,11 @@ function stateObject(name, type, role, options = {}) {
|
|
|
64
65
|
function channelObject(name) {
|
|
65
66
|
return { type: "channel", common: { name }, native: {} };
|
|
66
67
|
}
|
|
68
|
+
async function ensureObject(port, id, object) {
|
|
69
|
+
var _a;
|
|
70
|
+
await port.setObjectNotExists(id, object);
|
|
71
|
+
await port.extendObject(id, { common: { name: (_a = object.common) == null ? void 0 : _a.name } });
|
|
72
|
+
}
|
|
67
73
|
function readUserSnapshot(args) {
|
|
68
74
|
var _a, _b, _c;
|
|
69
75
|
const user = args.users.findById(args.userId);
|
|
@@ -125,143 +131,108 @@ async function publishAllUserStates(args) {
|
|
|
125
131
|
}
|
|
126
132
|
async function createUserChannel(port, userId) {
|
|
127
133
|
const id = `users.${userId}`;
|
|
128
|
-
await port
|
|
129
|
-
await port
|
|
130
|
-
await port
|
|
131
|
-
await
|
|
134
|
+
await ensureObject(port, "users", channelObject(import_stateNames.STATE_NAMES.usersChannel));
|
|
135
|
+
await ensureObject(port, id, channelObject(import_stateNames.STATE_NAMES.employeeChannel));
|
|
136
|
+
await ensureObject(port, `${id}.displayName`, stateObject(import_stateNames.STATE_NAMES.displayName, "string", "info.name"));
|
|
137
|
+
await ensureObject(
|
|
138
|
+
port,
|
|
132
139
|
`${id}.present`,
|
|
133
|
-
stateObject(
|
|
134
|
-
{ en: "Present (working time is running)", de: "Anwesenheit (Arbeitszeit l\xE4uft)" },
|
|
135
|
-
"boolean",
|
|
136
|
-
"switch",
|
|
137
|
-
{ write: true }
|
|
138
|
-
)
|
|
140
|
+
stateObject(import_stateNames.STATE_NAMES.userPresent, "boolean", "switch", { write: true })
|
|
139
141
|
);
|
|
140
|
-
await
|
|
142
|
+
await ensureObject(
|
|
143
|
+
port,
|
|
141
144
|
`${id}.hasOpenEntry`,
|
|
142
|
-
stateObject(
|
|
145
|
+
stateObject(import_stateNames.STATE_NAMES.hasOpenEntry, "boolean", "indicator.working")
|
|
143
146
|
);
|
|
144
|
-
await port.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
);
|
|
148
|
-
await port.setObjectNotExists(
|
|
147
|
+
await ensureObject(port, `${id}.lastPunch`, stateObject(import_stateNames.STATE_NAMES.lastPunch, "number", "value.time"));
|
|
148
|
+
await ensureObject(
|
|
149
|
+
port,
|
|
149
150
|
`${id}.todayWorkedMinutes`,
|
|
150
|
-
stateObject(
|
|
151
|
+
stateObject(import_stateNames.STATE_NAMES.todayWorkedMinutes, "number", "value", { unit: "min" })
|
|
151
152
|
);
|
|
152
|
-
await
|
|
153
|
+
await ensureObject(
|
|
154
|
+
port,
|
|
153
155
|
`${id}.todayBalanceMinutes`,
|
|
154
|
-
stateObject(
|
|
156
|
+
stateObject(import_stateNames.STATE_NAMES.todayBalanceMinutes, "number", "value", { unit: "min" })
|
|
155
157
|
);
|
|
156
|
-
await
|
|
158
|
+
await ensureObject(
|
|
159
|
+
port,
|
|
157
160
|
`${id}.monthWorkedMinutes`,
|
|
158
|
-
stateObject(
|
|
161
|
+
stateObject(import_stateNames.STATE_NAMES.monthWorkedMinutes, "number", "value", { unit: "min" })
|
|
159
162
|
);
|
|
160
|
-
await
|
|
163
|
+
await ensureObject(
|
|
164
|
+
port,
|
|
161
165
|
`${id}.monthBalanceMinutes`,
|
|
162
|
-
stateObject(
|
|
166
|
+
stateObject(import_stateNames.STATE_NAMES.monthBalanceMinutes, "number", "value", { unit: "min" })
|
|
163
167
|
);
|
|
164
|
-
await
|
|
168
|
+
await ensureObject(
|
|
169
|
+
port,
|
|
165
170
|
`${id}.yearBalanceMinutes`,
|
|
166
|
-
stateObject(
|
|
167
|
-
);
|
|
168
|
-
await port.setObjectNotExists(
|
|
169
|
-
`${id}.openConflicts`,
|
|
170
|
-
stateObject({ en: "Open conflicts", de: "Offene Konflikte" }, "number", "value")
|
|
171
|
+
stateObject(import_stateNames.STATE_NAMES.yearBalanceMinutes, "number", "value", { unit: "min" })
|
|
171
172
|
);
|
|
173
|
+
await ensureObject(port, `${id}.openConflicts`, stateObject(import_stateNames.STATE_NAMES.openConflicts, "number", "value"));
|
|
172
174
|
return id;
|
|
173
175
|
}
|
|
174
176
|
async function createCommandStates(port) {
|
|
175
|
-
await port
|
|
176
|
-
await
|
|
177
|
+
await ensureObject(port, "commands", channelObject(import_stateNames.STATE_NAMES.commandsChannel));
|
|
178
|
+
await ensureObject(
|
|
179
|
+
port,
|
|
177
180
|
COMMAND_IDS.punchUserId,
|
|
178
|
-
stateObject(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
"value",
|
|
182
|
-
{
|
|
183
|
-
write: true
|
|
184
|
-
}
|
|
185
|
-
)
|
|
181
|
+
stateObject(import_stateNames.STATE_NAMES.punchUserId, "number", "level", {
|
|
182
|
+
write: true
|
|
183
|
+
})
|
|
186
184
|
);
|
|
187
|
-
await
|
|
185
|
+
await ensureObject(
|
|
186
|
+
port,
|
|
188
187
|
COMMAND_IDS.punch,
|
|
189
|
-
stateObject(
|
|
188
|
+
stateObject(import_stateNames.STATE_NAMES.punch, "boolean", "button", {
|
|
190
189
|
read: false,
|
|
191
190
|
write: true
|
|
192
191
|
})
|
|
193
192
|
);
|
|
194
|
-
await
|
|
193
|
+
await ensureObject(
|
|
194
|
+
port,
|
|
195
195
|
COMMAND_IDS.quickPunch,
|
|
196
|
-
stateObject(
|
|
196
|
+
stateObject(import_stateNames.STATE_NAMES.quickPunch, "boolean", "button", {
|
|
197
197
|
read: false,
|
|
198
198
|
write: true
|
|
199
199
|
})
|
|
200
200
|
);
|
|
201
|
-
await
|
|
201
|
+
await ensureObject(
|
|
202
|
+
port,
|
|
202
203
|
COMMAND_IDS.closeMonth,
|
|
203
|
-
stateObject(
|
|
204
|
+
stateObject(import_stateNames.STATE_NAMES.closeMonth, "string", "text", {
|
|
204
205
|
write: true
|
|
205
206
|
})
|
|
206
207
|
);
|
|
207
|
-
await port.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{ en: "Recalculate period (YYYY-MM or YYYY)", de: "Zeitraum neu berechnen (JJJJ-MM oder JJJJ)" },
|
|
211
|
-
"string",
|
|
212
|
-
"text",
|
|
213
|
-
{ write: true }
|
|
214
|
-
)
|
|
215
|
-
);
|
|
216
|
-
await port.setObjectNotExists(
|
|
208
|
+
await ensureObject(port, COMMAND_IDS.recalc, stateObject(import_stateNames.STATE_NAMES.recalc, "string", "text", { write: true }));
|
|
209
|
+
await ensureObject(
|
|
210
|
+
port,
|
|
217
211
|
COMMAND_IDS.backup,
|
|
218
|
-
stateObject(
|
|
212
|
+
stateObject(import_stateNames.STATE_NAMES.backupCommand, "boolean", "button", {
|
|
219
213
|
read: false,
|
|
220
214
|
write: true
|
|
221
215
|
})
|
|
222
216
|
);
|
|
223
217
|
}
|
|
224
218
|
async function createInfoStates(port) {
|
|
225
|
-
await port
|
|
226
|
-
await port.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
await port.setObjectNotExists(
|
|
231
|
-
"info.schemaVersion",
|
|
232
|
-
stateObject({ en: "Database schema version", de: "Datenbank-Schemaversion" }, "string", "text")
|
|
233
|
-
);
|
|
234
|
-
await port.setObjectNotExists(
|
|
219
|
+
await ensureObject(port, "info", channelObject(import_stateNames.STATE_NAMES.infoChannel));
|
|
220
|
+
await ensureObject(port, "info.version", stateObject(import_stateNames.STATE_NAMES.version, "string", "text"));
|
|
221
|
+
await ensureObject(port, "info.schemaVersion", stateObject(import_stateNames.STATE_NAMES.schemaVersion, "string", "text"));
|
|
222
|
+
await ensureObject(
|
|
223
|
+
port,
|
|
235
224
|
"info.dbSizeBytes",
|
|
236
|
-
stateObject(
|
|
237
|
-
);
|
|
238
|
-
await port.setObjectNotExists(
|
|
239
|
-
"info.lastError",
|
|
240
|
-
stateObject({ en: "Last error", de: "Letzter Fehler" }, "string", "text")
|
|
241
|
-
);
|
|
242
|
-
await port.setObjectNotExists(
|
|
243
|
-
"info.lastBackup",
|
|
244
|
-
stateObject({ en: "Last backup", de: "Letzte Sicherung" }, "number", "value.time")
|
|
225
|
+
stateObject(import_stateNames.STATE_NAMES.dbSizeBytes, "number", "value", { unit: "bytes" })
|
|
245
226
|
);
|
|
227
|
+
await ensureObject(port, "info.lastError", stateObject(import_stateNames.STATE_NAMES.lastError, "string", "text"));
|
|
228
|
+
await ensureObject(port, "info.lastBackup", stateObject(import_stateNames.STATE_NAMES.lastBackup, "number", "value.time"));
|
|
246
229
|
}
|
|
247
230
|
async function createCompanyStates(port) {
|
|
248
|
-
await port
|
|
249
|
-
await port.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
);
|
|
253
|
-
await port.setObjectNotExists(
|
|
254
|
-
"company.present",
|
|
255
|
-
stateObject({ en: "Who is present", de: "Wer ist anwesend" }, "string", "text")
|
|
256
|
-
);
|
|
257
|
-
await port.setObjectNotExists(
|
|
258
|
-
"company.openConflicts",
|
|
259
|
-
stateObject({ en: "Open conflicts", de: "Offene Konflikte" }, "number", "value")
|
|
260
|
-
);
|
|
261
|
-
await port.setObjectNotExists(
|
|
262
|
-
"company.lastPunch",
|
|
263
|
-
stateObject({ en: "Last punch", de: "Letzte Buchung" }, "number", "value.time")
|
|
264
|
-
);
|
|
231
|
+
await ensureObject(port, "company", channelObject(import_stateNames.STATE_NAMES.companyChannel));
|
|
232
|
+
await ensureObject(port, "company.presentCount", stateObject(import_stateNames.STATE_NAMES.companyPresentCount, "number", "value"));
|
|
233
|
+
await ensureObject(port, "company.present", stateObject(import_stateNames.STATE_NAMES.companyPresent, "string", "text"));
|
|
234
|
+
await ensureObject(port, "company.openConflicts", stateObject(import_stateNames.STATE_NAMES.openConflicts, "number", "value"));
|
|
235
|
+
await ensureObject(port, "company.lastPunch", stateObject(import_stateNames.STATE_NAMES.companyLastPunch, "number", "value.time"));
|
|
265
236
|
}
|
|
266
237
|
function readCompanySnapshot(snapshots) {
|
|
267
238
|
const present = snapshots.filter((snapshot) => snapshot.hasOpenEntry);
|
|
@@ -283,27 +254,12 @@ async function publishCompanySnapshot(port, snapshot) {
|
|
|
283
254
|
await port.setState("company.lastPunch", (_a = snapshot.lastPunchUtc) != null ? _a : 0, true);
|
|
284
255
|
}
|
|
285
256
|
async function createEventStates(port) {
|
|
286
|
-
await port
|
|
287
|
-
await port.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
);
|
|
291
|
-
await port.
|
|
292
|
-
"events.lastType",
|
|
293
|
-
stateObject({ en: "Kind of the last event", de: "Art des letzten Ereignisses" }, "string", "text")
|
|
294
|
-
);
|
|
295
|
-
await port.setObjectNotExists(
|
|
296
|
-
"events.lastUser",
|
|
297
|
-
stateObject({ en: "Employee of the last event", de: "Mitarbeiter des letzten Ereignisses" }, "string", "text")
|
|
298
|
-
);
|
|
299
|
-
await port.setObjectNotExists(
|
|
300
|
-
"events.lastDirection",
|
|
301
|
-
stateObject({ en: "Direction of the last punch", de: "Richtung der letzten Buchung" }, "string", "text")
|
|
302
|
-
);
|
|
303
|
-
await port.setObjectNotExists(
|
|
304
|
-
"events.lastSource",
|
|
305
|
-
stateObject({ en: "Source of the last event", de: "Quelle des letzten Ereignisses" }, "string", "text")
|
|
306
|
-
);
|
|
257
|
+
await ensureObject(port, "events", channelObject(import_stateNames.STATE_NAMES.eventsChannel));
|
|
258
|
+
await ensureObject(port, "events.lastAt", stateObject(import_stateNames.STATE_NAMES.lastAt, "number", "value.time"));
|
|
259
|
+
await ensureObject(port, "events.lastType", stateObject(import_stateNames.STATE_NAMES.lastType, "string", "text"));
|
|
260
|
+
await ensureObject(port, "events.lastUser", stateObject(import_stateNames.STATE_NAMES.lastUser, "string", "text"));
|
|
261
|
+
await ensureObject(port, "events.lastDirection", stateObject(import_stateNames.STATE_NAMES.lastDirection, "string", "text"));
|
|
262
|
+
await ensureObject(port, "events.lastSource", stateObject(import_stateNames.STATE_NAMES.lastSource, "string", "text"));
|
|
307
263
|
}
|
|
308
264
|
async function publishEventSnapshot(port, snapshot) {
|
|
309
265
|
await port.setState("events.lastAt", snapshot.atUtc, true);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/adapter/states.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * States published by the adapter.\n *\n * The adapter exposes a small, stable state tree so that other adapters (and the user's dashboards) can show\n * and trigger time tracking without speaking to the REST API:\n *\n * ```\n * users.<userId>.displayName / present / hasOpenEntry / lastPunch / todayWorkedMinutes / todayBalanceMinutes\n * users.<userId>.openConflicts\n * commands.punchUserId / punch / quickPunch / closeMonth / recalc\n * ```\n *\n * All objects are created with `setObjectNotExists`, so existing instances keep their settings. The logic works\n * on a tiny port interface (`StatePort`) \u2014 the adapter instance in production, a recorder in the tests.\n */\n\nimport type { UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport type { SyncService } from \"../services/sync\";\nimport { localDate as resolveLocalDate } from \"../util/time\";\n\n/** Subset of the adapter API the state layer needs. */\nexport interface StatePort {\n\t/**\n\t * Creates an object when it does not exist yet.\n\t *\n\t * @param id - full state id (including the instance prefix)\n\t * @param object - object definition\n\t */\n\tsetObjectNotExists(id: string, object: ioBroker.SettableObject): Promise<unknown> | void;\n\t/**\n\t * Writes a state value.\n\t *\n\t * @param id - full state id (including the instance prefix)\n\t * @param value - value to write\n\t * @param ack - true for confirmed values\n\t */\n\tsetState(id: string, value: ioBroker.StateValue, ack?: boolean): Promise<unknown> | void;\n}\n\n/** Figures of one employee shown in the state tree. */\nexport interface UserSnapshot {\n\t/** Database id of the employee */\n\tuserId: number;\n\t/** Shown name */\n\tdisplayName: string;\n\t/** True when the last punch of today has no counterpart */\n\thasOpenEntry: boolean;\n\t/** Instant of the last punch of the day, `null` when there is none */\n\tlastPunchUtc: number | null;\n\t/** Net working time of today in minutes */\n\tworkedMinutes: number;\n\t/** Balance of today in minutes */\n\tbalanceMinutes: number;\n\t/** Punches waiting for a decision */\n\topenConflicts: number;\n\t/** Net working time of the current month in minutes */\n\tmonthWorkedMinutes: number;\n\t/** Balance of the current month in minutes */\n\tmonthBalanceMinutes: number;\n\t/** Balance of the current year in minutes */\n\tyearBalanceMinutes: number;\n}\n\n/** Figures of the whole company, published next to the employees. */\nexport interface CompanySnapshot {\n\t/** Employees that are clocked in right now */\n\tpresentCount: number;\n\t/** Names of those employees, separated by a comma (empty when nobody is present) */\n\tpresent: string;\n\t/** Punches of all employees waiting for a decision */\n\topenConflicts: number;\n\t/** Instant of the newest punch of today, `null` when nobody punched */\n\tlastPunchUtc: number | null;\n}\n\n/** Newest event of the instance, mirrored into the state tree. */\nexport interface EventSnapshot {\n\t/** Kind of the event, e.g. `punch` */\n\ttype: string;\n\t/** Instant the event belongs to */\n\tatUtc: number;\n\t/** Name of the employee the event belongs to, empty for instance wide events */\n\tuserName: string;\n\t/** Direction of a punch (`in`/`out`), empty for other events */\n\tdirection: string;\n\t/** Where the change came from, e.g. `web` or `terminal`, empty when unknown */\n\tsource: string;\n}\n\n/** Names of the writable command states. */\nexport const COMMAND_IDS = {\n\t/** Employee the commands apply to */\n\tpunchUserId: \"commands.punchUserId\",\n\t/** Punch in or out for `punchUserId` */\n\tpunch: \"commands.punch\",\n\t/** Punch in or out with the configured quick rounding */\n\tquickPunch: \"commands.quickPunch\",\n\t/** Close a month, value `YYYY-MM` */\n\tcloseMonth: \"commands.closeMonth\",\n\t/** Recalculate a period, value `YYYY-MM` or `YYYY` */\n\trecalc: \"commands.recalc\",\n\t/** Write a backup of the database */\n\tbackup: \"commands.backup\",\n} as const;\n\n/**\n * Builds a state object definition.\n *\n * @param name - name in English and German\n * @param name.en - English name\n * @param name.de - German name\n * @param type - state type\n * @param role - state role\n * @param options - additional flags\n * @param options.read - value can be read (default true)\n * @param options.write - value can be written (default false)\n * @param options.unit - unit of the value\n * @returns object definition\n */\nfunction stateObject(\n\tname: { en: string; de: string },\n\ttype: ioBroker.CommonType,\n\trole: string,\n\toptions: { read?: boolean; write?: boolean; unit?: string } = {},\n): ioBroker.SettableObject {\n\treturn {\n\t\ttype: \"state\",\n\t\tcommon: {\n\t\t\tname,\n\t\t\ttype,\n\t\t\trole,\n\t\t\tread: options.read !== false,\n\t\t\twrite: options.write === true,\n\t\t\t...(options.unit ? { unit: options.unit } : {}),\n\t\t},\n\t\tnative: {},\n\t};\n}\n\n/**\n * Builds a channel object definition.\n *\n * @param name - name in English and German\n * @param name.en - English name\n * @param name.de - German name\n * @returns object definition\n */\nfunction channelObject(name: { en: string; de: string }): ioBroker.SettableObject {\n\treturn { type: \"channel\", common: { name }, native: {} };\n}\n\n/**\n * Reads the figures of an employee for the state tree.\n *\n * @param args - data sources and employee\n * @param args.aggregation - aggregation service\n * @param args.users - user storage\n * @param args.sync - synchronisation service (open conflicts)\n * @param args.userId - database id of the employee\n * @param args.now - instant of the snapshot\n * @returns snapshot of the figures\n */\nexport function readUserSnapshot(args: {\n\taggregation: AggregationService;\n\tusers: UsersRepository;\n\tsync: SyncService;\n\tuserId: number;\n\tnow?: number;\n}): UserSnapshot | null {\n\tconst user = args.users.findById(args.userId);\n\tif (!user) {\n\t\treturn null;\n\t}\n\n\tconst timestamp = args.now ?? Math.floor(Date.now() / 1000);\n\tconst today = resolveLocalDate(timestamp, user.timezone);\n\tconst day = args.aggregation.recalculateDay(user.id, today, { now: timestamp });\n\t// the month is recalculated like the month view does it; the year is read, so the publishing stays cheap\n\tconst year = Number(today.slice(0, 4));\n\tconst month = Number(today.slice(5, 7));\n\tconst monthAggregate = args.aggregation.recalculateMonth(user.id, year, month, { now: timestamp });\n\tconst yearAggregate = args.aggregation.year(user.id, year);\n\n\treturn {\n\t\tuserId: user.id,\n\t\tdisplayName: user.displayName,\n\t\thasOpenEntry: day.hasOpenEntry,\n\t\tlastPunchUtc: Math.max(day.firstInUtc ?? 0, day.lastOutUtc ?? 0) || null,\n\t\tworkedMinutes: day.workedMin,\n\t\tbalanceMinutes: day.balanceMin,\n\t\topenConflicts: args.sync.conflicts(user.id).length,\n\t\tmonthWorkedMinutes: monthAggregate.workedMin,\n\t\tmonthBalanceMinutes: monthAggregate.balanceMin,\n\t\tyearBalanceMinutes: yearAggregate ? yearAggregate.workedMin - yearAggregate.targetMin : 0,\n\t};\n}\n\n/**\n * Publishes the figures of one employee.\n *\n * @param port - state port\n * @param snapshot - figures to publish\n */\nexport async function publishUserSnapshot(port: StatePort, snapshot: UserSnapshot): Promise<void> {\n\tconst id = `users.${snapshot.userId}`;\n\tawait port.setState(`${id}.displayName`, snapshot.displayName, true);\n\tawait port.setState(`${id}.present`, snapshot.hasOpenEntry, true);\n\tawait port.setState(`${id}.hasOpenEntry`, snapshot.hasOpenEntry, true);\n\tawait port.setState(`${id}.lastPunch`, snapshot.lastPunchUtc ?? 0, true);\n\tawait port.setState(`${id}.todayWorkedMinutes`, snapshot.workedMinutes, true);\n\tawait port.setState(`${id}.todayBalanceMinutes`, snapshot.balanceMinutes, true);\n\tawait port.setState(`${id}.monthWorkedMinutes`, snapshot.monthWorkedMinutes, true);\n\tawait port.setState(`${id}.monthBalanceMinutes`, snapshot.monthBalanceMinutes, true);\n\tawait port.setState(`${id}.yearBalanceMinutes`, snapshot.yearBalanceMinutes, true);\n\tawait port.setState(`${id}.openConflicts`, snapshot.openConflicts, true);\n}\n\n/**\n * Creates the channels of all employees and publishes their current figures.\n *\n * @param args - state port and data sources\n * @param args.port - state port\n * @param args.aggregation - aggregation service\n * @param args.users - user storage\n * @param args.sync - synchronisation service\n * @param args.now - instant of the snapshot\n * @returns the published snapshots\n */\nexport async function publishAllUserStates(args: {\n\tport: StatePort;\n\taggregation: AggregationService;\n\tusers: UsersRepository;\n\tsync: SyncService;\n\tnow?: number;\n}): Promise<UserSnapshot[]> {\n\tconst snapshots: UserSnapshot[] = [];\n\tfor (const user of args.users.list({ includeInactive: true })) {\n\t\tawait createUserChannel(args.port, user.id);\n\t\tconst snapshot = readUserSnapshot({\n\t\t\taggregation: args.aggregation,\n\t\t\tusers: args.users,\n\t\t\tsync: args.sync,\n\t\t\tuserId: user.id,\n\t\t\tnow: args.now,\n\t\t});\n\t\tif (!snapshot) {\n\t\t\tcontinue;\n\t\t}\n\t\tawait publishUserSnapshot(args.port, snapshot);\n\t\tsnapshots.push(snapshot);\n\t}\n\treturn snapshots;\n}\n\n/**\n * Creates the channel of one employee, including the shared parent channel `users`.\n *\n * The parent channel matters: the ioBroker object structure check (`E3009`) requires every state to have its\n * parents, so `users.<userId>.*` below a missing `users` produced 126 findings in the repository check\n * (PR #6697). `setObjectNotExists` keeps the call idempotent.\n *\n * @param port - state port\n * @param userId - database id of the employee\n * @returns the channel id\n */\nexport async function createUserChannel(port: StatePort, userId: number): Promise<string> {\n\tconst id = `users.${userId}`;\n\tawait port.setObjectNotExists(\"users\", channelObject({ en: \"Employees\", de: \"Mitarbeiter\" }));\n\tawait port.setObjectNotExists(id, channelObject({ en: \"Employee\", de: \"Mitarbeiter\" }));\n\tawait port.setObjectNotExists(`${id}.displayName`, stateObject({ en: \"Name\", de: \"Name\" }, \"string\", \"info.name\"));\n\t// the writable twin of `hasOpenEntry`: a script, a fingerprint reader or a dashboard writes it to say that\n\t// somebody arrived (`true`) or left (`false`) \u2014 see `presence.ts` for what the adapter does with it\n\tawait port.setObjectNotExists(\n\t\t`${id}.present`,\n\t\tstateObject(\n\t\t\t{ en: \"Present (working time is running)\", de: \"Anwesenheit (Arbeitszeit l\u00E4uft)\" },\n\t\t\t\"boolean\",\n\t\t\t\"switch\",\n\t\t\t{ write: true },\n\t\t),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.hasOpenEntry`,\n\t\tstateObject({ en: \"Punched in\", de: \"Eingestempelt\" }, \"boolean\", \"indicator.working\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.lastPunch`,\n\t\tstateObject({ en: \"Last punch\", de: \"Letzte Buchung\" }, \"number\", \"value.time\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.todayWorkedMinutes`,\n\t\tstateObject({ en: \"Worked today\", de: \"Heute gearbeitet\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.todayBalanceMinutes`,\n\t\tstateObject({ en: \"Balance today\", de: \"Saldo heute\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.monthWorkedMinutes`,\n\t\tstateObject({ en: \"Worked this month\", de: \"Diesen Monat gearbeitet\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.monthBalanceMinutes`,\n\t\tstateObject({ en: \"Balance this month\", de: \"Saldo diesen Monat\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.yearBalanceMinutes`,\n\t\tstateObject({ en: \"Balance this year\", de: \"Saldo dieses Jahr\" }, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t`${id}.openConflicts`,\n\t\tstateObject({ en: \"Open conflicts\", de: \"Offene Konflikte\" }, \"number\", \"value\"),\n\t);\n\treturn id;\n}\n\n/**\n * Creates the writable command states.\n *\n * @param port - state port\n */\nexport async function createCommandStates(port: StatePort): Promise<void> {\n\tawait port.setObjectNotExists(\"commands\", channelObject({ en: \"Commands\", de: \"Befehle\" }));\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.punchUserId,\n\t\tstateObject(\n\t\t\t{ en: \"Employee id for punch commands\", de: \"Mitarbeiter-Id f\u00FCr Stempelbefehle\" },\n\t\t\t\"number\",\n\t\t\t\"value\",\n\t\t\t{\n\t\t\t\twrite: true,\n\t\t\t},\n\t\t),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.punch,\n\t\tstateObject({ en: \"Punch in or out\", de: \"Ein- oder ausstempeln\" }, \"boolean\", \"button\", {\n\t\t\tread: false,\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.quickPunch,\n\t\tstateObject({ en: \"Punch with quick rounding\", de: \"Stempeln mit Schnellrundung\" }, \"boolean\", \"button\", {\n\t\t\tread: false,\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.closeMonth,\n\t\tstateObject({ en: \"Close month (YYYY-MM)\", de: \"Monat abschlie\u00DFen (JJJJ-MM)\" }, \"string\", \"text\", {\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.recalc,\n\t\tstateObject(\n\t\t\t{ en: \"Recalculate period (YYYY-MM or YYYY)\", de: \"Zeitraum neu berechnen (JJJJ-MM oder JJJJ)\" },\n\t\t\t\"string\",\n\t\t\t\"text\",\n\t\t\t{ write: true },\n\t\t),\n\t);\n\tawait port.setObjectNotExists(\n\t\tCOMMAND_IDS.backup,\n\t\tstateObject({ en: \"Write a database backup\", de: \"Datenbank-Sicherung schreiben\" }, \"boolean\", \"button\", {\n\t\t\tread: false,\n\t\t\twrite: true,\n\t\t}),\n\t);\n}\n\n/**\n * Creates the informational states of the instance.\n *\n * @param port - state port\n */\nexport async function createInfoStates(port: StatePort): Promise<void> {\n\tawait port.setObjectNotExists(\"info\", channelObject({ en: \"Information\", de: \"Information\" }));\n\tawait port.setObjectNotExists(\n\t\t\"info.version\",\n\t\tstateObject({ en: \"Adapter version\", de: \"Adapter-Version\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"info.schemaVersion\",\n\t\tstateObject({ en: \"Database schema version\", de: \"Datenbank-Schemaversion\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"info.dbSizeBytes\",\n\t\tstateObject({ en: \"Database size\", de: \"Datenbank-Gr\u00F6\u00DFe\" }, \"number\", \"value\", { unit: \"bytes\" }),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"info.lastError\",\n\t\tstateObject({ en: \"Last error\", de: \"Letzter Fehler\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"info.lastBackup\",\n\t\tstateObject({ en: \"Last backup\", de: \"Letzte Sicherung\" }, \"number\", \"value.time\"),\n\t);\n}\n\n/**\n * Creates the states of the company.\n *\n * @param port - state port\n */\nexport async function createCompanyStates(port: StatePort): Promise<void> {\n\tawait port.setObjectNotExists(\"company\", channelObject({ en: \"Company\", de: \"Firma\" }));\n\tawait port.setObjectNotExists(\n\t\t\"company.presentCount\",\n\t\tstateObject({ en: \"Present employees\", de: \"Anwesende Mitarbeiter\" }, \"number\", \"value\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"company.present\",\n\t\tstateObject({ en: \"Who is present\", de: \"Wer ist anwesend\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"company.openConflicts\",\n\t\tstateObject({ en: \"Open conflicts\", de: \"Offene Konflikte\" }, \"number\", \"value\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"company.lastPunch\",\n\t\tstateObject({ en: \"Last punch\", de: \"Letzte Buchung\" }, \"number\", \"value.time\"),\n\t);\n}\n\n/**\n * Reads the company figures out of the figures of the employees.\n *\n * @param snapshots - figures of all employees\n * @returns the company figures\n */\nexport function readCompanySnapshot(snapshots: UserSnapshot[]): CompanySnapshot {\n\tconst present = snapshots.filter(snapshot => snapshot.hasOpenEntry);\n\treturn {\n\t\tpresentCount: present.length,\n\t\tpresent: present.map(snapshot => snapshot.displayName).join(\", \"),\n\t\topenConflicts: snapshots.reduce((sum, snapshot) => sum + snapshot.openConflicts, 0),\n\t\tlastPunchUtc: snapshots.reduce<number | null>(\n\t\t\t(newest, snapshot) =>\n\t\t\t\tsnapshot.lastPunchUtc !== null && (newest === null || snapshot.lastPunchUtc > newest)\n\t\t\t\t\t? snapshot.lastPunchUtc\n\t\t\t\t\t: newest,\n\t\t\tnull,\n\t\t),\n\t};\n}\n\n/**\n * Publishes the company figures.\n *\n * @param port - state port\n * @param snapshot - figures to publish\n */\nexport async function publishCompanySnapshot(port: StatePort, snapshot: CompanySnapshot): Promise<void> {\n\tawait port.setState(\"company.presentCount\", snapshot.presentCount, true);\n\tawait port.setState(\"company.present\", snapshot.present, true);\n\tawait port.setState(\"company.openConflicts\", snapshot.openConflicts, true);\n\tawait port.setState(\"company.lastPunch\", snapshot.lastPunchUtc ?? 0, true);\n}\n\n/**\n * Creates the event states.\n *\n * @param port - state port\n */\nexport async function createEventStates(port: StatePort): Promise<void> {\n\tawait port.setObjectNotExists(\"events\", channelObject({ en: \"Events\", de: \"Ereignisse\" }));\n\tawait port.setObjectNotExists(\n\t\t\"events.lastAt\",\n\t\tstateObject({ en: \"Last event at\", de: \"Letztes Ereignis um\" }, \"number\", \"value.time\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"events.lastType\",\n\t\tstateObject({ en: \"Kind of the last event\", de: \"Art des letzten Ereignisses\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"events.lastUser\",\n\t\tstateObject({ en: \"Employee of the last event\", de: \"Mitarbeiter des letzten Ereignisses\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"events.lastDirection\",\n\t\tstateObject({ en: \"Direction of the last punch\", de: \"Richtung der letzten Buchung\" }, \"string\", \"text\"),\n\t);\n\tawait port.setObjectNotExists(\n\t\t\"events.lastSource\",\n\t\tstateObject({ en: \"Source of the last event\", de: \"Quelle des letzten Ereignisses\" }, \"string\", \"text\"),\n\t);\n}\n\n/**\n * Publishes the newest event.\n *\n * @param port - state port\n * @param snapshot - event to publish\n */\nexport async function publishEventSnapshot(port: StatePort, snapshot: EventSnapshot): Promise<void> {\n\tawait port.setState(\"events.lastAt\", snapshot.atUtc, true);\n\tawait port.setState(\"events.lastType\", snapshot.type, true);\n\tawait port.setState(\"events.lastUser\", snapshot.userName, true);\n\tawait port.setState(\"events.lastDirection\", snapshot.direction, true);\n\tawait port.setState(\"events.lastSource\", snapshot.source, true);\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBA,kBAA8C;
|
|
4
|
+
"sourcesContent": ["/**\n * States published by the adapter.\n *\n * The adapter exposes a small, stable state tree so that other adapters (and the user's dashboards) can show\n * and trigger time tracking without speaking to the REST API:\n *\n * ```\n * users.<userId>.displayName / present / hasOpenEntry / lastPunch / todayWorkedMinutes / todayBalanceMinutes\n * users.<userId>.openConflicts\n * commands.punchUserId / punch / quickPunch / closeMonth / recalc\n * ```\n *\n * All objects are created with `setObjectNotExists`, so existing instances keep their settings. The logic works\n * on a tiny port interface (`StatePort`) \u2014 the adapter instance in production, a recorder in the tests.\n */\n\nimport type { UsersRepository } from \"../db/repositories/users\";\nimport type { AggregationService } from \"../services/aggregation\";\nimport type { SyncService } from \"../services/sync\";\nimport { STATE_NAMES, type StateName } from \"./stateNames\";\nimport { localDate as resolveLocalDate } from \"../util/time\";\n\n/** Subset of the adapter API the state layer needs. */\nexport interface StatePort {\n\t/**\n\t * Creates an object when it does not exist yet.\n\t *\n\t * @param id - full state id (including the instance prefix)\n\t * @param object - object definition\n\t */\n\tsetObjectNotExists(id: string, object: ioBroker.SettableObject): Promise<unknown> | void;\n\t/**\n\t * Merges fields into an object that already exists.\n\t *\n\t * @param id - full state id (including the instance prefix)\n\t * @param object - fields to merge\n\t */\n\textendObject(id: string, object: ioBroker.PartialObject): Promise<unknown> | void;\n\t/**\n\t * Writes a state value.\n\t *\n\t * @param id - full state id (including the instance prefix)\n\t * @param value - value to write\n\t * @param ack - true for confirmed values\n\t */\n\tsetState(id: string, value: ioBroker.StateValue, ack?: boolean): Promise<unknown> | void;\n}\n\n/** Figures of one employee shown in the state tree. */\nexport interface UserSnapshot {\n\t/** Database id of the employee */\n\tuserId: number;\n\t/** Shown name */\n\tdisplayName: string;\n\t/** True when the last punch of today has no counterpart */\n\thasOpenEntry: boolean;\n\t/** Instant of the last punch of the day, `null` when there is none */\n\tlastPunchUtc: number | null;\n\t/** Net working time of today in minutes */\n\tworkedMinutes: number;\n\t/** Balance of today in minutes */\n\tbalanceMinutes: number;\n\t/** Punches waiting for a decision */\n\topenConflicts: number;\n\t/** Net working time of the current month in minutes */\n\tmonthWorkedMinutes: number;\n\t/** Balance of the current month in minutes */\n\tmonthBalanceMinutes: number;\n\t/** Balance of the current year in minutes */\n\tyearBalanceMinutes: number;\n}\n\n/** Figures of the whole company, published next to the employees. */\nexport interface CompanySnapshot {\n\t/** Employees that are clocked in right now */\n\tpresentCount: number;\n\t/** Names of those employees, separated by a comma (empty when nobody is present) */\n\tpresent: string;\n\t/** Punches of all employees waiting for a decision */\n\topenConflicts: number;\n\t/** Instant of the newest punch of today, `null` when nobody punched */\n\tlastPunchUtc: number | null;\n}\n\n/** Newest event of the instance, mirrored into the state tree. */\nexport interface EventSnapshot {\n\t/** Kind of the event, e.g. `punch` */\n\ttype: string;\n\t/** Instant the event belongs to */\n\tatUtc: number;\n\t/** Name of the employee the event belongs to, empty for instance wide events */\n\tuserName: string;\n\t/** Direction of a punch (`in`/`out`), empty for other events */\n\tdirection: string;\n\t/** Where the change came from, e.g. `web` or `terminal`, empty when unknown */\n\tsource: string;\n}\n\n/** Names of the writable command states. */\nexport const COMMAND_IDS = {\n\t/** Employee the commands apply to */\n\tpunchUserId: \"commands.punchUserId\",\n\t/** Punch in or out for `punchUserId` */\n\tpunch: \"commands.punch\",\n\t/** Punch in or out with the configured quick rounding */\n\tquickPunch: \"commands.quickPunch\",\n\t/** Close a month, value `YYYY-MM` */\n\tcloseMonth: \"commands.closeMonth\",\n\t/** Recalculate a period, value `YYYY-MM` or `YYYY` */\n\trecalc: \"commands.recalc\",\n\t/** Write a backup of the database */\n\tbackup: \"commands.backup\",\n} as const;\n\n/**\n * Builds a state object definition.\n *\n * @param name - name in English and German\n * @param name.en - English name\n * @param name.de - German name\n * @param type - state type\n * @param role - state role\n * @param options - additional flags\n * @param options.read - value can be read (default true)\n * @param options.write - value can be written (default false)\n * @param options.unit - unit of the value\n * @returns object definition\n */\nfunction stateObject(\n\tname: StateName,\n\ttype: ioBroker.CommonType,\n\trole: string,\n\toptions: { read?: boolean; write?: boolean; unit?: string } = {},\n): ioBroker.SettableObject {\n\treturn {\n\t\ttype: \"state\",\n\t\tcommon: {\n\t\t\tname,\n\t\t\ttype,\n\t\t\trole,\n\t\t\tread: options.read !== false,\n\t\t\twrite: options.write === true,\n\t\t\t...(options.unit ? { unit: options.unit } : {}),\n\t\t},\n\t\tnative: {},\n\t};\n}\n\n/**\n * Builds a channel object definition.\n *\n * @param name - name in English and German\n * @param name.en - English name\n * @param name.de - German name\n * @returns object definition\n */\nfunction channelObject(name: StateName): ioBroker.SettableObject {\n\treturn { type: \"channel\", common: { name }, native: {} };\n}\n\n/**\n * Creates an object when it is missing and keeps its name current.\n *\n * `setObjectNotExists` alone would leave the names of existing installations untouched \u2014 the ioBroker object\n * structure check (`E6001`) reads them from the running instance, so the name is refreshed on every start. The\n * merge only touches `common.name`, existing links (for example in `vis`) stay as they are.\n *\n * @param port - state port\n * @param id - full state id (including the instance prefix)\n * @param object - object definition\n */\nasync function ensureObject(port: StatePort, id: string, object: ioBroker.SettableObject): Promise<void> {\n\tawait port.setObjectNotExists(id, object);\n\tawait port.extendObject(id, { common: { name: object.common?.name } });\n}\n\n/**\n * Reads the figures of an employee for the state tree.\n *\n * @param args - data sources and employee\n * @param args.aggregation - aggregation service\n * @param args.users - user storage\n * @param args.sync - synchronisation service (open conflicts)\n * @param args.userId - database id of the employee\n * @param args.now - instant of the snapshot\n * @returns snapshot of the figures\n */\nexport function readUserSnapshot(args: {\n\taggregation: AggregationService;\n\tusers: UsersRepository;\n\tsync: SyncService;\n\tuserId: number;\n\tnow?: number;\n}): UserSnapshot | null {\n\tconst user = args.users.findById(args.userId);\n\tif (!user) {\n\t\treturn null;\n\t}\n\n\tconst timestamp = args.now ?? Math.floor(Date.now() / 1000);\n\tconst today = resolveLocalDate(timestamp, user.timezone);\n\tconst day = args.aggregation.recalculateDay(user.id, today, { now: timestamp });\n\t// the month is recalculated like the month view does it; the year is read, so the publishing stays cheap\n\tconst year = Number(today.slice(0, 4));\n\tconst month = Number(today.slice(5, 7));\n\tconst monthAggregate = args.aggregation.recalculateMonth(user.id, year, month, { now: timestamp });\n\tconst yearAggregate = args.aggregation.year(user.id, year);\n\n\treturn {\n\t\tuserId: user.id,\n\t\tdisplayName: user.displayName,\n\t\thasOpenEntry: day.hasOpenEntry,\n\t\tlastPunchUtc: Math.max(day.firstInUtc ?? 0, day.lastOutUtc ?? 0) || null,\n\t\tworkedMinutes: day.workedMin,\n\t\tbalanceMinutes: day.balanceMin,\n\t\topenConflicts: args.sync.conflicts(user.id).length,\n\t\tmonthWorkedMinutes: monthAggregate.workedMin,\n\t\tmonthBalanceMinutes: monthAggregate.balanceMin,\n\t\tyearBalanceMinutes: yearAggregate ? yearAggregate.workedMin - yearAggregate.targetMin : 0,\n\t};\n}\n\n/**\n * Publishes the figures of one employee.\n *\n * @param port - state port\n * @param snapshot - figures to publish\n */\nexport async function publishUserSnapshot(port: StatePort, snapshot: UserSnapshot): Promise<void> {\n\tconst id = `users.${snapshot.userId}`;\n\tawait port.setState(`${id}.displayName`, snapshot.displayName, true);\n\tawait port.setState(`${id}.present`, snapshot.hasOpenEntry, true);\n\tawait port.setState(`${id}.hasOpenEntry`, snapshot.hasOpenEntry, true);\n\tawait port.setState(`${id}.lastPunch`, snapshot.lastPunchUtc ?? 0, true);\n\tawait port.setState(`${id}.todayWorkedMinutes`, snapshot.workedMinutes, true);\n\tawait port.setState(`${id}.todayBalanceMinutes`, snapshot.balanceMinutes, true);\n\tawait port.setState(`${id}.monthWorkedMinutes`, snapshot.monthWorkedMinutes, true);\n\tawait port.setState(`${id}.monthBalanceMinutes`, snapshot.monthBalanceMinutes, true);\n\tawait port.setState(`${id}.yearBalanceMinutes`, snapshot.yearBalanceMinutes, true);\n\tawait port.setState(`${id}.openConflicts`, snapshot.openConflicts, true);\n}\n\n/**\n * Creates the channels of all employees and publishes their current figures.\n *\n * @param args - state port and data sources\n * @param args.port - state port\n * @param args.aggregation - aggregation service\n * @param args.users - user storage\n * @param args.sync - synchronisation service\n * @param args.now - instant of the snapshot\n * @returns the published snapshots\n */\nexport async function publishAllUserStates(args: {\n\tport: StatePort;\n\taggregation: AggregationService;\n\tusers: UsersRepository;\n\tsync: SyncService;\n\tnow?: number;\n}): Promise<UserSnapshot[]> {\n\tconst snapshots: UserSnapshot[] = [];\n\tfor (const user of args.users.list({ includeInactive: true })) {\n\t\tawait createUserChannel(args.port, user.id);\n\t\tconst snapshot = readUserSnapshot({\n\t\t\taggregation: args.aggregation,\n\t\t\tusers: args.users,\n\t\t\tsync: args.sync,\n\t\t\tuserId: user.id,\n\t\t\tnow: args.now,\n\t\t});\n\t\tif (!snapshot) {\n\t\t\tcontinue;\n\t\t}\n\t\tawait publishUserSnapshot(args.port, snapshot);\n\t\tsnapshots.push(snapshot);\n\t}\n\treturn snapshots;\n}\n\n/**\n * Creates the channel of one employee, including the shared parent channel `users`.\n *\n * The parent channel matters: the ioBroker object structure check (`E3009`) requires every state to have its\n * parents, so `users.<userId>.*` below a missing `users` produced 126 findings in the repository check\n * (PR #6697). `setObjectNotExists` keeps the call idempotent.\n *\n * @param port - state port\n * @param userId - database id of the employee\n * @returns the channel id\n */\nexport async function createUserChannel(port: StatePort, userId: number): Promise<string> {\n\tconst id = `users.${userId}`;\n\tawait ensureObject(port, \"users\", channelObject(STATE_NAMES.usersChannel));\n\tawait ensureObject(port, id, channelObject(STATE_NAMES.employeeChannel));\n\tawait ensureObject(port, `${id}.displayName`, stateObject(STATE_NAMES.displayName, \"string\", \"info.name\"));\n\t// the writable twin of `hasOpenEntry`: a script, a fingerprint reader or a dashboard writes it to say that\n\t// somebody arrived (`true`) or left (`false`) \u2014 see `presence.ts` for what the adapter does with it\n\tawait ensureObject(\n\t\tport,\n\t\t`${id}.present`,\n\t\tstateObject(STATE_NAMES.userPresent, \"boolean\", \"switch\", { write: true }),\n\t);\n\tawait ensureObject(\n\t\tport,\n\t\t`${id}.hasOpenEntry`,\n\t\tstateObject(STATE_NAMES.hasOpenEntry, \"boolean\", \"indicator.working\"),\n\t);\n\tawait ensureObject(port, `${id}.lastPunch`, stateObject(STATE_NAMES.lastPunch, \"number\", \"value.time\"));\n\tawait ensureObject(\n\t\tport,\n\t\t`${id}.todayWorkedMinutes`,\n\t\tstateObject(STATE_NAMES.todayWorkedMinutes, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait ensureObject(\n\t\tport,\n\t\t`${id}.todayBalanceMinutes`,\n\t\tstateObject(STATE_NAMES.todayBalanceMinutes, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait ensureObject(\n\t\tport,\n\t\t`${id}.monthWorkedMinutes`,\n\t\tstateObject(STATE_NAMES.monthWorkedMinutes, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait ensureObject(\n\t\tport,\n\t\t`${id}.monthBalanceMinutes`,\n\t\tstateObject(STATE_NAMES.monthBalanceMinutes, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait ensureObject(\n\t\tport,\n\t\t`${id}.yearBalanceMinutes`,\n\t\tstateObject(STATE_NAMES.yearBalanceMinutes, \"number\", \"value\", { unit: \"min\" }),\n\t);\n\tawait ensureObject(port, `${id}.openConflicts`, stateObject(STATE_NAMES.openConflicts, \"number\", \"value\"));\n\treturn id;\n}\n\n/**\n * Creates the writable command states.\n *\n * @param port - state port\n */\nexport async function createCommandStates(port: StatePort): Promise<void> {\n\tawait ensureObject(port, \"commands\", channelObject(STATE_NAMES.commandsChannel));\n\tawait ensureObject(\n\t\tport,\n\t\tCOMMAND_IDS.punchUserId,\n\t\tstateObject(STATE_NAMES.punchUserId, \"number\", \"level\", {\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait ensureObject(\n\t\tport,\n\t\tCOMMAND_IDS.punch,\n\t\tstateObject(STATE_NAMES.punch, \"boolean\", \"button\", {\n\t\t\tread: false,\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait ensureObject(\n\t\tport,\n\t\tCOMMAND_IDS.quickPunch,\n\t\tstateObject(STATE_NAMES.quickPunch, \"boolean\", \"button\", {\n\t\t\tread: false,\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait ensureObject(\n\t\tport,\n\t\tCOMMAND_IDS.closeMonth,\n\t\tstateObject(STATE_NAMES.closeMonth, \"string\", \"text\", {\n\t\t\twrite: true,\n\t\t}),\n\t);\n\tawait ensureObject(port, COMMAND_IDS.recalc, stateObject(STATE_NAMES.recalc, \"string\", \"text\", { write: true }));\n\tawait ensureObject(\n\t\tport,\n\t\tCOMMAND_IDS.backup,\n\t\tstateObject(STATE_NAMES.backupCommand, \"boolean\", \"button\", {\n\t\t\tread: false,\n\t\t\twrite: true,\n\t\t}),\n\t);\n}\n\n/**\n * Creates the informational states of the instance.\n *\n * @param port - state port\n */\nexport async function createInfoStates(port: StatePort): Promise<void> {\n\tawait ensureObject(port, \"info\", channelObject(STATE_NAMES.infoChannel));\n\tawait ensureObject(port, \"info.version\", stateObject(STATE_NAMES.version, \"string\", \"text\"));\n\tawait ensureObject(port, \"info.schemaVersion\", stateObject(STATE_NAMES.schemaVersion, \"string\", \"text\"));\n\tawait ensureObject(\n\t\tport,\n\t\t\"info.dbSizeBytes\",\n\t\tstateObject(STATE_NAMES.dbSizeBytes, \"number\", \"value\", { unit: \"bytes\" }),\n\t);\n\tawait ensureObject(port, \"info.lastError\", stateObject(STATE_NAMES.lastError, \"string\", \"text\"));\n\tawait ensureObject(port, \"info.lastBackup\", stateObject(STATE_NAMES.lastBackup, \"number\", \"value.time\"));\n}\n\n/**\n * Creates the states of the company.\n *\n * @param port - state port\n */\nexport async function createCompanyStates(port: StatePort): Promise<void> {\n\tawait ensureObject(port, \"company\", channelObject(STATE_NAMES.companyChannel));\n\tawait ensureObject(port, \"company.presentCount\", stateObject(STATE_NAMES.companyPresentCount, \"number\", \"value\"));\n\tawait ensureObject(port, \"company.present\", stateObject(STATE_NAMES.companyPresent, \"string\", \"text\"));\n\tawait ensureObject(port, \"company.openConflicts\", stateObject(STATE_NAMES.openConflicts, \"number\", \"value\"));\n\tawait ensureObject(port, \"company.lastPunch\", stateObject(STATE_NAMES.companyLastPunch, \"number\", \"value.time\"));\n}\n\n/**\n * Reads the company figures out of the figures of the employees.\n *\n * @param snapshots - figures of all employees\n * @returns the company figures\n */\nexport function readCompanySnapshot(snapshots: UserSnapshot[]): CompanySnapshot {\n\tconst present = snapshots.filter(snapshot => snapshot.hasOpenEntry);\n\treturn {\n\t\tpresentCount: present.length,\n\t\tpresent: present.map(snapshot => snapshot.displayName).join(\", \"),\n\t\topenConflicts: snapshots.reduce((sum, snapshot) => sum + snapshot.openConflicts, 0),\n\t\tlastPunchUtc: snapshots.reduce<number | null>(\n\t\t\t(newest, snapshot) =>\n\t\t\t\tsnapshot.lastPunchUtc !== null && (newest === null || snapshot.lastPunchUtc > newest)\n\t\t\t\t\t? snapshot.lastPunchUtc\n\t\t\t\t\t: newest,\n\t\t\tnull,\n\t\t),\n\t};\n}\n\n/**\n * Publishes the company figures.\n *\n * @param port - state port\n * @param snapshot - figures to publish\n */\nexport async function publishCompanySnapshot(port: StatePort, snapshot: CompanySnapshot): Promise<void> {\n\tawait port.setState(\"company.presentCount\", snapshot.presentCount, true);\n\tawait port.setState(\"company.present\", snapshot.present, true);\n\tawait port.setState(\"company.openConflicts\", snapshot.openConflicts, true);\n\tawait port.setState(\"company.lastPunch\", snapshot.lastPunchUtc ?? 0, true);\n}\n\n/**\n * Creates the event states.\n *\n * @param port - state port\n */\nexport async function createEventStates(port: StatePort): Promise<void> {\n\tawait ensureObject(port, \"events\", channelObject(STATE_NAMES.eventsChannel));\n\tawait ensureObject(port, \"events.lastAt\", stateObject(STATE_NAMES.lastAt, \"number\", \"value.time\"));\n\tawait ensureObject(port, \"events.lastType\", stateObject(STATE_NAMES.lastType, \"string\", \"text\"));\n\tawait ensureObject(port, \"events.lastUser\", stateObject(STATE_NAMES.lastUser, \"string\", \"text\"));\n\tawait ensureObject(port, \"events.lastDirection\", stateObject(STATE_NAMES.lastDirection, \"string\", \"text\"));\n\tawait ensureObject(port, \"events.lastSource\", stateObject(STATE_NAMES.lastSource, \"string\", \"text\"));\n}\n\n/**\n * Publishes the newest event.\n *\n * @param port - state port\n * @param snapshot - event to publish\n */\nexport async function publishEventSnapshot(port: StatePort, snapshot: EventSnapshot): Promise<void> {\n\tawait port.setState(\"events.lastAt\", snapshot.atUtc, true);\n\tawait port.setState(\"events.lastType\", snapshot.type, true);\n\tawait port.setState(\"events.lastUser\", snapshot.userName, true);\n\tawait port.setState(\"events.lastDirection\", snapshot.direction, true);\n\tawait port.setState(\"events.lastSource\", snapshot.source, true);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["resolveLocalDate"]
|
|
7
7
|
}
|