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,432 @@
|
|
|
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 absences_exports = {};
|
|
20
|
+
__export(absences_exports, {
|
|
21
|
+
UnknownAbsenceTypeError: () => UnknownAbsenceTypeError,
|
|
22
|
+
createAbsencesRepository: () => createAbsencesRepository,
|
|
23
|
+
mapAbsenceRow: () => mapAbsenceRow,
|
|
24
|
+
mapAbsenceTypeRow: () => mapAbsenceTypeRow
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(absences_exports);
|
|
27
|
+
var import_time = require("../../util/time");
|
|
28
|
+
var import_errors = require("../../errors");
|
|
29
|
+
var import_audit = require("./audit");
|
|
30
|
+
class UnknownAbsenceTypeError extends Error {
|
|
31
|
+
/**
|
|
32
|
+
* Creates the error.
|
|
33
|
+
*
|
|
34
|
+
* @param reference - id or code that could not be resolved
|
|
35
|
+
*/
|
|
36
|
+
constructor(reference) {
|
|
37
|
+
super(`unknown absence type "${reference}"`);
|
|
38
|
+
this.reference = reference;
|
|
39
|
+
this.name = "UnknownAbsenceTypeError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const TYPE_COLUMNS = "id, user_id, code, name, paid, factor, reduce_vacation, is_active";
|
|
43
|
+
const ABSENCE_COLUMNS = `id, user_id, type_id, date_from, date_to, day_portion, hours, status, note,
|
|
44
|
+
created_at, created_by`;
|
|
45
|
+
const AUDITED_FIELDS = ["typeId", "dateFrom", "dateTo", "dayPortion", "hours", "note"];
|
|
46
|
+
function mapAbsenceTypeRow(row) {
|
|
47
|
+
return {
|
|
48
|
+
id: row.id,
|
|
49
|
+
userId: row.user_id,
|
|
50
|
+
code: row.code,
|
|
51
|
+
name: row.name,
|
|
52
|
+
paid: row.paid !== 0,
|
|
53
|
+
factor: row.factor,
|
|
54
|
+
reduceVacation: row.reduce_vacation !== 0,
|
|
55
|
+
isActive: row.is_active !== 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function mapAbsenceRow(row) {
|
|
59
|
+
return {
|
|
60
|
+
id: row.id,
|
|
61
|
+
userId: row.user_id,
|
|
62
|
+
typeId: row.type_id,
|
|
63
|
+
dateFrom: row.date_from,
|
|
64
|
+
dateTo: row.date_to,
|
|
65
|
+
dayPortion: row.day_portion,
|
|
66
|
+
hours: row.hours,
|
|
67
|
+
status: row.status,
|
|
68
|
+
note: row.note,
|
|
69
|
+
createdAt: row.created_at,
|
|
70
|
+
createdBy: row.created_by
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function requireDate(value, label) {
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!(0, import_time.isDateString)(trimmed)) {
|
|
76
|
+
throw new import_errors.ValidationError(`${label} "${value}" is not a date (YYYY-MM-DD)`);
|
|
77
|
+
}
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
|
80
|
+
function requireDayPortion(value) {
|
|
81
|
+
if (!Number.isFinite(value) || value <= 0 || value > 1) {
|
|
82
|
+
throw new import_errors.ValidationError(`dayPortion must be greater than 0 and at most 1 (got ${value})`);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function createAbsencesRepository(db) {
|
|
87
|
+
const selectTypeById = db.prepare(`SELECT ${TYPE_COLUMNS} FROM absence_types WHERE id = ?`);
|
|
88
|
+
const selectTypeUserCode = db.prepare(`SELECT ${TYPE_COLUMNS} FROM absence_types WHERE code = ? AND user_id = ?`);
|
|
89
|
+
const selectTypeGlobalCode = db.prepare(
|
|
90
|
+
`SELECT ${TYPE_COLUMNS} FROM absence_types WHERE code = ? AND user_id IS NULL`
|
|
91
|
+
);
|
|
92
|
+
const selectAbsenceById = db.prepare(`SELECT ${ABSENCE_COLUMNS} FROM absences WHERE id = ?`);
|
|
93
|
+
const selectByUser = db.prepare(
|
|
94
|
+
`SELECT ${ABSENCE_COLUMNS} FROM absences
|
|
95
|
+
WHERE user_id = ? AND date_to >= ? AND date_from <= ?
|
|
96
|
+
ORDER BY date_from, date_to, id`
|
|
97
|
+
);
|
|
98
|
+
const selectWithTypes = db.prepare(
|
|
99
|
+
`SELECT a.id, a.user_id, a.type_id, a.date_from, a.date_to, a.day_portion, a.hours, a.status, a.note,
|
|
100
|
+
a.created_at, a.created_by,
|
|
101
|
+
t.code AS type_code, t.name AS type_name, t.paid AS type_paid, t.factor AS type_factor,
|
|
102
|
+
t.reduce_vacation AS type_reduce_vacation
|
|
103
|
+
FROM absences a JOIN absence_types t ON t.id = a.type_id
|
|
104
|
+
WHERE a.user_id = ? AND a.date_to >= ? AND a.date_from <= ?
|
|
105
|
+
ORDER BY a.date_from, a.date_to, a.id`
|
|
106
|
+
);
|
|
107
|
+
const insertAbsence = db.prepare(
|
|
108
|
+
`INSERT INTO absences
|
|
109
|
+
(user_id, type_id, date_from, date_to, day_portion, hours, status, note, created_at, created_by)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
111
|
+
);
|
|
112
|
+
const updateAbsence = db.prepare(
|
|
113
|
+
`UPDATE absences SET type_id = ?, date_from = ?, date_to = ?, day_portion = ?, hours = ?, note = ?
|
|
114
|
+
WHERE id = ?`
|
|
115
|
+
);
|
|
116
|
+
const updateStatusStatement = db.prepare("UPDATE absences SET status = ? WHERE id = ?");
|
|
117
|
+
const deleteAbsence = db.prepare("DELETE FROM absences WHERE id = ?");
|
|
118
|
+
const insertType = db.prepare(
|
|
119
|
+
`INSERT INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active)
|
|
120
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
121
|
+
);
|
|
122
|
+
const updateType = db.prepare(
|
|
123
|
+
`UPDATE absence_types SET name = ?, paid = ?, factor = ?, reduce_vacation = ?, is_active = ? WHERE id = ?`
|
|
124
|
+
);
|
|
125
|
+
const read = (id) => {
|
|
126
|
+
const row = selectAbsenceById.get(id);
|
|
127
|
+
return row ? mapAbsenceRow(row) : null;
|
|
128
|
+
};
|
|
129
|
+
const resolveType = (reference, userId) => {
|
|
130
|
+
if (typeof reference === "number") {
|
|
131
|
+
const row = selectTypeById.get(reference);
|
|
132
|
+
return row ? mapAbsenceTypeRow(row) : null;
|
|
133
|
+
}
|
|
134
|
+
if (userId != null) {
|
|
135
|
+
const own = selectTypeUserCode.get(reference, userId);
|
|
136
|
+
if (own) {
|
|
137
|
+
return mapAbsenceTypeRow(own);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const global = selectTypeGlobalCode.get(reference);
|
|
141
|
+
return global ? mapAbsenceTypeRow(global) : null;
|
|
142
|
+
};
|
|
143
|
+
const requireType = (input) => {
|
|
144
|
+
var _a;
|
|
145
|
+
const reference = (_a = input.typeId) != null ? _a : input.typeCode;
|
|
146
|
+
if (reference === void 0) {
|
|
147
|
+
throw new UnknownAbsenceTypeError("(none)");
|
|
148
|
+
}
|
|
149
|
+
const type = resolveType(reference, input.userId);
|
|
150
|
+
if (!type) {
|
|
151
|
+
throw new UnknownAbsenceTypeError(reference);
|
|
152
|
+
}
|
|
153
|
+
return type;
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
types(options) {
|
|
157
|
+
var _a;
|
|
158
|
+
const activeOnly = (options == null ? void 0 : options.includeInactive) === true ? "" : " AND is_active = 1";
|
|
159
|
+
const userId = (_a = options == null ? void 0 : options.userId) != null ? _a : null;
|
|
160
|
+
const rows = db.prepare(
|
|
161
|
+
`SELECT ${TYPE_COLUMNS} FROM absence_types
|
|
162
|
+
WHERE (user_id IS NULL OR user_id = ?)${activeOnly}
|
|
163
|
+
ORDER BY code, id`
|
|
164
|
+
).all(userId);
|
|
165
|
+
return rows.map(mapAbsenceTypeRow);
|
|
166
|
+
},
|
|
167
|
+
findType(reference, userId) {
|
|
168
|
+
return resolveType(reference, userId);
|
|
169
|
+
},
|
|
170
|
+
upsertType(input) {
|
|
171
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
172
|
+
const userId = (_a = input.userId) != null ? _a : null;
|
|
173
|
+
const code = input.code.trim().toUpperCase();
|
|
174
|
+
if (!/^[A-Za-z0-9_-]{1,8}$/.test(code)) {
|
|
175
|
+
throw new import_errors.ValidationError(`code must be 1 to 8 letters, digits, "-" or "_" (got "${input.code}")`);
|
|
176
|
+
}
|
|
177
|
+
const name = input.name.trim();
|
|
178
|
+
if (!name) {
|
|
179
|
+
throw new import_errors.ValidationError("name is required");
|
|
180
|
+
}
|
|
181
|
+
const factor = (_b = input.factor) != null ? _b : 100;
|
|
182
|
+
if (!Number.isInteger(factor) || factor < 0 || factor > 100) {
|
|
183
|
+
throw new import_errors.ValidationError(`factor must be a whole number between 0 and 100 (got ${input.factor})`);
|
|
184
|
+
}
|
|
185
|
+
const paid = (_c = input.paid) != null ? _c : true;
|
|
186
|
+
const reduceVacation = (_d = input.reduceVacation) != null ? _d : false;
|
|
187
|
+
const isActive = (_e = input.isActive) != null ? _e : true;
|
|
188
|
+
const now = (_f = input.now) != null ? _f : Math.floor(Date.now() / 1e3);
|
|
189
|
+
const existing = resolveType(code, userId);
|
|
190
|
+
let typeId = (_g = existing == null ? void 0 : existing.id) != null ? _g : 0;
|
|
191
|
+
const run = db.transaction(() => {
|
|
192
|
+
var _a2;
|
|
193
|
+
if (existing) {
|
|
194
|
+
updateType.run(name, paid ? 1 : 0, factor, reduceVacation ? 1 : 0, isActive ? 1 : 0, existing.id);
|
|
195
|
+
} else {
|
|
196
|
+
const result = insertType.run(
|
|
197
|
+
userId,
|
|
198
|
+
code,
|
|
199
|
+
name,
|
|
200
|
+
paid ? 1 : 0,
|
|
201
|
+
factor,
|
|
202
|
+
reduceVacation ? 1 : 0,
|
|
203
|
+
isActive ? 1 : 0
|
|
204
|
+
);
|
|
205
|
+
typeId = Number(result.lastInsertRowid);
|
|
206
|
+
}
|
|
207
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
208
|
+
atUtc: now,
|
|
209
|
+
actorId: input.actorId,
|
|
210
|
+
ip: (_a2 = input.actorIp) != null ? _a2 : null,
|
|
211
|
+
action: existing ? "absence_type.update" : "absence_type.create",
|
|
212
|
+
entity: "absence_types",
|
|
213
|
+
entityId: typeId,
|
|
214
|
+
detail: { code, userId, name, paid, factor, reduceVacation, isActive }
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
run();
|
|
218
|
+
const type = existing ? { ...existing, name, paid, factor, reduceVacation, isActive } : {
|
|
219
|
+
id: typeId,
|
|
220
|
+
userId,
|
|
221
|
+
code,
|
|
222
|
+
name,
|
|
223
|
+
paid,
|
|
224
|
+
factor,
|
|
225
|
+
reduceVacation,
|
|
226
|
+
isActive
|
|
227
|
+
};
|
|
228
|
+
return { type, created: existing === null };
|
|
229
|
+
},
|
|
230
|
+
findById: read,
|
|
231
|
+
create(input) {
|
|
232
|
+
var _a, _b, _c, _d;
|
|
233
|
+
const type = requireType(input);
|
|
234
|
+
const dateFrom = requireDate(input.dateFrom, "dateFrom");
|
|
235
|
+
const dateTo = requireDate((_a = input.dateTo) != null ? _a : dateFrom, "dateTo");
|
|
236
|
+
if (dateFrom > dateTo) {
|
|
237
|
+
throw new import_errors.ValidationError(`dateFrom (${dateFrom}) must not be after dateTo (${dateTo})`);
|
|
238
|
+
}
|
|
239
|
+
const dayPortion = requireDayPortion((_b = input.dayPortion) != null ? _b : 1);
|
|
240
|
+
const status = (_c = input.status) != null ? _c : "planned";
|
|
241
|
+
const now = (_d = input.now) != null ? _d : Math.floor(Date.now() / 1e3);
|
|
242
|
+
let absenceId = 0;
|
|
243
|
+
const run = db.transaction(() => {
|
|
244
|
+
var _a2, _b2, _c2;
|
|
245
|
+
const result = insertAbsence.run(
|
|
246
|
+
input.userId,
|
|
247
|
+
type.id,
|
|
248
|
+
dateFrom,
|
|
249
|
+
dateTo,
|
|
250
|
+
dayPortion,
|
|
251
|
+
(_a2 = input.hours) != null ? _a2 : null,
|
|
252
|
+
status,
|
|
253
|
+
(_b2 = input.note) != null ? _b2 : null,
|
|
254
|
+
now,
|
|
255
|
+
input.actorId
|
|
256
|
+
);
|
|
257
|
+
absenceId = Number(result.lastInsertRowid);
|
|
258
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
259
|
+
atUtc: now,
|
|
260
|
+
actorId: input.actorId,
|
|
261
|
+
action: "absence.create",
|
|
262
|
+
entity: "absence",
|
|
263
|
+
entityId: absenceId,
|
|
264
|
+
detail: {
|
|
265
|
+
userId: input.userId,
|
|
266
|
+
typeCode: type.code,
|
|
267
|
+
dateFrom,
|
|
268
|
+
dateTo,
|
|
269
|
+
dayPortion,
|
|
270
|
+
status
|
|
271
|
+
},
|
|
272
|
+
ip: (_c2 = input.actorIp) != null ? _c2 : null
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
run();
|
|
276
|
+
const created = read(absenceId);
|
|
277
|
+
if (!created) {
|
|
278
|
+
throw new Error(`absence ${absenceId} disappeared right after creation`);
|
|
279
|
+
}
|
|
280
|
+
return created;
|
|
281
|
+
},
|
|
282
|
+
update(input) {
|
|
283
|
+
var _a, _b;
|
|
284
|
+
const current = read(input.id);
|
|
285
|
+
if (!current) {
|
|
286
|
+
throw new import_errors.NotFoundError(`absence ${input.id} not found`);
|
|
287
|
+
}
|
|
288
|
+
const type = input.patch.typeId === void 0 && input.patch.typeCode === void 0 ? null : requireType({ ...input.patch, userId: current.userId });
|
|
289
|
+
const next = {
|
|
290
|
+
...current,
|
|
291
|
+
typeId: (_a = type == null ? void 0 : type.id) != null ? _a : current.typeId,
|
|
292
|
+
dateFrom: input.patch.dateFrom ? requireDate(input.patch.dateFrom, "dateFrom") : current.dateFrom,
|
|
293
|
+
dateTo: input.patch.dateTo ? requireDate(input.patch.dateTo, "dateTo") : current.dateTo,
|
|
294
|
+
dayPortion: input.patch.dayPortion === void 0 ? current.dayPortion : requireDayPortion(input.patch.dayPortion),
|
|
295
|
+
hours: input.patch.hours === void 0 ? current.hours : input.patch.hours,
|
|
296
|
+
note: input.patch.note === void 0 ? current.note : input.patch.note
|
|
297
|
+
};
|
|
298
|
+
if (next.dateFrom > next.dateTo) {
|
|
299
|
+
throw new import_errors.ValidationError(`dateFrom (${next.dateFrom}) must not be after dateTo (${next.dateTo})`);
|
|
300
|
+
}
|
|
301
|
+
const changes = (0, import_audit.diffFields)(
|
|
302
|
+
current,
|
|
303
|
+
next,
|
|
304
|
+
AUDITED_FIELDS
|
|
305
|
+
);
|
|
306
|
+
if (Object.keys(changes).length === 0) {
|
|
307
|
+
return current;
|
|
308
|
+
}
|
|
309
|
+
const now = (_b = input.now) != null ? _b : Math.floor(Date.now() / 1e3);
|
|
310
|
+
const run = db.transaction(() => {
|
|
311
|
+
var _a2, _b2;
|
|
312
|
+
updateAbsence.run(
|
|
313
|
+
next.typeId,
|
|
314
|
+
next.dateFrom,
|
|
315
|
+
next.dateTo,
|
|
316
|
+
next.dayPortion,
|
|
317
|
+
next.hours,
|
|
318
|
+
next.note,
|
|
319
|
+
input.id
|
|
320
|
+
);
|
|
321
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
322
|
+
atUtc: now,
|
|
323
|
+
actorId: input.actorId,
|
|
324
|
+
action: "absence.update",
|
|
325
|
+
entity: "absence",
|
|
326
|
+
entityId: input.id,
|
|
327
|
+
detail: { changes, reason: (_a2 = input.reason) != null ? _a2 : null },
|
|
328
|
+
ip: (_b2 = input.actorIp) != null ? _b2 : null
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
run();
|
|
332
|
+
const updated = read(input.id);
|
|
333
|
+
if (!updated) {
|
|
334
|
+
throw new Error(`absence ${input.id} disappeared right after the update`);
|
|
335
|
+
}
|
|
336
|
+
return updated;
|
|
337
|
+
},
|
|
338
|
+
setStatus(input) {
|
|
339
|
+
var _a;
|
|
340
|
+
const current = read(input.id);
|
|
341
|
+
if (!current) {
|
|
342
|
+
throw new import_errors.NotFoundError(`absence ${input.id} not found`);
|
|
343
|
+
}
|
|
344
|
+
if (current.status === input.status) {
|
|
345
|
+
return current;
|
|
346
|
+
}
|
|
347
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
348
|
+
const run = db.transaction(() => {
|
|
349
|
+
var _a2;
|
|
350
|
+
updateStatusStatement.run(input.status, input.id);
|
|
351
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
352
|
+
atUtc: now,
|
|
353
|
+
actorId: input.actorId,
|
|
354
|
+
action: "absence.status",
|
|
355
|
+
entity: "absence",
|
|
356
|
+
entityId: input.id,
|
|
357
|
+
detail: { changes: { status: { old: current.status, new: input.status } } },
|
|
358
|
+
ip: (_a2 = input.actorIp) != null ? _a2 : null
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
run();
|
|
362
|
+
const updated = read(input.id);
|
|
363
|
+
if (!updated) {
|
|
364
|
+
throw new Error(`absence ${input.id} disappeared right after the status change`);
|
|
365
|
+
}
|
|
366
|
+
return updated;
|
|
367
|
+
},
|
|
368
|
+
remove(input) {
|
|
369
|
+
var _a;
|
|
370
|
+
const current = read(input.id);
|
|
371
|
+
if (!current) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
375
|
+
const run = db.transaction(() => {
|
|
376
|
+
var _a2;
|
|
377
|
+
deleteAbsence.run(input.id);
|
|
378
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
379
|
+
atUtc: now,
|
|
380
|
+
actorId: input.actorId,
|
|
381
|
+
action: "absence.delete",
|
|
382
|
+
entity: "absence",
|
|
383
|
+
entityId: input.id,
|
|
384
|
+
detail: {
|
|
385
|
+
userId: current.userId,
|
|
386
|
+
dateFrom: current.dateFrom,
|
|
387
|
+
dateTo: current.dateTo,
|
|
388
|
+
status: current.status
|
|
389
|
+
},
|
|
390
|
+
ip: (_a2 = input.actorIp) != null ? _a2 : null
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
run();
|
|
394
|
+
return true;
|
|
395
|
+
},
|
|
396
|
+
listByUser(userId, options) {
|
|
397
|
+
var _a, _b;
|
|
398
|
+
const from = (_a = options == null ? void 0 : options.from) != null ? _a : (options == null ? void 0 : options.year) === void 0 ? "0000-01-01" : `${options.year}-01-01`;
|
|
399
|
+
const to = (_b = options == null ? void 0 : options.to) != null ? _b : (options == null ? void 0 : options.year) === void 0 ? "9999-12-31" : `${options.year}-12-31`;
|
|
400
|
+
const rows = selectByUser.all(userId, from, to);
|
|
401
|
+
return rows.map(mapAbsenceRow);
|
|
402
|
+
},
|
|
403
|
+
forDate(userId, date) {
|
|
404
|
+
const day = requireDate(date, "date");
|
|
405
|
+
const rows = selectByUser.all(userId, day, day);
|
|
406
|
+
return rows.map(mapAbsenceRow);
|
|
407
|
+
},
|
|
408
|
+
withTypesInRange(userId, from, to) {
|
|
409
|
+
const rows = selectWithTypes.all(
|
|
410
|
+
userId,
|
|
411
|
+
requireDate(from, "from"),
|
|
412
|
+
requireDate(to, "to")
|
|
413
|
+
);
|
|
414
|
+
return rows.map((row) => ({
|
|
415
|
+
...mapAbsenceRow(row),
|
|
416
|
+
typeCode: row.type_code,
|
|
417
|
+
typeName: row.type_name,
|
|
418
|
+
paid: row.type_paid !== 0,
|
|
419
|
+
factor: row.type_factor,
|
|
420
|
+
reduceVacation: row.type_reduce_vacation !== 0
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
426
|
+
0 && (module.exports = {
|
|
427
|
+
UnknownAbsenceTypeError,
|
|
428
|
+
createAbsencesRepository,
|
|
429
|
+
mapAbsenceRow,
|
|
430
|
+
mapAbsenceTypeRow
|
|
431
|
+
});
|
|
432
|
+
//# sourceMappingURL=absences.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/lib/db/repositories/absences.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Absence repository (`absence_types`, `absences`).\n *\n * An absence is a date range plus a day portion, so half days and multi-day absences share one\n * representation. The status separates requested (`planned`) from booked (`taken`) absences; the\n * vacation balance reports used and planned days separately.\n */\n\nimport type { Db } from \"../database\";\nimport { isDateString } from \"../../util/time\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { diffFields, writeAuditLog } from \"./audit\";\n\n/** Status of an absence: requested/planned or already taken. */\nexport type AbsenceStatus = \"taken\" | \"planned\";\n\n/** An absence type (global when `userId` is `null`, otherwise user specific). */\nexport interface AbsenceTypeRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Owner, `null` for the global types */\n\tuserId: number | null;\n\t/** Short code, e.g. `F` for vacation */\n\tcode: string;\n\t/** Display name */\n\tname: string;\n\t/** True when the absence is paid */\n\tpaid: boolean;\n\t/** Percentage of the working time credited (100 = fully credited) */\n\tfactor: number;\n\t/** True for vacation, which reduces the vacation balance */\n\treduceVacation: boolean;\n\t/** Inactive types are hidden but keep their history */\n\tisActive: boolean;\n}\n\n/** Input for creating or updating an absence type. */\nexport interface UpsertAbsenceTypeInput {\n\t/** Owner, `null` (default) for the global types */\n\tuserId?: number | null;\n\t/** Short code, e.g. `F` for vacation */\n\tcode: string;\n\t/** Display name */\n\tname: string;\n\t/** True when the absence is paid, defaults to `true` */\n\tpaid?: boolean;\n\t/** Percentage of the working time credited (defaults to `100`) */\n\tfactor?: number;\n\t/** True for vacation, which reduces the vacation balance, defaults to `false` */\n\treduceVacation?: boolean;\n\t/** False hides the type but keeps its history, defaults to `true` */\n\tisActive?: boolean;\n\t/** Who creates or changes the type */\n\tactorId: number;\n\t/** Client IP address of the actor */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** An absence as stored in the database. */\nexport interface AbsenceRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Employee the absence belongs to */\n\tuserId: number;\n\t/** Absence type */\n\ttypeId: number;\n\t/** First day, local date `YYYY-MM-DD` */\n\tdateFrom: string;\n\t/** Last day, local date `YYYY-MM-DD` */\n\tdateTo: string;\n\t/** Portion of each day (1 = whole day, 0.5 = half day) */\n\tdayPortion: number;\n\t/** Hours of a partial day absence, informational only */\n\thours: number | null;\n\t/** Requested or already taken */\n\tstatus: AbsenceStatus;\n\t/** Free-form note */\n\tnote: string | null;\n\t/** Instant of creation, UTC epoch seconds */\n\tcreatedAt: number;\n\t/** Who created the absence */\n\tcreatedBy: number | null;\n}\n\n/** An absence together with its type (input for the calculation service). */\nexport interface AbsenceWithType extends AbsenceRecord {\n\t/** Short code of the type, e.g. `F` */\n\ttypeCode: string;\n\t/** Display name of the type */\n\ttypeName: string;\n\t/** True when the absence is paid */\n\tpaid: boolean;\n\t/** Percentage of the working time credited */\n\tfactor: number;\n\t/** True for vacation, which reduces the vacation balance */\n\treduceVacation: boolean;\n}\n\n/** Input for creating an absence. */\nexport interface CreateAbsenceInput {\n\t/** Employee the absence belongs to */\n\tuserId: number;\n\t/** Absence type by id (alternative to `typeCode`) */\n\ttypeId?: number;\n\t/** Absence type by code, user specific types win over the global ones */\n\ttypeCode?: string;\n\t/** First day, local date `YYYY-MM-DD` */\n\tdateFrom: string;\n\t/** Last day, defaults to `dateFrom` */\n\tdateTo?: string;\n\t/** Portion of each day, default 1 */\n\tdayPortion?: number;\n\t/** Hours of a partial day absence */\n\thours?: number | null;\n\t/** Status, default `planned` (a request that still needs approval) */\n\tstatus?: AbsenceStatus;\n\t/** Free-form note */\n\tnote?: string | null;\n\t/** Who creates the absence */\n\tactorId: number;\n\t/** Client IP address of the actor */\n\tactorIp?: string | null;\n\t/** Instant of creation, defaults to now */\n\tnow?: number;\n}\n\n/** Input for changing an absence. */\nexport interface UpdateAbsenceInput {\n\t/** Id of the changed absence */\n\tid: number;\n\t/** Fields to change */\n\tpatch: {\n\t\ttypeId?: number;\n\t\ttypeCode?: string;\n\t\tdateFrom?: string;\n\t\tdateTo?: string;\n\t\tdayPortion?: number;\n\t\thours?: number | null;\n\t\tnote?: string | null;\n\t};\n\t/** Reason of the change (recorded in the audit trail) */\n\treason?: string | null;\n\t/** Who performs the change */\n\tactorId: number;\n\t/** Client IP address of the actor */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** Thrown when an absence type cannot be resolved. */\nexport class UnknownAbsenceTypeError extends Error {\n\t/**\n\t * Creates the error.\n\t *\n\t * @param reference - id or code that could not be resolved\n\t */\n\tconstructor(public readonly reference: string | number) {\n\t\tsuper(`unknown absence type \"${reference}\"`);\n\t\tthis.name = \"UnknownAbsenceTypeError\";\n\t}\n}\n\n/** Absence storage operations. */\nexport interface AbsencesRepository {\n\t/** Absence types of a user, including the global ones */\n\ttypes(options?: { userId?: number | null; includeInactive?: boolean }): AbsenceTypeRecord[];\n\t/** Resolves a type by id or code, user specific types win over the global ones */\n\tfindType(reference: number | string, userId?: number | null): AbsenceTypeRecord | null;\n\t/** Creates an absence type or updates the existing one with the same code */\n\tupsertType(input: UpsertAbsenceTypeInput): { type: AbsenceTypeRecord; created: boolean };\n\t/** Reads an absence by id */\n\tfindById(id: number): AbsenceRecord | null;\n\t/** Creates an absence and audits it */\n\tcreate(input: CreateAbsenceInput): AbsenceRecord;\n\t/** Changes an absence and audits the changed fields */\n\tupdate(input: UpdateAbsenceInput): AbsenceRecord;\n\t/** Moves an absence between `planned` and `taken` */\n\tsetStatus(input: {\n\t\tid: number;\n\t\tstatus: AbsenceStatus;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): AbsenceRecord;\n\t/** Deletes an absence by id */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n\t/** Absences of one user within an optional period, sorted by start date */\n\tlistByUser(userId: number, options?: { from?: string; to?: string; year?: number }): AbsenceRecord[];\n\t/** Absences overlapping a date, sorted by start date */\n\tforDate(userId: number, date: string): AbsenceRecord[];\n\t/** Absences overlapping a range, including their type information */\n\twithTypesInRange(userId: number, from: string, to: string): AbsenceWithType[];\n}\n\ninterface AbsenceTypeRow {\n\tid: number;\n\tuser_id: number | null;\n\tcode: string;\n\tname: string;\n\tpaid: number;\n\tfactor: number;\n\treduce_vacation: number;\n\tis_active: number;\n}\n\ninterface AbsenceRow {\n\tid: number;\n\tuser_id: number;\n\ttype_id: number;\n\tdate_from: string;\n\tdate_to: string;\n\tday_portion: number;\n\thours: number | null;\n\tstatus: AbsenceStatus;\n\tnote: string | null;\n\tcreated_at: number;\n\tcreated_by: number | null;\n}\n\ninterface AbsenceWithTypeRow extends AbsenceRow {\n\ttype_code: string;\n\ttype_name: string;\n\ttype_paid: number;\n\ttype_factor: number;\n\ttype_reduce_vacation: number;\n}\n\nconst TYPE_COLUMNS = \"id, user_id, code, name, paid, factor, reduce_vacation, is_active\";\nconst ABSENCE_COLUMNS = `id, user_id, type_id, date_from, date_to, day_portion, hours, status, note,\n\\tcreated_at, created_by`;\n\n/** Field names of an absence that are compared for the audit trail. */\nconst AUDITED_FIELDS: (keyof AbsenceRecord)[] = [\"typeId\", \"dateFrom\", \"dateTo\", \"dayPortion\", \"hours\", \"note\"];\n\n/**\n * Maps a database row to an absence type record.\n *\n * @param row - raw database row\n * @returns absence type record\n */\nexport function mapAbsenceTypeRow(row: AbsenceTypeRow): AbsenceTypeRecord {\n\treturn {\n\t\tid: row.id,\n\t\tuserId: row.user_id,\n\t\tcode: row.code,\n\t\tname: row.name,\n\t\tpaid: row.paid !== 0,\n\t\tfactor: row.factor,\n\t\treduceVacation: row.reduce_vacation !== 0,\n\t\tisActive: row.is_active !== 0,\n\t};\n}\n\n/**\n * Maps a database row to an absence record.\n *\n * @param row - raw database row\n * @returns absence record\n */\nexport function mapAbsenceRow(row: AbsenceRow): AbsenceRecord {\n\treturn {\n\t\tid: row.id,\n\t\tuserId: row.user_id,\n\t\ttypeId: row.type_id,\n\t\tdateFrom: row.date_from,\n\t\tdateTo: row.date_to,\n\t\tdayPortion: row.day_portion,\n\t\thours: row.hours,\n\t\tstatus: row.status,\n\t\tnote: row.note,\n\t\tcreatedAt: row.created_at,\n\t\tcreatedBy: row.created_by,\n\t};\n}\n\n/**\n * Checks and normalises a calendar date.\n *\n * @param value - date to validate\n * @param label - field name used in the error message\n * @returns the validated date\n */\nfunction requireDate(value: string, label: string): string {\n\tconst trimmed = value.trim();\n\tif (!isDateString(trimmed)) {\n\t\tthrow new ValidationError(`${label} \"${value}\" is not a date (YYYY-MM-DD)`);\n\t}\n\treturn trimmed;\n}\n\n/**\n * Validates the day portion of an absence.\n *\n * @param value - raw day portion\n * @returns the validated day portion\n */\nfunction requireDayPortion(value: number): number {\n\tif (!Number.isFinite(value) || value <= 0 || value > 1) {\n\t\tthrow new ValidationError(`dayPortion must be greater than 0 and at most 1 (got ${value})`);\n\t}\n\treturn value;\n}\n\n/**\n * Creates the absence repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createAbsencesRepository(db: Db): AbsencesRepository {\n\tconst selectTypeById = db.prepare(`SELECT ${TYPE_COLUMNS} FROM absence_types WHERE id = ?`);\n\tconst selectTypeUserCode = db.prepare(`SELECT ${TYPE_COLUMNS} FROM absence_types WHERE code = ? AND user_id = ?`);\n\tconst selectTypeGlobalCode = db.prepare(\n\t\t`SELECT ${TYPE_COLUMNS} FROM absence_types WHERE code = ? AND user_id IS NULL`,\n\t);\n\tconst selectAbsenceById = db.prepare(`SELECT ${ABSENCE_COLUMNS} FROM absences WHERE id = ?`);\n\tconst selectByUser = db.prepare(\n\t\t`SELECT ${ABSENCE_COLUMNS} FROM absences\n\t\t WHERE user_id = ? AND date_to >= ? AND date_from <= ?\n\t\t ORDER BY date_from, date_to, id`,\n\t);\n\tconst selectWithTypes = db.prepare(\n\t\t`SELECT a.id, a.user_id, a.type_id, a.date_from, a.date_to, a.day_portion, a.hours, a.status, a.note,\n\t\t a.created_at, a.created_by,\n\t\t t.code AS type_code, t.name AS type_name, t.paid AS type_paid, t.factor AS type_factor,\n\t\t t.reduce_vacation AS type_reduce_vacation\n\t\t FROM absences a JOIN absence_types t ON t.id = a.type_id\n\t\t WHERE a.user_id = ? AND a.date_to >= ? AND a.date_from <= ?\n\t\t ORDER BY a.date_from, a.date_to, a.id`,\n\t);\n\tconst insertAbsence = db.prepare(\n\t\t`INSERT INTO absences\n\t\t (user_id, type_id, date_from, date_to, day_portion, hours, status, note, created_at, created_by)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateAbsence = db.prepare(\n\t\t`UPDATE absences SET type_id = ?, date_from = ?, date_to = ?, day_portion = ?, hours = ?, note = ?\n\t\t WHERE id = ?`,\n\t);\n\tconst updateStatusStatement = db.prepare(\"UPDATE absences SET status = ? WHERE id = ?\");\n\tconst deleteAbsence = db.prepare(\"DELETE FROM absences WHERE id = ?\");\n\tconst insertType = db.prepare(\n\t\t`INSERT INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateType = db.prepare(\n\t\t`UPDATE absence_types SET name = ?, paid = ?, factor = ?, reduce_vacation = ?, is_active = ? WHERE id = ?`,\n\t);\n\n\tconst read = (id: number): AbsenceRecord | null => {\n\t\tconst row = selectAbsenceById.get(id) as AbsenceRow | undefined;\n\t\treturn row ? mapAbsenceRow(row) : null;\n\t};\n\n\t/**\n\t * Resolves a type by id or code (user specific types win over the global ones).\n\t *\n\t * @param reference - id or code\n\t * @param userId - owner used to prefer user specific types\n\t * @returns type record or `null`\n\t */\n\tconst resolveType = (reference: number | string, userId?: number | null): AbsenceTypeRecord | null => {\n\t\tif (typeof reference === \"number\") {\n\t\t\tconst row = selectTypeById.get(reference) as AbsenceTypeRow | undefined;\n\t\t\treturn row ? mapAbsenceTypeRow(row) : null;\n\t\t}\n\t\tif (userId != null) {\n\t\t\tconst own = selectTypeUserCode.get(reference, userId) as AbsenceTypeRow | undefined;\n\t\t\tif (own) {\n\t\t\t\treturn mapAbsenceTypeRow(own);\n\t\t\t}\n\t\t}\n\t\tconst global = selectTypeGlobalCode.get(reference) as AbsenceTypeRow | undefined;\n\t\treturn global ? mapAbsenceTypeRow(global) : null;\n\t};\n\n\t/**\n\t * Resolves a type or throws when it cannot be found.\n\t *\n\t * @param input - type reference and owner\n\t * @param input.typeId - type id (alternative to `input.typeCode`)\n\t * @param input.typeCode - type code (alternative to `input.typeId`)\n\t * @param input.userId - owner used to prefer user specific types\n\t * @returns resolved type\n\t */\n\tconst requireType = (input: { typeId?: number; typeCode?: string; userId: number }): AbsenceTypeRecord => {\n\t\tconst reference = input.typeId ?? input.typeCode;\n\t\tif (reference === undefined) {\n\t\t\tthrow new UnknownAbsenceTypeError(\"(none)\");\n\t\t}\n\t\tconst type = resolveType(reference, input.userId);\n\t\tif (!type) {\n\t\t\tthrow new UnknownAbsenceTypeError(reference);\n\t\t}\n\t\treturn type;\n\t};\n\n\treturn {\n\t\ttypes(options?: { userId?: number | null; includeInactive?: boolean }): AbsenceTypeRecord[] {\n\t\t\tconst activeOnly = options?.includeInactive === true ? \"\" : \" AND is_active = 1\";\n\t\t\tconst userId = options?.userId ?? null;\n\t\t\tconst rows = db\n\t\t\t\t.prepare(\n\t\t\t\t\t`SELECT ${TYPE_COLUMNS} FROM absence_types\n\t\t\t\t\t WHERE (user_id IS NULL OR user_id = ?)${activeOnly}\n\t\t\t\t\t ORDER BY code, id`,\n\t\t\t\t)\n\t\t\t\t.all(userId) as AbsenceTypeRow[];\n\t\t\treturn rows.map(mapAbsenceTypeRow);\n\t\t},\n\n\t\tfindType(reference: number | string, userId?: number | null): AbsenceTypeRecord | null {\n\t\t\treturn resolveType(reference, userId);\n\t\t},\n\n\t\tupsertType(input: UpsertAbsenceTypeInput): { type: AbsenceTypeRecord; created: boolean } {\n\t\t\tconst userId = input.userId ?? null;\n\t\t\tconst code = input.code.trim().toUpperCase();\n\t\t\tif (!/^[A-Za-z0-9_-]{1,8}$/.test(code)) {\n\t\t\t\tthrow new ValidationError(`code must be 1 to 8 letters, digits, \"-\" or \"_\" (got \"${input.code}\")`);\n\t\t\t}\n\t\t\tconst name = input.name.trim();\n\t\t\tif (!name) {\n\t\t\t\tthrow new ValidationError(\"name is required\");\n\t\t\t}\n\t\t\tconst factor = input.factor ?? 100;\n\t\t\tif (!Number.isInteger(factor) || factor < 0 || factor > 100) {\n\t\t\t\tthrow new ValidationError(`factor must be a whole number between 0 and 100 (got ${input.factor})`);\n\t\t\t}\n\t\t\tconst paid = input.paid ?? true;\n\t\t\tconst reduceVacation = input.reduceVacation ?? false;\n\t\t\tconst isActive = input.isActive ?? true;\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst existing = resolveType(code, userId);\n\t\t\tlet typeId = existing?.id ?? 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tif (existing) {\n\t\t\t\t\tupdateType.run(name, paid ? 1 : 0, factor, reduceVacation ? 1 : 0, isActive ? 1 : 0, existing.id);\n\t\t\t\t} else {\n\t\t\t\t\tconst result = insertType.run(\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\tcode,\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tpaid ? 1 : 0,\n\t\t\t\t\t\tfactor,\n\t\t\t\t\t\treduceVacation ? 1 : 0,\n\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t);\n\t\t\t\t\ttypeId = Number(result.lastInsertRowid);\n\t\t\t\t}\n\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t\taction: existing ? \"absence_type.update\" : \"absence_type.create\",\n\t\t\t\t\tentity: \"absence_types\",\n\t\t\t\t\tentityId: typeId,\n\t\t\t\t\tdetail: { code, userId, name, paid, factor, reduceVacation, isActive },\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst type = existing\n\t\t\t\t? { ...existing, name, paid, factor, reduceVacation, isActive }\n\t\t\t\t: {\n\t\t\t\t\t\tid: typeId,\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\tcode,\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tpaid,\n\t\t\t\t\t\tfactor,\n\t\t\t\t\t\treduceVacation,\n\t\t\t\t\t\tisActive,\n\t\t\t\t\t};\n\t\t\treturn { type, created: existing === null };\n\t\t},\n\n\t\tfindById: read,\n\n\t\tcreate(input: CreateAbsenceInput): AbsenceRecord {\n\t\t\tconst type = requireType(input);\n\t\t\tconst dateFrom = requireDate(input.dateFrom, \"dateFrom\");\n\t\t\tconst dateTo = requireDate(input.dateTo ?? dateFrom, \"dateTo\");\n\t\t\tif (dateFrom > dateTo) {\n\t\t\t\tthrow new ValidationError(`dateFrom (${dateFrom}) must not be after dateTo (${dateTo})`);\n\t\t\t}\n\t\t\tconst dayPortion = requireDayPortion(input.dayPortion ?? 1);\n\t\t\tconst status: AbsenceStatus = input.status ?? \"planned\";\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tlet absenceId = 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst result = insertAbsence.run(\n\t\t\t\t\tinput.userId,\n\t\t\t\t\ttype.id,\n\t\t\t\t\tdateFrom,\n\t\t\t\t\tdateTo,\n\t\t\t\t\tdayPortion,\n\t\t\t\t\tinput.hours ?? null,\n\t\t\t\t\tstatus,\n\t\t\t\t\tinput.note ?? null,\n\t\t\t\t\tnow,\n\t\t\t\t\tinput.actorId,\n\t\t\t\t);\n\t\t\t\tabsenceId = Number(result.lastInsertRowid);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"absence.create\",\n\t\t\t\t\tentity: \"absence\",\n\t\t\t\t\tentityId: absenceId,\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\tuserId: input.userId,\n\t\t\t\t\t\ttypeCode: type.code,\n\t\t\t\t\t\tdateFrom,\n\t\t\t\t\t\tdateTo,\n\t\t\t\t\t\tdayPortion,\n\t\t\t\t\t\tstatus,\n\t\t\t\t\t},\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst created = read(absenceId);\n\t\t\tif (!created) {\n\t\t\t\tthrow new Error(`absence ${absenceId} disappeared right after creation`);\n\t\t\t}\n\t\t\treturn created;\n\t\t},\n\n\t\tupdate(input: UpdateAbsenceInput): AbsenceRecord {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\tthrow new NotFoundError(`absence ${input.id} not found`);\n\t\t\t}\n\n\t\t\tconst type =\n\t\t\t\tinput.patch.typeId === undefined && input.patch.typeCode === undefined\n\t\t\t\t\t? null\n\t\t\t\t\t: requireType({ ...input.patch, userId: current.userId });\n\t\t\tconst next: AbsenceRecord = {\n\t\t\t\t...current,\n\t\t\t\ttypeId: type?.id ?? current.typeId,\n\t\t\t\tdateFrom: input.patch.dateFrom ? requireDate(input.patch.dateFrom, \"dateFrom\") : current.dateFrom,\n\t\t\t\tdateTo: input.patch.dateTo ? requireDate(input.patch.dateTo, \"dateTo\") : current.dateTo,\n\t\t\t\tdayPortion:\n\t\t\t\t\tinput.patch.dayPortion === undefined\n\t\t\t\t\t\t? current.dayPortion\n\t\t\t\t\t\t: requireDayPortion(input.patch.dayPortion),\n\t\t\t\thours: input.patch.hours === undefined ? current.hours : input.patch.hours,\n\t\t\t\tnote: input.patch.note === undefined ? current.note : input.patch.note,\n\t\t\t};\n\t\t\tif (next.dateFrom > next.dateTo) {\n\t\t\t\tthrow new ValidationError(`dateFrom (${next.dateFrom}) must not be after dateTo (${next.dateTo})`);\n\t\t\t}\n\n\t\t\tconst changes = diffFields(\n\t\t\t\tcurrent as unknown as Record<string, unknown>,\n\t\t\t\tnext as unknown as Record<string, unknown>,\n\t\t\t\tAUDITED_FIELDS as unknown as (keyof Record<string, unknown>)[],\n\t\t\t);\n\t\t\tif (Object.keys(changes).length === 0) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupdateAbsence.run(\n\t\t\t\t\tnext.typeId,\n\t\t\t\t\tnext.dateFrom,\n\t\t\t\t\tnext.dateTo,\n\t\t\t\t\tnext.dayPortion,\n\t\t\t\t\tnext.hours,\n\t\t\t\t\tnext.note,\n\t\t\t\t\tinput.id,\n\t\t\t\t);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"absence.update\",\n\t\t\t\t\tentity: \"absence\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { changes, reason: input.reason ?? null },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst updated = read(input.id);\n\t\t\tif (!updated) {\n\t\t\t\tthrow new Error(`absence ${input.id} disappeared right after the update`);\n\t\t\t}\n\t\t\treturn updated;\n\t\t},\n\n\t\tsetStatus(input: {\n\t\t\tid: number;\n\t\t\tstatus: AbsenceStatus;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): AbsenceRecord {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\tthrow new NotFoundError(`absence ${input.id} not found`);\n\t\t\t}\n\t\t\tif (current.status === input.status) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupdateStatusStatement.run(input.status, input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"absence.status\",\n\t\t\t\t\tentity: \"absence\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { changes: { status: { old: current.status, new: input.status } } },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst updated = read(input.id);\n\t\t\tif (!updated) {\n\t\t\t\tthrow new Error(`absence ${input.id} disappeared right after the status change`);\n\t\t\t}\n\t\t\treturn updated;\n\t\t},\n\n\t\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteAbsence.run(input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"absence.delete\",\n\t\t\t\t\tentity: \"absence\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\tuserId: current.userId,\n\t\t\t\t\t\tdateFrom: current.dateFrom,\n\t\t\t\t\t\tdateTo: current.dateTo,\n\t\t\t\t\t\tstatus: current.status,\n\t\t\t\t\t},\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn true;\n\t\t},\n\n\t\tlistByUser(userId: number, options?: { from?: string; to?: string; year?: number }): AbsenceRecord[] {\n\t\t\tconst from = options?.from ?? (options?.year === undefined ? \"0000-01-01\" : `${options.year}-01-01`);\n\t\t\tconst to = options?.to ?? (options?.year === undefined ? \"9999-12-31\" : `${options.year}-12-31`);\n\t\t\tconst rows = selectByUser.all(userId, from, to) as AbsenceRow[];\n\t\t\treturn rows.map(mapAbsenceRow);\n\t\t},\n\n\t\tforDate(userId: number, date: string): AbsenceRecord[] {\n\t\t\tconst day = requireDate(date, \"date\");\n\t\t\tconst rows = selectByUser.all(userId, day, day) as AbsenceRow[];\n\t\t\treturn rows.map(mapAbsenceRow);\n\t\t},\n\n\t\twithTypesInRange(userId: number, from: string, to: string): AbsenceWithType[] {\n\t\t\tconst rows = selectWithTypes.all(\n\t\t\t\tuserId,\n\t\t\t\trequireDate(from, \"from\"),\n\t\t\t\trequireDate(to, \"to\"),\n\t\t\t) as AbsenceWithTypeRow[];\n\t\t\treturn rows.map(row => ({\n\t\t\t\t...mapAbsenceRow(row),\n\t\t\t\ttypeCode: row.type_code,\n\t\t\t\ttypeName: row.type_name,\n\t\t\t\tpaid: row.type_paid !== 0,\n\t\t\t\tfactor: row.type_factor,\n\t\t\t\treduceVacation: row.type_reduce_vacation !== 0,\n\t\t\t}));\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["_a", "_b", "_c"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
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 audit_exports = {};
|
|
20
|
+
__export(audit_exports, {
|
|
21
|
+
diffFields: () => diffFields,
|
|
22
|
+
readTimeEntryAudit: () => readTimeEntryAudit,
|
|
23
|
+
writeAuditLog: () => writeAuditLog,
|
|
24
|
+
writeTimeEntryAudit: () => writeTimeEntryAudit
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(audit_exports);
|
|
27
|
+
function diffFields(before, after, fields) {
|
|
28
|
+
const changes = {};
|
|
29
|
+
for (const field of fields) {
|
|
30
|
+
if (before[field] !== after[field]) {
|
|
31
|
+
changes[String(field)] = { old: before[field], new: after[field] };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return changes;
|
|
35
|
+
}
|
|
36
|
+
function writeAuditLog(db, input) {
|
|
37
|
+
var _a, _b, _c, _d;
|
|
38
|
+
const result = db.prepare(
|
|
39
|
+
`INSERT INTO audit_log (at_utc, actor_id, action, entity, entity_id, detail, ip)
|
|
40
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
41
|
+
).run(
|
|
42
|
+
(_a = input.atUtc) != null ? _a : Math.floor(Date.now() / 1e3),
|
|
43
|
+
(_b = input.actorId) != null ? _b : null,
|
|
44
|
+
input.action,
|
|
45
|
+
(_c = input.entity) != null ? _c : null,
|
|
46
|
+
input.entityId == null ? null : String(input.entityId),
|
|
47
|
+
input.detail === void 0 ? null : JSON.stringify(input.detail),
|
|
48
|
+
(_d = input.ip) != null ? _d : null
|
|
49
|
+
);
|
|
50
|
+
return Number(result.lastInsertRowid);
|
|
51
|
+
}
|
|
52
|
+
function writeTimeEntryAudit(db, input) {
|
|
53
|
+
var _a, _b, _c, _d, _e, _f;
|
|
54
|
+
const result = db.prepare(
|
|
55
|
+
`INSERT INTO time_entry_audit
|
|
56
|
+
(entry_id, user_id, action, changes, old_ts_utc, new_ts_utc, revision, reason, actor_id, actor_ip, at_utc)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
58
|
+
).run(
|
|
59
|
+
input.entryId,
|
|
60
|
+
input.userId,
|
|
61
|
+
input.action,
|
|
62
|
+
input.changes ? JSON.stringify(input.changes) : null,
|
|
63
|
+
(_a = input.oldTsUtc) != null ? _a : null,
|
|
64
|
+
(_b = input.newTsUtc) != null ? _b : null,
|
|
65
|
+
(_c = input.revision) != null ? _c : null,
|
|
66
|
+
(_d = input.reason) != null ? _d : null,
|
|
67
|
+
input.actorId,
|
|
68
|
+
(_e = input.actorIp) != null ? _e : null,
|
|
69
|
+
(_f = input.atUtc) != null ? _f : Math.floor(Date.now() / 1e3)
|
|
70
|
+
);
|
|
71
|
+
return Number(result.lastInsertRowid);
|
|
72
|
+
}
|
|
73
|
+
function readTimeEntryAudit(db, entryId) {
|
|
74
|
+
const rows = db.prepare(
|
|
75
|
+
`SELECT id, action, changes, revision, reason, actor_id AS actorId, at_utc AS atUtc
|
|
76
|
+
FROM time_entry_audit WHERE entry_id = ? ORDER BY id DESC`
|
|
77
|
+
).all(entryId);
|
|
78
|
+
return rows.map((row) => ({
|
|
79
|
+
...row,
|
|
80
|
+
changes: row.changes ? JSON.parse(row.changes) : null
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
84
|
+
0 && (module.exports = {
|
|
85
|
+
diffFields,
|
|
86
|
+
readTimeEntryAudit,
|
|
87
|
+
writeAuditLog,
|
|
88
|
+
writeTimeEntryAudit
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/lib/db/repositories/audit.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Audit trail helpers: every change of a punch and every administrative action is recorded append-only.\n */\n\nimport type { Db } from \"../database\";\n\n/** A single change of one field. */\nexport interface FieldChange {\n\t/** Value before the change */\n\told: unknown;\n\t/** Value after the change */\n\tnew: unknown;\n}\n\n/**\n * Compares the old and the new value of every field and collects the differences.\n *\n * @param before - record before the change\n * @param after - record after the change\n * @param fields - field names to compare\n * @returns changed fields, empty when nothing changed\n */\nexport function diffFields<T extends Record<string, unknown>>(\n\tbefore: T,\n\tafter: T,\n\tfields: (keyof T)[],\n): Record<string, FieldChange> {\n\tconst changes: Record<string, FieldChange> = {};\n\tfor (const field of fields) {\n\t\tif (before[field] !== after[field]) {\n\t\t\tchanges[String(field)] = { old: before[field], new: after[field] };\n\t\t}\n\t}\n\treturn changes;\n}\n\n/** Input for a generic audit log entry. */\nexport interface AuditLogInput {\n\t/** Actor user id, `null` for system actions (import, scheduled jobs) */\n\tactorId?: number | null;\n\t/** Short action key, e.g. `entry.update`, `settings.set` */\n\taction: string;\n\t/** Affected entity, e.g. `time_entry`, `user` */\n\tentity?: string | null;\n\t/** Id of the affected entity */\n\tentityId?: string | number | null;\n\t/** Free-form detail (serialised as JSON) */\n\tdetail?: unknown;\n\t/** Client IP address */\n\tip?: string | null;\n\t/** Instant of the action, defaults to now */\n\tatUtc?: number;\n}\n\n/** Input for a punch audit entry. */\nexport interface TimeEntryAuditInput {\n\t/** Punch id, `null` when the punch was deleted */\n\tentryId: number | null;\n\t/** Owner of the punch */\n\tuserId: number;\n\t/** Kind of change */\n\taction: \"create\" | \"update\" | \"delete\";\n\t/** Changed fields (serialised as JSON) */\n\tchanges?: Record<string, FieldChange> | null;\n\t/** Timestamp before the change */\n\toldTsUtc?: number | null;\n\t/** Timestamp after the change */\n\tnewTsUtc?: number | null;\n\t/** Revision after the change */\n\trevision?: number | null;\n\t/** Mandatory reason for administrative corrections */\n\treason?: string | null;\n\t/** Who performed the change */\n\tactorId: number;\n\t/** Client IP address */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tatUtc?: number;\n}\n\n/**\n * Writes an entry to the generic audit log.\n *\n * @param db - open database handle\n * @param input - audit information\n * @returns id of the created audit row\n */\nexport function writeAuditLog(db: Db, input: AuditLogInput): number {\n\tconst result = db\n\t\t.prepare(\n\t\t\t`INSERT INTO audit_log (at_utc, actor_id, action, entity, entity_id, detail, ip)\n\t\t\t VALUES (?, ?, ?, ?, ?, ?, ?)`,\n\t\t)\n\t\t.run(\n\t\t\tinput.atUtc ?? Math.floor(Date.now() / 1000),\n\t\t\tinput.actorId ?? null,\n\t\t\tinput.action,\n\t\t\tinput.entity ?? null,\n\t\t\tinput.entityId == null ? null : String(input.entityId),\n\t\t\tinput.detail === undefined ? null : JSON.stringify(input.detail),\n\t\t\tinput.ip ?? null,\n\t\t);\n\treturn Number(result.lastInsertRowid);\n}\n\n/**\n * Writes an entry to the punch audit trail (append-only).\n *\n * @param db - open database handle\n * @param input - punch audit information\n * @returns id of the created audit row\n */\nexport function writeTimeEntryAudit(db: Db, input: TimeEntryAuditInput): number {\n\tconst result = db\n\t\t.prepare(\n\t\t\t`INSERT INTO time_entry_audit\n\t\t\t (entry_id, user_id, action, changes, old_ts_utc, new_ts_utc, revision, reason, actor_id, actor_ip, at_utc)\n\t\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t\t)\n\t\t.run(\n\t\t\tinput.entryId,\n\t\t\tinput.userId,\n\t\t\tinput.action,\n\t\t\tinput.changes ? JSON.stringify(input.changes) : null,\n\t\t\tinput.oldTsUtc ?? null,\n\t\t\tinput.newTsUtc ?? null,\n\t\t\tinput.revision ?? null,\n\t\t\tinput.reason ?? null,\n\t\t\tinput.actorId,\n\t\t\tinput.actorIp ?? null,\n\t\t\tinput.atUtc ?? Math.floor(Date.now() / 1000),\n\t\t);\n\treturn Number(result.lastInsertRowid);\n}\n\n/**\n * Reads the audit trail of one punch (newest first).\n *\n * @param db - open database handle\n * @param entryId - punch id\n * @returns audit rows with parsed `changes`\n */\nexport function readTimeEntryAudit(\n\tdb: Db,\n\tentryId: number,\n): {\n\tid: number;\n\taction: string;\n\tchanges: Record<string, FieldChange> | null;\n\trevision: number | null;\n\treason: string | null;\n\tactorId: number;\n\tatUtc: number;\n}[] {\n\tconst rows = db\n\t\t.prepare(\n\t\t\t`SELECT id, action, changes, revision, reason, actor_id AS actorId, at_utc AS atUtc\n\t\t\t FROM time_entry_audit WHERE entry_id = ? ORDER BY id DESC`,\n\t\t)\n\t\t.all(entryId) as {\n\t\tid: number;\n\t\taction: string;\n\t\tchanges: string | null;\n\t\trevision: number | null;\n\t\treason: string | null;\n\t\tactorId: number;\n\t\tatUtc: number;\n\t}[];\n\n\treturn rows.map(row => ({\n\t\t...row,\n\t\tchanges: row.changes ? (JSON.parse(row.changes) as Record<string, FieldChange>) : null,\n\t}));\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBO,SAAS,WACf,QACA,OACA,QAC8B;AAC9B,QAAM,UAAuC,CAAC;AAC9C,aAAW,SAAS,QAAQ;AAC3B,QAAI,OAAO,KAAK,MAAM,MAAM,KAAK,GAAG;AACnC,cAAQ,OAAO,KAAK,CAAC,IAAI,EAAE,KAAK,OAAO,KAAK,GAAG,KAAK,MAAM,KAAK,EAAE;AAAA,IAClE;AAAA,EACD;AACA,SAAO;AACR;AAqDO,SAAS,cAAc,IAAQ,OAA8B;AAvFpE;AAwFC,QAAM,SAAS,GACb;AAAA,IACA;AAAA;AAAA,EAED,EACC;AAAA,KACA,WAAM,UAAN,YAAe,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,KAC3C,WAAM,YAAN,YAAiB;AAAA,IACjB,MAAM;AAAA,KACN,WAAM,WAAN,YAAgB;AAAA,IAChB,MAAM,YAAY,OAAO,OAAO,OAAO,MAAM,QAAQ;AAAA,IACrD,MAAM,WAAW,SAAY,OAAO,KAAK,UAAU,MAAM,MAAM;AAAA,KAC/D,WAAM,OAAN,YAAY;AAAA,EACb;AACD,SAAO,OAAO,OAAO,eAAe;AACrC;AASO,SAAS,oBAAoB,IAAQ,OAAoC;AAhHhF;AAiHC,QAAM,SAAS,GACb;AAAA,IACA;AAAA;AAAA;AAAA,EAGD,EACC;AAAA,IACA,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM,UAAU,KAAK,UAAU,MAAM,OAAO,IAAI;AAAA,KAChD,WAAM,aAAN,YAAkB;AAAA,KAClB,WAAM,aAAN,YAAkB;AAAA,KAClB,WAAM,aAAN,YAAkB;AAAA,KAClB,WAAM,WAAN,YAAgB;AAAA,IAChB,MAAM;AAAA,KACN,WAAM,YAAN,YAAiB;AAAA,KACjB,WAAM,UAAN,YAAe,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,EAC5C;AACD,SAAO,OAAO,OAAO,eAAe;AACrC;AASO,SAAS,mBACf,IACA,SASG;AACH,QAAM,OAAO,GACX;AAAA,IACA;AAAA;AAAA,EAED,EACC,IAAI,OAAO;AAUb,SAAO,KAAK,IAAI,UAAQ;AAAA,IACvB,GAAG;AAAA,IACH,SAAS,IAAI,UAAW,KAAK,MAAM,IAAI,OAAO,IAAoC;AAAA,EACnF,EAAE;AACH;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|