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,603 @@
|
|
|
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 migrations_exports = {};
|
|
20
|
+
__export(migrations_exports, {
|
|
21
|
+
hasColumn: () => hasColumn,
|
|
22
|
+
migrations: () => migrations
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(migrations_exports);
|
|
25
|
+
function hasColumn(db, table, column) {
|
|
26
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
27
|
+
return rows.some((row) => row.name === column);
|
|
28
|
+
}
|
|
29
|
+
const migrations = [
|
|
30
|
+
{
|
|
31
|
+
version: 1,
|
|
32
|
+
name: "core: users, roles, permissions, work profiles, shift and pause rules, settings, sessions, audit, import runs",
|
|
33
|
+
sql: `
|
|
34
|
+
CREATE TABLE users (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
login TEXT NOT NULL UNIQUE,
|
|
37
|
+
password_hash TEXT NOT NULL DEFAULT '',
|
|
38
|
+
display_name TEXT NOT NULL,
|
|
39
|
+
email TEXT,
|
|
40
|
+
rfid_card TEXT,
|
|
41
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
42
|
+
must_change_pw INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
locale TEXT NOT NULL DEFAULT 'de-DE',
|
|
44
|
+
timezone TEXT NOT NULL DEFAULT 'Europe/Berlin',
|
|
45
|
+
created_at INTEGER NOT NULL,
|
|
46
|
+
updated_at INTEGER NOT NULL,
|
|
47
|
+
last_login_at INTEGER
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE roles (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
key TEXT NOT NULL UNIQUE,
|
|
53
|
+
name TEXT NOT NULL
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE permissions (
|
|
57
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
58
|
+
key TEXT NOT NULL UNIQUE
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE role_permissions (
|
|
62
|
+
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
|
63
|
+
permission_id INTEGER NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
|
64
|
+
PRIMARY KEY (role_id, permission_id)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE user_roles (
|
|
68
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
69
|
+
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
|
70
|
+
PRIMARY KEY (user_id, role_id)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE work_profiles (
|
|
74
|
+
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
75
|
+
percent REAL NOT NULL DEFAULT 100,
|
|
76
|
+
weekly_hours REAL NOT NULL DEFAULT 42.5,
|
|
77
|
+
workdays TEXT NOT NULL DEFAULT '0;1;1;1;1;1;0',
|
|
78
|
+
start_date INTEGER,
|
|
79
|
+
end_date INTEGER,
|
|
80
|
+
overtime_carryover INTEGER NOT NULL DEFAULT 0,
|
|
81
|
+
vorholzeit_per_year INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
vacation_carryover REAL NOT NULL DEFAULT 0,
|
|
83
|
+
vacation_per_year REAL NOT NULL DEFAULT 0,
|
|
84
|
+
overtime_model TEXT NOT NULL DEFAULT 'monthly'
|
|
85
|
+
CHECK (overtime_model IN ('cumulative','yearly','monthly')),
|
|
86
|
+
holiday_flags TEXT
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE shift_rules (
|
|
90
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
91
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
92
|
+
day_of_week INTEGER CHECK (day_of_week IS NULL OR day_of_week BETWEEN 0 AND 6),
|
|
93
|
+
from_min INTEGER,
|
|
94
|
+
to_min INTEGER,
|
|
95
|
+
surcharge REAL NOT NULL DEFAULT 0,
|
|
96
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE TABLE pause_rules (
|
|
100
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
101
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
102
|
+
from_min INTEGER NOT NULL,
|
|
103
|
+
to_min INTEGER,
|
|
104
|
+
pause_min INTEGER NOT NULL,
|
|
105
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
106
|
+
CHECK (to_min IS NULL OR to_min > from_min)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
CREATE TABLE app_settings (
|
|
110
|
+
key TEXT PRIMARY KEY,
|
|
111
|
+
value TEXT NOT NULL,
|
|
112
|
+
updated_at INTEGER NOT NULL
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
CREATE TABLE sessions (
|
|
116
|
+
id TEXT PRIMARY KEY,
|
|
117
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
118
|
+
created_at INTEGER NOT NULL,
|
|
119
|
+
expires_at INTEGER NOT NULL,
|
|
120
|
+
user_agent TEXT,
|
|
121
|
+
ip TEXT,
|
|
122
|
+
revoked INTEGER NOT NULL DEFAULT 0
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
CREATE TABLE audit_log (
|
|
126
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
127
|
+
at_utc INTEGER NOT NULL,
|
|
128
|
+
actor_id INTEGER,
|
|
129
|
+
action TEXT NOT NULL,
|
|
130
|
+
entity TEXT,
|
|
131
|
+
entity_id TEXT,
|
|
132
|
+
detail TEXT,
|
|
133
|
+
ip TEXT
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
CREATE TABLE import_runs (
|
|
137
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
138
|
+
source TEXT NOT NULL,
|
|
139
|
+
source_path TEXT NOT NULL,
|
|
140
|
+
started_at INTEGER NOT NULL,
|
|
141
|
+
finished_at INTEGER,
|
|
142
|
+
mode TEXT NOT NULL DEFAULT 'dry-run'
|
|
143
|
+
CHECK (mode IN ('dry-run','commit')),
|
|
144
|
+
status TEXT NOT NULL DEFAULT 'running'
|
|
145
|
+
CHECK (status IN ('running','ok','warnings','mismatch','failed')),
|
|
146
|
+
timezone_assumed TEXT,
|
|
147
|
+
stats TEXT,
|
|
148
|
+
warnings TEXT,
|
|
149
|
+
actor_id INTEGER REFERENCES users(id)
|
|
150
|
+
);
|
|
151
|
+
`
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
version: 2,
|
|
155
|
+
name: "time: entries, audit, day/month/year aggregates, payouts",
|
|
156
|
+
sql: `
|
|
157
|
+
CREATE TABLE time_entries (
|
|
158
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
159
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
160
|
+
ts_utc INTEGER NOT NULL,
|
|
161
|
+
client_ts_utc INTEGER,
|
|
162
|
+
ts_local INTEGER NOT NULL,
|
|
163
|
+
local_date TEXT NOT NULL,
|
|
164
|
+
direction TEXT NOT NULL DEFAULT 'auto'
|
|
165
|
+
CHECK (direction IN ('in','out','auto')),
|
|
166
|
+
source TEXT NOT NULL DEFAULT 'web'
|
|
167
|
+
CHECK (source IN ('web','nfc','terminal','api','admin','import')),
|
|
168
|
+
idempotency_key TEXT,
|
|
169
|
+
sync_state TEXT NOT NULL DEFAULT 'synced'
|
|
170
|
+
CHECK (sync_state IN ('synced','pending','conflict')),
|
|
171
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
172
|
+
note TEXT,
|
|
173
|
+
created_by INTEGER REFERENCES users(id),
|
|
174
|
+
created_at INTEGER NOT NULL,
|
|
175
|
+
updated_at INTEGER NOT NULL,
|
|
176
|
+
updated_by INTEGER REFERENCES users(id)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
CREATE INDEX idx_entries_user_date ON time_entries(user_id, local_date);
|
|
180
|
+
CREATE INDEX idx_entries_user_ts ON time_entries(user_id, ts_utc);
|
|
181
|
+
CREATE UNIQUE INDEX idx_entries_idem
|
|
182
|
+
ON time_entries(user_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
|
|
183
|
+
|
|
184
|
+
CREATE TABLE time_entry_audit (
|
|
185
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
186
|
+
entry_id INTEGER,
|
|
187
|
+
user_id INTEGER NOT NULL,
|
|
188
|
+
action TEXT NOT NULL,
|
|
189
|
+
changes TEXT,
|
|
190
|
+
old_ts_utc INTEGER,
|
|
191
|
+
new_ts_utc INTEGER,
|
|
192
|
+
revision INTEGER,
|
|
193
|
+
reason TEXT,
|
|
194
|
+
actor_id INTEGER NOT NULL,
|
|
195
|
+
actor_ip TEXT,
|
|
196
|
+
at_utc INTEGER NOT NULL
|
|
197
|
+
);
|
|
198
|
+
CREATE INDEX idx_entry_audit_entry ON time_entry_audit(entry_id);
|
|
199
|
+
`
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
version: 3,
|
|
203
|
+
name: "aggregates: day, month, year and payouts",
|
|
204
|
+
sql: `
|
|
205
|
+
CREATE TABLE day_aggregates (
|
|
206
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
207
|
+
local_date TEXT NOT NULL,
|
|
208
|
+
worked_min INTEGER NOT NULL DEFAULT 0,
|
|
209
|
+
break_min INTEGER NOT NULL DEFAULT 0,
|
|
210
|
+
target_min INTEGER NOT NULL DEFAULT 0,
|
|
211
|
+
balance_min INTEGER NOT NULL DEFAULT 0,
|
|
212
|
+
absence_code TEXT,
|
|
213
|
+
is_holiday INTEGER NOT NULL DEFAULT 0,
|
|
214
|
+
first_in_utc INTEGER,
|
|
215
|
+
last_out_utc INTEGER,
|
|
216
|
+
has_open_entry INTEGER NOT NULL DEFAULT 0,
|
|
217
|
+
updated_at INTEGER NOT NULL,
|
|
218
|
+
PRIMARY KEY (user_id, local_date)
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
CREATE TABLE month_aggregates (
|
|
222
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
223
|
+
year INTEGER NOT NULL,
|
|
224
|
+
month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),
|
|
225
|
+
worked_min INTEGER NOT NULL DEFAULT 0,
|
|
226
|
+
target_min INTEGER NOT NULL DEFAULT 0,
|
|
227
|
+
balance_min INTEGER NOT NULL DEFAULT 0,
|
|
228
|
+
overtime_min INTEGER NOT NULL DEFAULT 0,
|
|
229
|
+
vacation_used REAL NOT NULL DEFAULT 0,
|
|
230
|
+
updated_at INTEGER NOT NULL,
|
|
231
|
+
PRIMARY KEY (user_id, year, month)
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
CREATE TABLE year_aggregates (
|
|
235
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
236
|
+
year INTEGER NOT NULL,
|
|
237
|
+
worked_min INTEGER NOT NULL DEFAULT 0,
|
|
238
|
+
target_min INTEGER NOT NULL DEFAULT 0,
|
|
239
|
+
overtime_min INTEGER NOT NULL DEFAULT 0,
|
|
240
|
+
overtime_start INTEGER NOT NULL DEFAULT 0,
|
|
241
|
+
vacation_days REAL NOT NULL DEFAULT 0,
|
|
242
|
+
vacation_used REAL NOT NULL DEFAULT 0,
|
|
243
|
+
vacation_left REAL NOT NULL DEFAULT 0,
|
|
244
|
+
vacation_planned_days REAL NOT NULL DEFAULT 0,
|
|
245
|
+
settled INTEGER NOT NULL DEFAULT 0,
|
|
246
|
+
updated_at INTEGER NOT NULL,
|
|
247
|
+
PRIMARY KEY (user_id, year)
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
CREATE TABLE payouts (
|
|
251
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
252
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
253
|
+
year INTEGER NOT NULL,
|
|
254
|
+
month INTEGER,
|
|
255
|
+
minutes INTEGER NOT NULL DEFAULT 0,
|
|
256
|
+
amount REAL,
|
|
257
|
+
note TEXT,
|
|
258
|
+
created_at INTEGER NOT NULL,
|
|
259
|
+
created_by INTEGER REFERENCES users(id)
|
|
260
|
+
);
|
|
261
|
+
CREATE INDEX idx_payouts_user ON payouts(user_id, year, month);
|
|
262
|
+
`
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
version: 4,
|
|
266
|
+
name: "absence: types, absences, holidays",
|
|
267
|
+
sql: `
|
|
268
|
+
CREATE TABLE absence_types (
|
|
269
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
270
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
271
|
+
code TEXT NOT NULL,
|
|
272
|
+
name TEXT NOT NULL,
|
|
273
|
+
paid INTEGER NOT NULL DEFAULT 1,
|
|
274
|
+
factor REAL NOT NULL DEFAULT 100,
|
|
275
|
+
reduce_vacation INTEGER NOT NULL DEFAULT 0,
|
|
276
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
CREATE UNIQUE INDEX idx_absence_types_global
|
|
280
|
+
ON absence_types(code) WHERE user_id IS NULL;
|
|
281
|
+
CREATE UNIQUE INDEX idx_absence_types_user
|
|
282
|
+
ON absence_types(user_id, code) WHERE user_id IS NOT NULL;
|
|
283
|
+
|
|
284
|
+
CREATE TABLE absences (
|
|
285
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
286
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
287
|
+
type_id INTEGER NOT NULL REFERENCES absence_types(id),
|
|
288
|
+
date_from TEXT NOT NULL,
|
|
289
|
+
date_to TEXT NOT NULL,
|
|
290
|
+
day_portion REAL NOT NULL DEFAULT 1 CHECK (day_portion > 0 AND day_portion <= 1),
|
|
291
|
+
hours REAL,
|
|
292
|
+
status TEXT NOT NULL DEFAULT 'taken'
|
|
293
|
+
CHECK (status IN ('taken','planned')),
|
|
294
|
+
note TEXT,
|
|
295
|
+
created_at INTEGER NOT NULL,
|
|
296
|
+
created_by INTEGER REFERENCES users(id),
|
|
297
|
+
CHECK (date_from <= date_to)
|
|
298
|
+
);
|
|
299
|
+
CREATE INDEX idx_absences_user_dates ON absences(user_id, date_from, date_to);
|
|
300
|
+
|
|
301
|
+
CREATE TABLE holidays (
|
|
302
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
303
|
+
region TEXT NOT NULL DEFAULT 'global',
|
|
304
|
+
year INTEGER NOT NULL,
|
|
305
|
+
date TEXT NOT NULL,
|
|
306
|
+
name TEXT NOT NULL,
|
|
307
|
+
UNIQUE (region, date)
|
|
308
|
+
);
|
|
309
|
+
CREATE INDEX idx_holidays_year ON holidays(year, region);
|
|
310
|
+
`
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
version: 5,
|
|
314
|
+
name: "devices: rfid tags and kiosk terminals",
|
|
315
|
+
sql: `
|
|
316
|
+
CREATE TABLE rfid_tags (
|
|
317
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
318
|
+
uid TEXT,
|
|
319
|
+
token_hash TEXT NOT NULL,
|
|
320
|
+
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
321
|
+
label TEXT,
|
|
322
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
323
|
+
expires_at INTEGER,
|
|
324
|
+
last_used_at INTEGER,
|
|
325
|
+
created_at INTEGER NOT NULL
|
|
326
|
+
);
|
|
327
|
+
CREATE UNIQUE INDEX idx_rfid_uid ON rfid_tags(uid) WHERE uid IS NOT NULL;
|
|
328
|
+
|
|
329
|
+
CREATE TABLE kiosk_terminals (
|
|
330
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
331
|
+
name TEXT NOT NULL,
|
|
332
|
+
location TEXT,
|
|
333
|
+
token_hash TEXT NOT NULL,
|
|
334
|
+
pin_required INTEGER NOT NULL DEFAULT 1,
|
|
335
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
336
|
+
expires_at INTEGER,
|
|
337
|
+
last_seen_at INTEGER,
|
|
338
|
+
created_at INTEGER NOT NULL
|
|
339
|
+
);
|
|
340
|
+
`
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
version: 6,
|
|
344
|
+
name: "users: personal PIN for the kiosk terminal",
|
|
345
|
+
sql: `ALTER TABLE users ADD COLUMN pin_hash TEXT;`
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
version: 7,
|
|
349
|
+
name: "kiosk terminals: short lived sessions",
|
|
350
|
+
sql: `
|
|
351
|
+
ALTER TABLE kiosk_terminals ADD COLUMN session_hash TEXT;
|
|
352
|
+
ALTER TABLE kiosk_terminals ADD COLUMN session_expires_at INTEGER;
|
|
353
|
+
`
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
version: 8,
|
|
357
|
+
name: "defaults: time zone Europe/Zurich becomes Europe/Berlin",
|
|
358
|
+
sql: `
|
|
359
|
+
-- The default time zone of the adapter is Europe/Berlin now. Migration 1 is append-only, so a new
|
|
360
|
+
-- database already gets the new default with it; existing installations are adjusted here: every
|
|
361
|
+
-- employee and every instance setting that still carries the old default is moved over. Values that
|
|
362
|
+
-- were chosen deliberately keep their name \u2014 they only change when they are exactly the old default.
|
|
363
|
+
UPDATE users SET timezone = 'Europe/Berlin' WHERE timezone = 'Europe/Zurich';
|
|
364
|
+
UPDATE app_settings SET value = 'Europe/Berlin' WHERE key = 'timezone' AND value = 'Europe/Zurich';
|
|
365
|
+
`
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
version: 9,
|
|
369
|
+
name: "users: picture of the employee",
|
|
370
|
+
sql: `ALTER TABLE users ADD COLUMN avatar TEXT;`
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
version: 10,
|
|
374
|
+
name: "drops the leftovers of the removed data import",
|
|
375
|
+
run: (db) => {
|
|
376
|
+
if (hasColumn(db, "users", "legacy_sha1")) {
|
|
377
|
+
db.exec("ALTER TABLE users DROP COLUMN legacy_sha1");
|
|
378
|
+
}
|
|
379
|
+
if (hasColumn(db, "work_profiles", "legacy_source")) {
|
|
380
|
+
db.exec("ALTER TABLE work_profiles DROP COLUMN legacy_source");
|
|
381
|
+
}
|
|
382
|
+
if (!hasColumn(db, "rfid_tags", "legacy_code")) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
db.exec(`
|
|
386
|
+
CREATE TABLE rfid_tags_new (
|
|
387
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
388
|
+
uid TEXT,
|
|
389
|
+
token_hash TEXT NOT NULL,
|
|
390
|
+
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
391
|
+
label TEXT,
|
|
392
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
393
|
+
expires_at INTEGER,
|
|
394
|
+
last_used_at INTEGER,
|
|
395
|
+
created_at INTEGER NOT NULL
|
|
396
|
+
);
|
|
397
|
+
INSERT INTO rfid_tags_new (id, uid, token_hash, user_id, label, is_active, expires_at, last_used_at, created_at)
|
|
398
|
+
SELECT id, uid, token_hash, user_id, label, is_active, expires_at, last_used_at, created_at
|
|
399
|
+
FROM rfid_tags;
|
|
400
|
+
DROP TABLE rfid_tags;
|
|
401
|
+
ALTER TABLE rfid_tags_new RENAME TO rfid_tags;
|
|
402
|
+
CREATE UNIQUE INDEX idx_rfid_uid ON rfid_tags(uid) WHERE uid IS NOT NULL;
|
|
403
|
+
`);
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
version: 11,
|
|
408
|
+
name: "terminals: the employees of a terminal",
|
|
409
|
+
sql: `
|
|
410
|
+
-- A terminal stands at one place \u2014 an office, a workshop \u2014 and is meant for the employees that work
|
|
411
|
+
-- there. No row for a terminal means "all employees", so terminals that existed before keep working
|
|
412
|
+
-- exactly as they did.
|
|
413
|
+
CREATE TABLE terminal_users (
|
|
414
|
+
terminal_id INTEGER NOT NULL REFERENCES kiosk_terminals(id) ON DELETE CASCADE,
|
|
415
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
416
|
+
created_at INTEGER NOT NULL,
|
|
417
|
+
PRIMARY KEY (terminal_id, user_id)
|
|
418
|
+
);
|
|
419
|
+
CREATE INDEX idx_terminal_users_user ON terminal_users(user_id);
|
|
420
|
+
`
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
version: 12,
|
|
424
|
+
name: "work profiles: paid breaks",
|
|
425
|
+
sql: `ALTER TABLE work_profiles ADD COLUMN pause_paid INTEGER NOT NULL DEFAULT 0;`
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
version: 13,
|
|
429
|
+
name: "work profiles: paid break minutes instead of the paid flag",
|
|
430
|
+
run: (db) => {
|
|
431
|
+
if (!hasColumn(db, "work_profiles", "pause_paid_minutes")) {
|
|
432
|
+
db.exec("ALTER TABLE work_profiles ADD COLUMN pause_paid_minutes INTEGER NOT NULL DEFAULT 0");
|
|
433
|
+
}
|
|
434
|
+
if (hasColumn(db, "work_profiles", "pause_paid")) {
|
|
435
|
+
db.exec("UPDATE work_profiles SET pause_paid_minutes = 1440 WHERE pause_paid = 1");
|
|
436
|
+
db.exec("ALTER TABLE work_profiles DROP COLUMN pause_paid");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
version: 14,
|
|
442
|
+
name: "day aggregates: paid break minutes",
|
|
443
|
+
sql: `ALTER TABLE day_aggregates ADD COLUMN paid_break_min INTEGER NOT NULL DEFAULT 0;`
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
version: 15,
|
|
447
|
+
name: "trigger rules: ioBroker states that punch or set the presence",
|
|
448
|
+
sql: `
|
|
449
|
+
-- A rule watches a state of another adapter (a fingerprint reader, a button, a door contact) and turns a
|
|
450
|
+
-- write on it into a punch or a presence change. Mode "condition" fires when the value matches
|
|
451
|
+
-- "condition" and punches for "user_id"; mode "user" posts the employee the value names. "last_fired_at"
|
|
452
|
+
-- keeps a chatty reader in check together with "cooldown_sec".
|
|
453
|
+
CREATE TABLE trigger_rules (
|
|
454
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
455
|
+
label TEXT,
|
|
456
|
+
source_state TEXT NOT NULL,
|
|
457
|
+
mode TEXT NOT NULL DEFAULT 'condition'
|
|
458
|
+
CHECK (mode IN ('condition','user')),
|
|
459
|
+
condition TEXT,
|
|
460
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
461
|
+
action TEXT NOT NULL DEFAULT 'punch'
|
|
462
|
+
CHECK (action IN ('punch','quickPunch','present','absent')),
|
|
463
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
464
|
+
cooldown_sec INTEGER NOT NULL DEFAULT 0,
|
|
465
|
+
last_fired_at INTEGER,
|
|
466
|
+
created_at INTEGER NOT NULL,
|
|
467
|
+
updated_at INTEGER NOT NULL
|
|
468
|
+
);
|
|
469
|
+
CREATE INDEX idx_trigger_rules_source ON trigger_rules(source_state);
|
|
470
|
+
`
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
version: 16,
|
|
474
|
+
name: "automation rules: what the adapter does on its own (clock out, reminders)",
|
|
475
|
+
sql: `
|
|
476
|
+
-- A rule of the adapter itself, not of a foreign state: at a certain local time (clockOut, missingPunch)
|
|
477
|
+
-- or after a while without a break (breakReminder) the adapter acts. "user_id" is NULL for every
|
|
478
|
+
-- employee, so a company rule and a rule for one person can live side by side.
|
|
479
|
+
CREATE TABLE automation_rules (
|
|
480
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
481
|
+
label TEXT,
|
|
482
|
+
kind TEXT NOT NULL
|
|
483
|
+
CHECK (kind IN ('clockOut','missingPunch','breakReminder')),
|
|
484
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
485
|
+
at_minute INTEGER,
|
|
486
|
+
after_minutes INTEGER,
|
|
487
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
488
|
+
created_at INTEGER NOT NULL,
|
|
489
|
+
updated_at INTEGER NOT NULL
|
|
490
|
+
);
|
|
491
|
+
CREATE INDEX idx_automation_rules_kind ON automation_rules(kind);
|
|
492
|
+
|
|
493
|
+
-- What a rule already did: one row per rule, employee and local date keeps it at "once a day" and doubles
|
|
494
|
+
-- as the log the administration shows. A punch that is already written is never written twice.
|
|
495
|
+
CREATE TABLE automation_runs (
|
|
496
|
+
rule_id INTEGER NOT NULL REFERENCES automation_rules(id) ON DELETE CASCADE,
|
|
497
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
498
|
+
local_date TEXT NOT NULL,
|
|
499
|
+
fired_at INTEGER NOT NULL,
|
|
500
|
+
action TEXT NOT NULL,
|
|
501
|
+
PRIMARY KEY (rule_id, user_id, local_date)
|
|
502
|
+
);
|
|
503
|
+
CREATE INDEX idx_automation_runs_user ON automation_runs(user_id, local_date);
|
|
504
|
+
`
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
version: 17,
|
|
508
|
+
name: "automation rules: weekdays and a weekly instead of a daily guard",
|
|
509
|
+
sql: `
|
|
510
|
+
-- A rule can be limited to certain weekdays: bit 0 is Monday \u2026 bit 6 is Sunday, 127 is every day.
|
|
511
|
+
ALTER TABLE automation_rules ADD COLUMN weekdays INTEGER NOT NULL DEFAULT 127;
|
|
512
|
+
|
|
513
|
+
-- 'day' keeps the guard of one run per employee and date, 'week' allows one run per ISO week.
|
|
514
|
+
ALTER TABLE automation_rules ADD COLUMN repeat TEXT NOT NULL DEFAULT 'day';
|
|
515
|
+
|
|
516
|
+
-- the guard column no longer holds a date only: for a weekly rule it is the ISO week (2026-W38), so the
|
|
517
|
+
-- name says what it keeps. Existing rows stay valid \u2014 a date is the period of a daily rule.
|
|
518
|
+
ALTER TABLE automation_runs RENAME COLUMN local_date TO period;
|
|
519
|
+
`
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
version: 18,
|
|
523
|
+
name: "automation rules: the kind clockIn (punch in somebody who is missing)",
|
|
524
|
+
sql: `
|
|
525
|
+
-- A fourth kind joins the rules: 'clockIn' punches an employee in who is still missing at the configured
|
|
526
|
+
-- time (the decision makes sure nobody is clocked in already). SQLite cannot change a CHECK constraint in
|
|
527
|
+
-- place, so the table is rebuilt \u2014 and because automation_runs belongs to it with ON DELETE CASCADE, the
|
|
528
|
+
-- run log is copied first: dropping the parent would take it with it.
|
|
529
|
+
CREATE TABLE automation_runs_copy (
|
|
530
|
+
rule_id INTEGER NOT NULL,
|
|
531
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
532
|
+
period TEXT NOT NULL,
|
|
533
|
+
fired_at INTEGER NOT NULL,
|
|
534
|
+
action TEXT NOT NULL,
|
|
535
|
+
PRIMARY KEY (rule_id, user_id, period)
|
|
536
|
+
);
|
|
537
|
+
INSERT INTO automation_runs_copy (rule_id, user_id, period, fired_at, action)
|
|
538
|
+
SELECT rule_id, user_id, period, fired_at, action FROM automation_runs;
|
|
539
|
+
|
|
540
|
+
CREATE TABLE automation_rules_new (
|
|
541
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
542
|
+
label TEXT,
|
|
543
|
+
kind TEXT NOT NULL
|
|
544
|
+
CHECK (kind IN ('clockIn','clockOut','missingPunch','breakReminder')),
|
|
545
|
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
546
|
+
at_minute INTEGER,
|
|
547
|
+
after_minutes INTEGER,
|
|
548
|
+
weekdays INTEGER NOT NULL DEFAULT 127,
|
|
549
|
+
repeat TEXT NOT NULL DEFAULT 'day',
|
|
550
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
551
|
+
created_at INTEGER NOT NULL,
|
|
552
|
+
updated_at INTEGER NOT NULL
|
|
553
|
+
);
|
|
554
|
+
INSERT INTO automation_rules_new
|
|
555
|
+
(id, label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, created_at, updated_at)
|
|
556
|
+
SELECT id, label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, created_at, updated_at
|
|
557
|
+
FROM automation_rules;
|
|
558
|
+
|
|
559
|
+
DROP TABLE automation_runs;
|
|
560
|
+
DROP TABLE automation_rules;
|
|
561
|
+
ALTER TABLE automation_rules_new RENAME TO automation_rules;
|
|
562
|
+
-- the runs keep their user reference; the reference to the rule is kept by the repository, which deletes
|
|
563
|
+
-- the runs of a rule together with it
|
|
564
|
+
ALTER TABLE automation_runs_copy RENAME TO automation_runs;
|
|
565
|
+
|
|
566
|
+
CREATE INDEX idx_automation_rules_kind ON automation_rules(kind);
|
|
567
|
+
CREATE INDEX idx_automation_runs_user ON automation_runs(user_id, period);
|
|
568
|
+
`
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
version: 19,
|
|
572
|
+
name: "holidays: a stable key next to the name, so the app can translate the seeded days",
|
|
573
|
+
sql: `
|
|
574
|
+
-- The seeded holidays carry an English name. The key says which day it is, so the web app shows it in the
|
|
575
|
+
-- language of the display; a day somebody added by hand has no key and keeps its own name.
|
|
576
|
+
ALTER TABLE holidays ADD COLUMN key TEXT;
|
|
577
|
+
|
|
578
|
+
-- the days that are already stored get their key from the date
|
|
579
|
+
UPDATE holidays SET key = 'newYear' WHERE key IS NULL AND date LIKE '%-01-01';
|
|
580
|
+
UPDATE holidays SET key = 'epiphany' WHERE key IS NULL AND date LIKE '%-01-06';
|
|
581
|
+
UPDATE holidays SET key = 'labourDay' WHERE key IS NULL AND date LIKE '%-05-01';
|
|
582
|
+
UPDATE holidays SET key = 'nationalDay' WHERE key IS NULL AND (date LIKE '%-08-01' OR date LIKE '%-10-26');
|
|
583
|
+
UPDATE holidays SET key = 'assumption' WHERE key IS NULL AND date LIKE '%-08-15';
|
|
584
|
+
UPDATE holidays SET key = 'germanUnity' WHERE key IS NULL AND date LIKE '%-10-03';
|
|
585
|
+
UPDATE holidays SET key = 'allSaints' WHERE key IS NULL AND date LIKE '%-11-01';
|
|
586
|
+
UPDATE holidays SET key = 'immaculateConception' WHERE key IS NULL AND date LIKE '%-12-08';
|
|
587
|
+
UPDATE holidays SET key = 'christmas' WHERE key IS NULL AND date LIKE '%-12-25';
|
|
588
|
+
UPDATE holidays SET key = 'boxingDay' WHERE key IS NULL AND date LIKE '%-12-26';
|
|
589
|
+
-- the movable feasts carry their English name, which is unique enough to find them again
|
|
590
|
+
UPDATE holidays SET key = 'goodFriday' WHERE key IS NULL AND name = 'Good Friday';
|
|
591
|
+
UPDATE holidays SET key = 'easterMonday' WHERE key IS NULL AND name = 'Easter Monday';
|
|
592
|
+
UPDATE holidays SET key = 'ascension' WHERE key IS NULL AND name = 'Ascension Day';
|
|
593
|
+
UPDATE holidays SET key = 'whitMonday' WHERE key IS NULL AND name = 'Whit Monday';
|
|
594
|
+
UPDATE holidays SET key = 'corpusChristi' WHERE key IS NULL AND name = 'Corpus Christi';
|
|
595
|
+
`
|
|
596
|
+
}
|
|
597
|
+
];
|
|
598
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
599
|
+
0 && (module.exports = {
|
|
600
|
+
hasColumn,
|
|
601
|
+
migrations
|
|
602
|
+
});
|
|
603
|
+
//# sourceMappingURL=migrations.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/db/migrations.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Versioned database migrations.\n *\n * The schema follows the internal specification (section 2). Rules:\n * - Times are stored as UTC epoch seconds (INTEGER); durations as whole minutes.\n * - Local date/time fields (`ts_local`, `local_date`) are derived caches of `ts_utc` + user time zone.\n * - Migrations are never *reinterpreted*: a released migration keeps its effect for databases that already\n * ran it, and every schema change comes as a new migration. Definitions that only freshly created\n * databases ever see may be tidied up afterwards, as long as fresh and upgraded installations end up with\n * the same schema (see migration 10, which cleans up behind migrations 1, 2 and 5).\n */\n\nimport type { Db } from \"./database\";\n\n/**\n * A single schema migration: either plain SQL or a JavaScript step for changes that SQL cannot express\n * (e.g. \u201Cdrop this column, but only when it is still there\u201D).\n */\nexport interface Migration {\n\t/** Monotonically increasing version number, applied in ascending order */\n\tversion: number;\n\t/** Short description, stored in `schema_migrations` for traceability */\n\tname: string;\n\t/** SQL statements (may contain several statements separated by semicolons) */\n\tsql?: string;\n\t/** JavaScript steps, run inside the same transaction as `sql` */\n\trun?: (db: Db) => void;\n}\n\n/**\n * Checks whether a table has a column.\n *\n * Migrations that clean up after older versions use this to stay safe for both kinds of database: one that was\n * created by an older version and one that was created by the current one (which never creates the column).\n *\n * @param db - open database handle\n * @param table - table name\n * @param column - column name\n * @returns true when the column exists\n */\nexport function hasColumn(db: Db, table: string, column: string): boolean {\n\tconst rows = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];\n\treturn rows.some(row => row.name === column);\n}\n\nexport const migrations: Migration[] = [\n\t{\n\t\tversion: 1,\n\t\tname: \"core: users, roles, permissions, work profiles, shift and pause rules, settings, sessions, audit, import runs\",\n\t\tsql: `\n\t\t\tCREATE TABLE users (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tlogin TEXT NOT NULL UNIQUE,\n\t\t\t\tpassword_hash TEXT NOT NULL DEFAULT '',\n\t\t\t\tdisplay_name TEXT NOT NULL,\n\t\t\t\temail TEXT,\n\t\t\t\trfid_card TEXT,\n\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1,\n\t\t\t\tmust_change_pw INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tlocale TEXT NOT NULL DEFAULT 'de-DE',\n\t\t\t\ttimezone TEXT NOT NULL DEFAULT 'Europe/Berlin',\n\t\t\t\tcreated_at INTEGER NOT NULL,\n\t\t\t\tupdated_at INTEGER NOT NULL,\n\t\t\t\tlast_login_at INTEGER\n\t\t\t);\n\n\t\t\tCREATE TABLE roles (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tkey TEXT NOT NULL UNIQUE,\n\t\t\t\tname TEXT NOT NULL\n\t\t\t);\n\n\t\t\tCREATE TABLE permissions (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tkey TEXT NOT NULL UNIQUE\n\t\t\t);\n\n\t\t\tCREATE TABLE role_permissions (\n\t\t\t\trole_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,\n\t\t\t\tpermission_id INTEGER NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,\n\t\t\t\tPRIMARY KEY (role_id, permission_id)\n\t\t\t);\n\n\t\t\tCREATE TABLE user_roles (\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\trole_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,\n\t\t\t\tPRIMARY KEY (user_id, role_id)\n\t\t\t);\n\n\t\t\tCREATE TABLE work_profiles (\n\t\t\t\tuser_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tpercent REAL NOT NULL DEFAULT 100,\n\t\t\t\tweekly_hours REAL NOT NULL DEFAULT 42.5,\n\t\t\t\tworkdays TEXT NOT NULL DEFAULT '0;1;1;1;1;1;0',\n\t\t\t\tstart_date INTEGER,\n\t\t\t\tend_date INTEGER,\n\t\t\t\tovertime_carryover INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tvorholzeit_per_year INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tvacation_carryover REAL NOT NULL DEFAULT 0,\n\t\t\t\tvacation_per_year REAL NOT NULL DEFAULT 0,\n\t\t\t\tovertime_model TEXT NOT NULL DEFAULT 'monthly'\n\t\t\t\t CHECK (overtime_model IN ('cumulative','yearly','monthly')),\n\t\t\t\tholiday_flags TEXT\n\t\t\t);\n\n\t\t\tCREATE TABLE shift_rules (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tuser_id INTEGER REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tday_of_week INTEGER CHECK (day_of_week IS NULL OR day_of_week BETWEEN 0 AND 6),\n\t\t\t\tfrom_min INTEGER,\n\t\t\t\tto_min INTEGER,\n\t\t\t\tsurcharge REAL NOT NULL DEFAULT 0,\n\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1\n\t\t\t);\n\n\t\t\tCREATE TABLE pause_rules (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tuser_id INTEGER REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tfrom_min INTEGER NOT NULL,\n\t\t\t\tto_min INTEGER,\n\t\t\t\tpause_min INTEGER NOT NULL,\n\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1,\n\t\t\t\tCHECK (to_min IS NULL OR to_min > from_min)\n\t\t\t);\n\n\t\t\tCREATE TABLE app_settings (\n\t\t\t\tkey TEXT PRIMARY KEY,\n\t\t\t\tvalue TEXT NOT NULL,\n\t\t\t\tupdated_at INTEGER NOT NULL\n\t\t\t);\n\n\t\t\tCREATE TABLE sessions (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tcreated_at INTEGER NOT NULL,\n\t\t\t\texpires_at INTEGER NOT NULL,\n\t\t\t\tuser_agent TEXT,\n\t\t\t\tip TEXT,\n\t\t\t\trevoked INTEGER NOT NULL DEFAULT 0\n\t\t\t);\n\n\t\t\tCREATE TABLE audit_log (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tat_utc INTEGER NOT NULL,\n\t\t\t\tactor_id INTEGER,\n\t\t\t\taction TEXT NOT NULL,\n\t\t\t\tentity TEXT,\n\t\t\t\tentity_id TEXT,\n\t\t\t\tdetail TEXT,\n\t\t\t\tip TEXT\n\t\t\t);\n\n\t\t\tCREATE TABLE import_runs (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tsource TEXT NOT NULL,\n\t\t\t\tsource_path TEXT NOT NULL,\n\t\t\t\tstarted_at INTEGER NOT NULL,\n\t\t\t\tfinished_at INTEGER,\n\t\t\t\tmode TEXT NOT NULL DEFAULT 'dry-run'\n\t\t\t\t CHECK (mode IN ('dry-run','commit')),\n\t\t\t\tstatus TEXT NOT NULL DEFAULT 'running'\n\t\t\t\t CHECK (status IN ('running','ok','warnings','mismatch','failed')),\n\t\t\t\ttimezone_assumed TEXT,\n\t\t\t\tstats TEXT,\n\t\t\t\twarnings TEXT,\n\t\t\t\tactor_id INTEGER REFERENCES users(id)\n\t\t\t);\n\t\t`,\n\t},\n\t{\n\t\tversion: 2,\n\t\tname: \"time: entries, audit, day/month/year aggregates, payouts\",\n\t\tsql: `\n\t\t\tCREATE TABLE time_entries (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tts_utc INTEGER NOT NULL,\n\t\t\t\tclient_ts_utc INTEGER,\n\t\t\t\tts_local INTEGER NOT NULL,\n\t\t\t\tlocal_date TEXT NOT NULL,\n\t\t\t\tdirection TEXT NOT NULL DEFAULT 'auto'\n\t\t\t\t CHECK (direction IN ('in','out','auto')),\n\t\t\t\tsource TEXT NOT NULL DEFAULT 'web'\n\t\t\t\t CHECK (source IN ('web','nfc','terminal','api','admin','import')),\n\t\t\t\tidempotency_key TEXT,\n\t\t\t\tsync_state TEXT NOT NULL DEFAULT 'synced'\n\t\t\t\t CHECK (sync_state IN ('synced','pending','conflict')),\n\t\t\t\trevision INTEGER NOT NULL DEFAULT 1,\n\t\t\t\tnote TEXT,\n\t\t\t\tcreated_by INTEGER REFERENCES users(id),\n\t\t\t\tcreated_at INTEGER NOT NULL,\n\t\t\t\tupdated_at INTEGER NOT NULL,\n\t\t\t\tupdated_by INTEGER REFERENCES users(id)\n\t\t\t);\n\n\t\t\tCREATE INDEX idx_entries_user_date ON time_entries(user_id, local_date);\n\t\t\tCREATE INDEX idx_entries_user_ts ON time_entries(user_id, ts_utc);\n\t\t\tCREATE UNIQUE INDEX idx_entries_idem\n\t\t\t\tON time_entries(user_id, idempotency_key) WHERE idempotency_key IS NOT NULL;\n\n\t\t\tCREATE TABLE time_entry_audit (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tentry_id INTEGER,\n\t\t\t\tuser_id INTEGER NOT NULL,\n\t\t\t\taction TEXT NOT NULL,\n\t\t\t\tchanges TEXT,\n\t\t\t\told_ts_utc INTEGER,\n\t\t\t\tnew_ts_utc INTEGER,\n\t\t\t\trevision INTEGER,\n\t\t\t\treason TEXT,\n\t\t\t\tactor_id INTEGER NOT NULL,\n\t\t\t\tactor_ip TEXT,\n\t\t\t\tat_utc INTEGER NOT NULL\n\t\t\t);\n\t\t\tCREATE INDEX idx_entry_audit_entry ON time_entry_audit(entry_id);\n\t\t`,\n\t},\n\t{\n\t\tversion: 3,\n\t\tname: \"aggregates: day, month, year and payouts\",\n\t\tsql: `\n\t\t\tCREATE TABLE day_aggregates (\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tlocal_date TEXT NOT NULL,\n\t\t\t\tworked_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tbreak_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\ttarget_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tbalance_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tabsence_code TEXT,\n\t\t\t\tis_holiday INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tfirst_in_utc INTEGER,\n\t\t\t\tlast_out_utc INTEGER,\n\t\t\t\thas_open_entry INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tupdated_at INTEGER NOT NULL,\n\t\t\t\tPRIMARY KEY (user_id, local_date)\n\t\t\t);\n\n\t\t\tCREATE TABLE month_aggregates (\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tyear INTEGER NOT NULL,\n\t\t\t\tmonth INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12),\n\t\t\t\tworked_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\ttarget_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tbalance_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tovertime_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tvacation_used REAL NOT NULL DEFAULT 0,\n\t\t\t\tupdated_at INTEGER NOT NULL,\n\t\t\t\tPRIMARY KEY (user_id, year, month)\n\t\t\t);\n\n\t\t\tCREATE TABLE year_aggregates (\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tyear INTEGER NOT NULL,\n\t\t\t\tworked_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\ttarget_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tovertime_min INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tovertime_start INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tvacation_days REAL NOT NULL DEFAULT 0,\n\t\t\t\tvacation_used REAL NOT NULL DEFAULT 0,\n\t\t\t\tvacation_left REAL NOT NULL DEFAULT 0,\n\t\t\t\tvacation_planned_days REAL NOT NULL DEFAULT 0,\n\t\t\t\tsettled INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tupdated_at INTEGER NOT NULL,\n\t\t\t\tPRIMARY KEY (user_id, year)\n\t\t\t);\n\n\t\t\tCREATE TABLE payouts (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tyear INTEGER NOT NULL,\n\t\t\t\tmonth INTEGER,\n\t\t\t\tminutes INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tamount REAL,\n\t\t\t\tnote TEXT,\n\t\t\t\tcreated_at INTEGER NOT NULL,\n\t\t\t\tcreated_by INTEGER REFERENCES users(id)\n\t\t\t);\n\t\t\tCREATE INDEX idx_payouts_user ON payouts(user_id, year, month);\n\t\t`,\n\t},\n\t{\n\t\tversion: 4,\n\t\tname: \"absence: types, absences, holidays\",\n\t\tsql: `\n\t\t\tCREATE TABLE absence_types (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tuser_id INTEGER REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tcode TEXT NOT NULL,\n\t\t\t\tname TEXT NOT NULL,\n\t\t\t\tpaid INTEGER NOT NULL DEFAULT 1,\n\t\t\t\tfactor REAL NOT NULL DEFAULT 100,\n\t\t\t\treduce_vacation INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1\n\t\t\t);\n\n\t\t\tCREATE UNIQUE INDEX idx_absence_types_global\n\t\t\t\tON absence_types(code) WHERE user_id IS NULL;\n\t\t\tCREATE UNIQUE INDEX idx_absence_types_user\n\t\t\t\tON absence_types(user_id, code) WHERE user_id IS NOT NULL;\n\n\t\t\tCREATE TABLE absences (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\ttype_id INTEGER NOT NULL REFERENCES absence_types(id),\n\t\t\t\tdate_from TEXT NOT NULL,\n\t\t\t\tdate_to TEXT NOT NULL,\n\t\t\t\tday_portion REAL NOT NULL DEFAULT 1 CHECK (day_portion > 0 AND day_portion <= 1),\n\t\t\t\thours REAL,\n\t\t\t\tstatus TEXT NOT NULL DEFAULT 'taken'\n\t\t\t\t CHECK (status IN ('taken','planned')),\n\t\t\t\tnote TEXT,\n\t\t\t\tcreated_at INTEGER NOT NULL,\n\t\t\t\tcreated_by INTEGER REFERENCES users(id),\n\t\t\t\tCHECK (date_from <= date_to)\n\t\t\t);\n\t\t\tCREATE INDEX idx_absences_user_dates ON absences(user_id, date_from, date_to);\n\n\t\t\tCREATE TABLE holidays (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tregion TEXT NOT NULL DEFAULT 'global',\n\t\t\t\tyear INTEGER NOT NULL,\n\t\t\t\tdate TEXT NOT NULL,\n\t\t\t\tname TEXT NOT NULL,\n\t\t\t\tUNIQUE (region, date)\n\t\t\t);\n\t\t\tCREATE INDEX idx_holidays_year ON holidays(year, region);\n\t\t`,\n\t},\n\t{\n\t\tversion: 5,\n\t\tname: \"devices: rfid tags and kiosk terminals\",\n\t\tsql: `\n\t\t\tCREATE TABLE rfid_tags (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tuid TEXT,\n\t\t\t\ttoken_hash TEXT NOT NULL,\n\t\t\t\tuser_id INTEGER REFERENCES users(id) ON DELETE SET NULL,\n\t\t\t\tlabel TEXT,\n\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1,\n\t\t\t\texpires_at INTEGER,\n\t\t\t\tlast_used_at INTEGER,\n\t\t\t\tcreated_at INTEGER NOT NULL\n\t\t\t);\n\t\t\tCREATE UNIQUE INDEX idx_rfid_uid ON rfid_tags(uid) WHERE uid IS NOT NULL;\n\n\t\t\tCREATE TABLE kiosk_terminals (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tname TEXT NOT NULL,\n\t\t\t\tlocation TEXT,\n\t\t\t\ttoken_hash TEXT NOT NULL,\n\t\t\t\tpin_required INTEGER NOT NULL DEFAULT 1,\n\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1,\n\t\t\t\texpires_at INTEGER,\n\t\t\t\tlast_seen_at INTEGER,\n\t\t\t\tcreated_at INTEGER NOT NULL\n\t\t\t);\n\t\t`,\n\t},\n\t{\n\t\tversion: 6,\n\t\tname: \"users: personal PIN for the kiosk terminal\",\n\t\tsql: `ALTER TABLE users ADD COLUMN pin_hash TEXT;`,\n\t},\n\t{\n\t\tversion: 7,\n\t\tname: \"kiosk terminals: short lived sessions\",\n\t\tsql: `\n\t\t\tALTER TABLE kiosk_terminals ADD COLUMN session_hash TEXT;\n\t\t\tALTER TABLE kiosk_terminals ADD COLUMN session_expires_at INTEGER;\n\t\t`,\n\t},\n\t{\n\t\tversion: 8,\n\t\tname: \"defaults: time zone Europe/Zurich becomes Europe/Berlin\",\n\t\tsql: `\n\t\t\t-- The default time zone of the adapter is Europe/Berlin now. Migration 1 is append-only, so a new\n\t\t\t-- database already gets the new default with it; existing installations are adjusted here: every\n\t\t\t-- employee and every instance setting that still carries the old default is moved over. Values that\n\t\t\t-- were chosen deliberately keep their name \u2014 they only change when they are exactly the old default.\n\t\t\tUPDATE users SET timezone = 'Europe/Berlin' WHERE timezone = 'Europe/Zurich';\n\t\t\tUPDATE app_settings SET value = 'Europe/Berlin' WHERE key = 'timezone' AND value = 'Europe/Zurich';\n\t\t`,\n\t},\n\t{\n\t\tversion: 9,\n\t\tname: \"users: picture of the employee\",\n\t\tsql: `ALTER TABLE users ADD COLUMN avatar TEXT;`,\n\t},\n\t{\n\t\tversion: 10,\n\t\tname: \"drops the leftovers of the removed data import\",\n\t\trun: (db: Db): void => {\n\t\t\t// The adapter does not read data of another time tracking system any more, so the columns that existed\n\t\t\t// for it are dropped. Fresh databases never create them (migrations 1, 2 and 5 are clean), so every\n\t\t\t// step asks first: an upgraded database loses the columns, a fresh one has nothing to do.\n\t\t\tif (hasColumn(db, \"users\", \"legacy_sha1\")) {\n\t\t\t\tdb.exec(\"ALTER TABLE users DROP COLUMN legacy_sha1\");\n\t\t\t}\n\t\t\tif (hasColumn(db, \"work_profiles\", \"legacy_source\")) {\n\t\t\t\tdb.exec(\"ALTER TABLE work_profiles DROP COLUMN legacy_source\");\n\t\t\t}\n\n\t\t\t// rfid_tags carried the printed card number of that system as a second identifier. SQLite cannot drop\n\t\t\t// a column that a CHECK constraint mentions, so the table is rebuilt without it; existing rows keep\n\t\t\t// their id, their uid and their token hash, and the unique index on the uid is recreated.\n\t\t\tif (!hasColumn(db, \"rfid_tags\", \"legacy_code\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdb.exec(`\n\t\t\t\tCREATE TABLE rfid_tags_new (\n\t\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\t\tuid TEXT,\n\t\t\t\t\ttoken_hash TEXT NOT NULL,\n\t\t\t\t\tuser_id INTEGER REFERENCES users(id) ON DELETE SET NULL,\n\t\t\t\t\tlabel TEXT,\n\t\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1,\n\t\t\t\t\texpires_at INTEGER,\n\t\t\t\t\tlast_used_at INTEGER,\n\t\t\t\t\tcreated_at INTEGER NOT NULL\n\t\t\t\t);\n\t\t\t\tINSERT INTO rfid_tags_new (id, uid, token_hash, user_id, label, is_active, expires_at, last_used_at, created_at)\n\t\t\t\t\tSELECT id, uid, token_hash, user_id, label, is_active, expires_at, last_used_at, created_at\n\t\t\t\t\tFROM rfid_tags;\n\t\t\t\tDROP TABLE rfid_tags;\n\t\t\t\tALTER TABLE rfid_tags_new RENAME TO rfid_tags;\n\t\t\t\tCREATE UNIQUE INDEX idx_rfid_uid ON rfid_tags(uid) WHERE uid IS NOT NULL;\n\t\t\t`);\n\t\t},\n\t},\n\t{\n\t\tversion: 11,\n\t\tname: \"terminals: the employees of a terminal\",\n\t\tsql: `\n\t\t\t-- A terminal stands at one place \u2014 an office, a workshop \u2014 and is meant for the employees that work\n\t\t\t-- there. No row for a terminal means \"all employees\", so terminals that existed before keep working\n\t\t\t-- exactly as they did.\n\t\t\tCREATE TABLE terminal_users (\n\t\t\t\tterminal_id INTEGER NOT NULL REFERENCES kiosk_terminals(id) ON DELETE CASCADE,\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tcreated_at INTEGER NOT NULL,\n\t\t\t\tPRIMARY KEY (terminal_id, user_id)\n\t\t\t);\n\t\t\tCREATE INDEX idx_terminal_users_user ON terminal_users(user_id);\n\t\t`,\n\t},\n\t{\n\t\tversion: 12,\n\t\tname: \"work profiles: paid breaks\",\n\t\tsql: `ALTER TABLE work_profiles ADD COLUMN pause_paid INTEGER NOT NULL DEFAULT 0;`,\n\t},\n\t{\n\t\tversion: 13,\n\t\tname: \"work profiles: paid break minutes instead of the paid flag\",\n\t\trun: (db: Db): void => {\n\t\t\t// The flag became an amount: a company may pay a quarter of an hour and nothing more. A profile that had\n\t\t\t// the flag set keeps a fully paid break (a day has 1440 minutes), a fresh profile pays nothing at all.\n\t\t\tif (!hasColumn(db, \"work_profiles\", \"pause_paid_minutes\")) {\n\t\t\t\tdb.exec(\"ALTER TABLE work_profiles ADD COLUMN pause_paid_minutes INTEGER NOT NULL DEFAULT 0\");\n\t\t\t}\n\t\t\tif (hasColumn(db, \"work_profiles\", \"pause_paid\")) {\n\t\t\t\tdb.exec(\"UPDATE work_profiles SET pause_paid_minutes = 1440 WHERE pause_paid = 1\");\n\t\t\t\tdb.exec(\"ALTER TABLE work_profiles DROP COLUMN pause_paid\");\n\t\t\t}\n\t\t},\n\t},\n\t{\n\t\tversion: 14,\n\t\tname: \"day aggregates: paid break minutes\",\n\t\tsql: `ALTER TABLE day_aggregates ADD COLUMN paid_break_min INTEGER NOT NULL DEFAULT 0;`,\n\t},\n\t{\n\t\tversion: 15,\n\t\tname: \"trigger rules: ioBroker states that punch or set the presence\",\n\t\tsql: `\n\t\t\t-- A rule watches a state of another adapter (a fingerprint reader, a button, a door contact) and turns a\n\t\t\t-- write on it into a punch or a presence change. Mode \"condition\" fires when the value matches\n\t\t\t-- \"condition\" and punches for \"user_id\"; mode \"user\" posts the employee the value names. \"last_fired_at\"\n\t\t\t-- keeps a chatty reader in check together with \"cooldown_sec\".\n\t\t\tCREATE TABLE trigger_rules (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tlabel TEXT,\n\t\t\t\tsource_state TEXT NOT NULL,\n\t\t\t\tmode TEXT NOT NULL DEFAULT 'condition'\n\t\t\t\t\tCHECK (mode IN ('condition','user')),\n\t\t\t\tcondition TEXT,\n\t\t\t\tuser_id INTEGER REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\taction TEXT NOT NULL DEFAULT 'punch'\n\t\t\t\t\tCHECK (action IN ('punch','quickPunch','present','absent')),\n\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1,\n\t\t\t\tcooldown_sec INTEGER NOT NULL DEFAULT 0,\n\t\t\t\tlast_fired_at INTEGER,\n\t\t\t\tcreated_at INTEGER NOT NULL,\n\t\t\t\tupdated_at INTEGER NOT NULL\n\t\t\t);\n\t\t\tCREATE INDEX idx_trigger_rules_source ON trigger_rules(source_state);\n\t\t`,\n\t},\n\t{\n\t\tversion: 16,\n\t\tname: \"automation rules: what the adapter does on its own (clock out, reminders)\",\n\t\tsql: `\n\t\t\t-- A rule of the adapter itself, not of a foreign state: at a certain local time (clockOut, missingPunch)\n\t\t\t-- or after a while without a break (breakReminder) the adapter acts. \"user_id\" is NULL for every\n\t\t\t-- employee, so a company rule and a rule for one person can live side by side.\n\t\t\tCREATE TABLE automation_rules (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tlabel TEXT,\n\t\t\t\tkind TEXT NOT NULL\n\t\t\t\t\tCHECK (kind IN ('clockOut','missingPunch','breakReminder')),\n\t\t\t\tuser_id INTEGER REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tat_minute INTEGER,\n\t\t\t\tafter_minutes INTEGER,\n\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1,\n\t\t\t\tcreated_at INTEGER NOT NULL,\n\t\t\t\tupdated_at INTEGER NOT NULL\n\t\t\t);\n\t\t\tCREATE INDEX idx_automation_rules_kind ON automation_rules(kind);\n\n\t\t\t-- What a rule already did: one row per rule, employee and local date keeps it at \"once a day\" and doubles\n\t\t\t-- as the log the administration shows. A punch that is already written is never written twice.\n\t\t\tCREATE TABLE automation_runs (\n\t\t\t\trule_id INTEGER NOT NULL REFERENCES automation_rules(id) ON DELETE CASCADE,\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tlocal_date TEXT NOT NULL,\n\t\t\t\tfired_at INTEGER NOT NULL,\n\t\t\t\taction TEXT NOT NULL,\n\t\t\t\tPRIMARY KEY (rule_id, user_id, local_date)\n\t\t\t);\n\t\t\tCREATE INDEX idx_automation_runs_user ON automation_runs(user_id, local_date);\n\t\t`,\n\t},\n\t{\n\t\tversion: 17,\n\t\tname: \"automation rules: weekdays and a weekly instead of a daily guard\",\n\t\tsql: `\n\t\t\t-- A rule can be limited to certain weekdays: bit 0 is Monday \u2026 bit 6 is Sunday, 127 is every day.\n\t\t\tALTER TABLE automation_rules ADD COLUMN weekdays INTEGER NOT NULL DEFAULT 127;\n\n\t\t\t-- 'day' keeps the guard of one run per employee and date, 'week' allows one run per ISO week.\n\t\t\tALTER TABLE automation_rules ADD COLUMN repeat TEXT NOT NULL DEFAULT 'day';\n\n\t\t\t-- the guard column no longer holds a date only: for a weekly rule it is the ISO week (2026-W38), so the\n\t\t\t-- name says what it keeps. Existing rows stay valid \u2014 a date is the period of a daily rule.\n\t\t\tALTER TABLE automation_runs RENAME COLUMN local_date TO period;\n\t\t`,\n\t},\n\t{\n\t\tversion: 18,\n\t\tname: \"automation rules: the kind clockIn (punch in somebody who is missing)\",\n\t\tsql: `\n\t\t\t-- A fourth kind joins the rules: 'clockIn' punches an employee in who is still missing at the configured\n\t\t\t-- time (the decision makes sure nobody is clocked in already). SQLite cannot change a CHECK constraint in\n\t\t\t-- place, so the table is rebuilt \u2014 and because automation_runs belongs to it with ON DELETE CASCADE, the\n\t\t\t-- run log is copied first: dropping the parent would take it with it.\n\t\t\tCREATE TABLE automation_runs_copy (\n\t\t\t\trule_id INTEGER NOT NULL,\n\t\t\t\tuser_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tperiod TEXT NOT NULL,\n\t\t\t\tfired_at INTEGER NOT NULL,\n\t\t\t\taction TEXT NOT NULL,\n\t\t\t\tPRIMARY KEY (rule_id, user_id, period)\n\t\t\t);\n\t\t\tINSERT INTO automation_runs_copy (rule_id, user_id, period, fired_at, action)\n\t\t\t\tSELECT rule_id, user_id, period, fired_at, action FROM automation_runs;\n\n\t\t\tCREATE TABLE automation_rules_new (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tlabel TEXT,\n\t\t\t\tkind TEXT NOT NULL\n\t\t\t\t\tCHECK (kind IN ('clockIn','clockOut','missingPunch','breakReminder')),\n\t\t\t\tuser_id INTEGER REFERENCES users(id) ON DELETE CASCADE,\n\t\t\t\tat_minute INTEGER,\n\t\t\t\tafter_minutes INTEGER,\n\t\t\t\tweekdays INTEGER NOT NULL DEFAULT 127,\n\t\t\t\trepeat TEXT NOT NULL DEFAULT 'day',\n\t\t\t\tis_active INTEGER NOT NULL DEFAULT 1,\n\t\t\t\tcreated_at INTEGER NOT NULL,\n\t\t\t\tupdated_at INTEGER NOT NULL\n\t\t\t);\n\t\t\tINSERT INTO automation_rules_new\n\t\t\t\t(id, label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, created_at, updated_at)\n\t\t\t\tSELECT id, label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, created_at, updated_at\n\t\t\t\tFROM automation_rules;\n\n\t\t\tDROP TABLE automation_runs;\n\t\t\tDROP TABLE automation_rules;\n\t\t\tALTER TABLE automation_rules_new RENAME TO automation_rules;\n\t\t\t-- the runs keep their user reference; the reference to the rule is kept by the repository, which deletes\n\t\t\t-- the runs of a rule together with it\n\t\t\tALTER TABLE automation_runs_copy RENAME TO automation_runs;\n\n\t\t\tCREATE INDEX idx_automation_rules_kind ON automation_rules(kind);\n\t\t\tCREATE INDEX idx_automation_runs_user ON automation_runs(user_id, period);\n\t\t`,\n\t},\n\t{\n\t\tversion: 19,\n\t\tname: \"holidays: a stable key next to the name, so the app can translate the seeded days\",\n\t\tsql: `\n\t\t\t-- The seeded holidays carry an English name. The key says which day it is, so the web app shows it in the\n\t\t\t-- language of the display; a day somebody added by hand has no key and keeps its own name.\n\t\t\tALTER TABLE holidays ADD COLUMN key TEXT;\n\n\t\t\t-- the days that are already stored get their key from the date\n\t\t\tUPDATE holidays SET key = 'newYear' WHERE key IS NULL AND date LIKE '%-01-01';\n\t\t\tUPDATE holidays SET key = 'epiphany' WHERE key IS NULL AND date LIKE '%-01-06';\n\t\t\tUPDATE holidays SET key = 'labourDay' WHERE key IS NULL AND date LIKE '%-05-01';\n\t\t\tUPDATE holidays SET key = 'nationalDay' WHERE key IS NULL AND (date LIKE '%-08-01' OR date LIKE '%-10-26');\n\t\t\tUPDATE holidays SET key = 'assumption' WHERE key IS NULL AND date LIKE '%-08-15';\n\t\t\tUPDATE holidays SET key = 'germanUnity' WHERE key IS NULL AND date LIKE '%-10-03';\n\t\t\tUPDATE holidays SET key = 'allSaints' WHERE key IS NULL AND date LIKE '%-11-01';\n\t\t\tUPDATE holidays SET key = 'immaculateConception' WHERE key IS NULL AND date LIKE '%-12-08';\n\t\t\tUPDATE holidays SET key = 'christmas' WHERE key IS NULL AND date LIKE '%-12-25';\n\t\t\tUPDATE holidays SET key = 'boxingDay' WHERE key IS NULL AND date LIKE '%-12-26';\n\t\t\t-- the movable feasts carry their English name, which is unique enough to find them again\n\t\t\tUPDATE holidays SET key = 'goodFriday' WHERE key IS NULL AND name = 'Good Friday';\n\t\t\tUPDATE holidays SET key = 'easterMonday' WHERE key IS NULL AND name = 'Easter Monday';\n\t\t\tUPDATE holidays SET key = 'ascension' WHERE key IS NULL AND name = 'Ascension Day';\n\t\t\tUPDATE holidays SET key = 'whitMonday' WHERE key IS NULL AND name = 'Whit Monday';\n\t\t\tUPDATE holidays SET key = 'corpusChristi' WHERE key IS NULL AND name = 'Corpus Christi';\n\t\t`,\n\t},\n];\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|