iobroker.time-tracker 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -351,6 +351,22 @@ local SQLite file, access is role-based, and every correction is written to an a
351
351
 
352
352
  ### **WORK IN PROGRESS**
353
353
 
354
+ ### 0.4.3 (2026-09-22)
355
+
356
+ - (Alex) change: “active” of an absence type now reads “**visible for everybody**” and means exactly that. Switched on,
357
+ the employees pick the type in the app and request it (vacation, further training); switched off, the type belongs to
358
+ the administration alone (sickness, accident, military service — such a note reaches a company on the same day and is
359
+ booked, not requested). The server refuses an employee a type that is not public (`403`), the administration sees
360
+ every type in its own lists, and sickness, accident and military service start switched off (migration 22)
361
+
362
+ ### 0.4.2 (2026-09-22)
363
+
364
+ - (Alex) fix: the roles appear in the language of the display (the database keeps “Administrator”, “Manager” and
365
+ “Employee”, the app translates them now), the two date fields of “enter an absence” no longer overlap with their
366
+ label, an absence type can be removed again as long as no absence uses it (new `DELETE /absence-types/:id` with a
367
+ confirmation that names the type), and an inactive type stays in the lists of the administration while the employees
368
+ no longer see it in their picker (`GET /absence-types?includeInactive=true`)
369
+
354
370
  ### 0.4.1 (2026-09-22)
355
371
 
356
372
  - (Alex) docs: the installation chapter no longer carries the build steps for work on the sources — they live in
@@ -370,22 +386,6 @@ local SQLite file, access is role-based, and every correction is written to an a
370
386
  code, name, paid, factor and the vacation deduction. The type that uses up the vacation allowance is marked in the
371
387
  lists and in the picker, so “F – Ferien” reads as vacation at one glance
372
388
 
373
- ### 0.3.3 (2026-09-22)
374
-
375
- - (Alex) fix: the installation chapter no longer points to the dialog that installs an adapter from a URL — the
376
- repository checker reports that as `E6013` (“suggests to install the adapter directly from GitHub, directly from npm
377
- or using npm commands”). It now leads with the normal way through the adapter list of the ioBroker admin, names the
378
- registry for a machine without the admin and keeps the build steps in a clearly marked note for work on the sources.
379
- The troubleshooting row for a missing web app no longer carries npm commands either
380
-
381
- ### 0.3.2 (2026-09-22)
382
-
383
- - (Alex) fix: the automation rules are a **list** now — one row per rule with its caption, kind, time, target and
384
- weekdays, plus the buttons for *active*, *edit* and *delete* on the right. The form only opens in a dialog, for a
385
- new or an edited rule, so the list stays readable. The save button of the card says “**Regeln speichern**” (it
386
- carried the caption of the break rules before). New texts in all 11 languages, and an end-to-end test keeps both
387
- findings
388
-
389
389
  Older entries are kept in [`CHANGELOG_OLD.md`](CHANGELOG_OLD.md).
390
390
 
391
391
  ## Provenance
@@ -623,6 +623,18 @@ const migrations = [
623
623
  }
624
624
  db.exec("CREATE INDEX IF NOT EXISTS idx_absences_approval ON absences(approval, date_from)");
625
625
  }
626
+ },
627
+ {
628
+ version: 22,
629
+ name: "absence types: sickness, accident and military are for the administration only",
630
+ sql: `
631
+ -- A sickness note usually reaches a company on the same day, so nobody \u201Crequests\u201D it: the administration
632
+ -- books it. \u201CVisible for everybody\u201D (is_active) therefore starts switched off for these three types, so an
633
+ -- employee does not find them in the app. The administration can switch it on again in the card of the
634
+ -- absence types at any time.
635
+ UPDATE absence_types SET is_active = 0
636
+ WHERE user_id IS NULL AND code IN ('K','U','M') AND is_active = 1;
637
+ `
626
638
  }
627
639
  ];
628
640
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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\t{\n\t\tversion: 20,\n\t\tname: \"work profiles: whether the presence card shows the working time of the day\",\n\t\tsql: `\n\t\t\t-- The presence screen is visible before the PIN is entered, so showing the worked time of an employee\n\t\t\t-- is a decision of the administration, per employee. Off by default.\n\t\t\tALTER TABLE work_profiles ADD COLUMN show_worked_time INTEGER NOT NULL DEFAULT 0;\n\t\t`,\n\t},\n\t{\n\t\tversion: 21,\n\t\tname: \"absences: approval by the administration (requested, approved, rejected)\",\n\t\trun: (db: Db): void => {\n\t\t\t// An employee requests, the administration decides. `approval` is separate from `status`, which keeps\n\t\t\t// saying whether the days are planned or already taken: a request can be planned and still wait for\n\t\t\t// its decision. Everything that exists before this migration counts as approved, so no figure changes\n\t\t\t// for a running installation; a request of an employee starts as `requested`.\n\t\t\tif (!hasColumn(db, \"absences\", \"approval\")) {\n\t\t\t\tdb.exec(\n\t\t\t\t\t\"ALTER TABLE absences ADD COLUMN approval TEXT NOT NULL DEFAULT 'approved' \" +\n\t\t\t\t\t\t\"CHECK (approval IN ('requested','approved','rejected'))\",\n\t\t\t\t);\n\t\t\t}\n\t\t\t// Who decided when and why \u2014 a rejection without a reason is not much use to the employee.\n\t\t\tif (!hasColumn(db, \"absences\", \"decided_at\")) {\n\t\t\t\tdb.exec(\"ALTER TABLE absences ADD COLUMN decided_at INTEGER\");\n\t\t\t}\n\t\t\tif (!hasColumn(db, \"absences\", \"decided_by\")) {\n\t\t\t\tdb.exec(\"ALTER TABLE absences ADD COLUMN decided_by INTEGER REFERENCES users(id) ON DELETE SET NULL\");\n\t\t\t}\n\t\t\tif (!hasColumn(db, \"absences\", \"decision_note\")) {\n\t\t\t\tdb.exec(\"ALTER TABLE absences ADD COLUMN decision_note TEXT\");\n\t\t\t}\n\t\t\t// The administration looks at the open requests first, so they get an index of their own.\n\t\t\tdb.exec(\"CREATE INDEX IF NOT EXISTS idx_absences_approval ON absences(approval, date_from)\");\n\t\t},\n\t},\n];\n"],
5
- "mappings": "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",
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\t{\n\t\tversion: 20,\n\t\tname: \"work profiles: whether the presence card shows the working time of the day\",\n\t\tsql: `\n\t\t\t-- The presence screen is visible before the PIN is entered, so showing the worked time of an employee\n\t\t\t-- is a decision of the administration, per employee. Off by default.\n\t\t\tALTER TABLE work_profiles ADD COLUMN show_worked_time INTEGER NOT NULL DEFAULT 0;\n\t\t`,\n\t},\n\t{\n\t\tversion: 21,\n\t\tname: \"absences: approval by the administration (requested, approved, rejected)\",\n\t\trun: (db: Db): void => {\n\t\t\t// An employee requests, the administration decides. `approval` is separate from `status`, which keeps\n\t\t\t// saying whether the days are planned or already taken: a request can be planned and still wait for\n\t\t\t// its decision. Everything that exists before this migration counts as approved, so no figure changes\n\t\t\t// for a running installation; a request of an employee starts as `requested`.\n\t\t\tif (!hasColumn(db, \"absences\", \"approval\")) {\n\t\t\t\tdb.exec(\n\t\t\t\t\t\"ALTER TABLE absences ADD COLUMN approval TEXT NOT NULL DEFAULT 'approved' \" +\n\t\t\t\t\t\t\"CHECK (approval IN ('requested','approved','rejected'))\",\n\t\t\t\t);\n\t\t\t}\n\t\t\t// Who decided when and why \u2014 a rejection without a reason is not much use to the employee.\n\t\t\tif (!hasColumn(db, \"absences\", \"decided_at\")) {\n\t\t\t\tdb.exec(\"ALTER TABLE absences ADD COLUMN decided_at INTEGER\");\n\t\t\t}\n\t\t\tif (!hasColumn(db, \"absences\", \"decided_by\")) {\n\t\t\t\tdb.exec(\"ALTER TABLE absences ADD COLUMN decided_by INTEGER REFERENCES users(id) ON DELETE SET NULL\");\n\t\t\t}\n\t\t\tif (!hasColumn(db, \"absences\", \"decision_note\")) {\n\t\t\t\tdb.exec(\"ALTER TABLE absences ADD COLUMN decision_note TEXT\");\n\t\t\t}\n\t\t\t// The administration looks at the open requests first, so they get an index of their own.\n\t\t\tdb.exec(\"CREATE INDEX IF NOT EXISTS idx_absences_approval ON absences(approval, date_from)\");\n\t\t},\n\t},\n\t{\n\t\tversion: 22,\n\t\tname: \"absence types: sickness, accident and military are for the administration only\",\n\t\tsql: `\n\t\t\t-- A sickness note usually reaches a company on the same day, so nobody \u201Crequests\u201D it: the administration\n\t\t\t-- books it. \u201CVisible for everybody\u201D (is_active) therefore starts switched off for these three types, so an\n\t\t\t-- employee does not find them in the app. The administration can switch it on again in the card of the\n\t\t\t-- absence types at any time.\n\t\t\tUPDATE absence_types SET is_active = 0\n\t\t\t WHERE user_id IS NULL AND code IN ('K','U','M') AND is_active = 1;\n\t\t`,\n\t},\n];\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwCO,SAAS,UAAU,IAAQ,OAAe,QAAyB;AACzE,QAAM,OAAO,GAAG,QAAQ,qBAAqB,KAAK,GAAG,EAAE,IAAI;AAC3D,SAAO,KAAK,KAAK,SAAO,IAAI,SAAS,MAAM;AAC5C;AAEO,MAAM,aAA0B;AAAA,EACtC;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuHN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4CN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA2DN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA4CN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA,EACN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA,EAIN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA,EACN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK,CAAC,OAAiB;AAItB,UAAI,UAAU,IAAI,SAAS,aAAa,GAAG;AAC1C,WAAG,KAAK,2CAA2C;AAAA,MACpD;AACA,UAAI,UAAU,IAAI,iBAAiB,eAAe,GAAG;AACpD,WAAG,KAAK,qDAAqD;AAAA,MAC9D;AAKA,UAAI,CAAC,UAAU,IAAI,aAAa,aAAa,GAAG;AAC/C;AAAA,MACD;AACA,SAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAkBP;AAAA,IACF;AAAA,EACD;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA,EACN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK,CAAC,OAAiB;AAGtB,UAAI,CAAC,UAAU,IAAI,iBAAiB,oBAAoB,GAAG;AAC1D,WAAG,KAAK,oFAAoF;AAAA,MAC7F;AACA,UAAI,UAAU,IAAI,iBAAiB,YAAY,GAAG;AACjD,WAAG,KAAK,yEAAyE;AACjF,WAAG,KAAK,kDAAkD;AAAA,MAC3D;AAAA,IACD;AAAA,EACD;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA,EACN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8BN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6CN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,EAKN;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK,CAAC,OAAiB;AAKtB,UAAI,CAAC,UAAU,IAAI,YAAY,UAAU,GAAG;AAC3C,WAAG;AAAA,UACF;AAAA,QAED;AAAA,MACD;AAEA,UAAI,CAAC,UAAU,IAAI,YAAY,YAAY,GAAG;AAC7C,WAAG,KAAK,oDAAoD;AAAA,MAC7D;AACA,UAAI,CAAC,UAAU,IAAI,YAAY,YAAY,GAAG;AAC7C,WAAG,KAAK,4FAA4F;AAAA,MACrG;AACA,UAAI,CAAC,UAAU,IAAI,YAAY,eAAe,GAAG;AAChD,WAAG,KAAK,oDAAoD;AAAA,MAC7D;AAEA,SAAG,KAAK,mFAAmF;AAAA,IAC5F;AAAA,EACD;AAAA,EACA;AAAA,IACC,SAAS;AAAA,IACT,MAAM;AAAA,IACN,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQN;AACD;",
6
6
  "names": []
7
7
  }
@@ -143,6 +143,8 @@ function createAbsencesRepository(db) {
143
143
  const updateType = db.prepare(
144
144
  `UPDATE absence_types SET name = ?, paid = ?, factor = ?, reduce_vacation = ?, is_active = ? WHERE id = ?`
145
145
  );
146
+ const deleteType = db.prepare("DELETE FROM absence_types WHERE id = ?");
147
+ const countAbsencesOfType = db.prepare("SELECT COUNT(*) AS count FROM absences WHERE type_id = ?");
146
148
  const read = (id) => {
147
149
  const row = selectAbsenceById.get(id);
148
150
  return row ? mapAbsenceRow(row) : null;
@@ -248,6 +250,33 @@ function createAbsencesRepository(db) {
248
250
  };
249
251
  return { type, created: existing === null };
250
252
  },
253
+ removeType(input) {
254
+ var _a;
255
+ const existing = resolveType(input.id);
256
+ if (!existing) {
257
+ return false;
258
+ }
259
+ const used = countAbsencesOfType.get(input.id).count;
260
+ if (used > 0) {
261
+ throw new import_errors.ValidationError(`absence type ${existing.code} is still used by ${used} absence(s)`);
262
+ }
263
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
264
+ const run = db.transaction(() => {
265
+ var _a2;
266
+ deleteType.run(input.id);
267
+ (0, import_audit.writeAuditLog)(db, {
268
+ atUtc: now,
269
+ actorId: input.actorId,
270
+ action: "absence.type.remove",
271
+ entity: "absence_type",
272
+ entityId: input.id,
273
+ detail: { code: existing.code, name: existing.name },
274
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
275
+ });
276
+ });
277
+ run();
278
+ return true;
279
+ },
251
280
  findById: read,
252
281
  create(input) {
253
282
  var _a, _b, _c, _d, _e;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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/**\n * Approval of an absence: the employee requests, the administration decides.\n *\n * Independent of {@link AbsenceStatus}, which says whether the days are planned or already taken \u2014 a request can be\n * planned and still wait for its decision. Rows that exist before migration 21 count as `approved`, so nothing\n * changes for a running installation.\n */\nexport type AbsenceApproval = \"requested\" | \"approved\" | \"rejected\";\n\n/**\n * True when an absence counts for the calculation.\n *\n * A row counts unless it is a request that still waits for its decision or a rejected one. Everything else is\n * effective \u2014 including rows that predate migration 21 and therefore carry no value at all.\n *\n * @param absence - the absence to check\n * @param absence.approval - approval of the absence, missing on rows that predate migration 21\n * @returns true when the days count\n */\nexport function isApproved(absence: { approval?: AbsenceApproval }): boolean {\n\treturn absence.approval !== \"requested\" && absence.approval !== \"rejected\";\n}\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/** Approval of the administration: requested, approved or rejected */\n\tapproval: AbsenceApproval;\n\t/** Instant of the decision, UTC epoch seconds (`null` while nobody decided) */\n\tdecidedAt: number | null;\n\t/** Who decided about the request */\n\tdecidedBy: number | null;\n\t/** Reason of the decision, shown to the employee */\n\tdecisionNote: string | null;\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/** Approval, default `approved` (the administration enters dates, an employee requests them) */\n\tapproval?: AbsenceApproval;\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/** Approves or rejects a request of an employee and audits the decision */\n\tsetApproval(input: {\n\t\tid: number;\n\t\tapproval: AbsenceApproval;\n\t\tnote?: string | null;\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\t/** Absences of **all** employees overlapping a range \u2014 the overview of the administration */\n\tallInRange(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\tapproval: AbsenceApproval;\n\tdecided_at: number | null;\n\tdecided_by: number | null;\n\tdecision_note: string | null;\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, approval, decided_at, decided_by, decision_note, 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\tapproval: row.approval,\n\t\tdecidedAt: row.decided_at,\n\t\tdecidedBy: row.decided_by,\n\t\tdecisionNote: row.decision_note,\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.approval,\n\t\t a.decided_at, a.decided_by, a.decision_note, a.note, 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\t// the same list for the administration: every employee in one answer, so the overview needs one request\n\tconst selectAllWithTypes = 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.approval,\n\t\t a.decided_at, a.decided_by, a.decision_note, a.note, 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.date_to >= ? AND a.date_from <= ?\n\t\t ORDER BY a.date_from, a.date_to, a.user_id, 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, approval, decided_at, decided_by,\n\t\t decision_note, 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 updateApprovalStatement = db.prepare(\n\t\t\"UPDATE absences SET approval = ?, decided_at = ?, decided_by = ?, decision_note = ? WHERE id = ?\",\n\t);\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\t// the administration enters dates straight away, a request of an employee waits for its decision\n\t\t\tconst approval: AbsenceApproval = input.approval ?? \"approved\";\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst decidedAt = approval === \"requested\" ? null : now;\n\t\t\tconst decidedBy = approval === \"requested\" ? null : input.actorId;\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\tapproval,\n\t\t\t\t\tdecidedAt,\n\t\t\t\t\tdecidedBy,\n\t\t\t\t\tnull,\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\tsetApproval(input: {\n\t\t\tid: number;\n\t\t\tapproval: AbsenceApproval;\n\t\t\tnote?: string | null;\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\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupdateApprovalStatement.run(input.approval, now, input.actorId, input.note ?? null, 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.approval\",\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\tchanges: { approval: { old: current.approval, new: input.approval } },\n\t\t\t\t\t\t...(input.note ? { note: input.note } : {}),\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 updated = read(input.id);\n\t\t\tif (!updated) {\n\t\t\t\tthrow new Error(`absence ${input.id} disappeared right after the decision`);\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\n\t\tallInRange(from: string, to: string): AbsenceWithType[] {\n\t\t\tconst rows = selectAllWithTypes.all(\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": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,kBAA6B;AAC7B,oBAA+C;AAC/C,mBAA0C;AAwBnC,SAAS,WAAW,SAAkD;AAC5E,SAAO,QAAQ,aAAa,eAAe,QAAQ,aAAa;AACjE;AAqJO,MAAM,gCAAgC,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMlD,YAA4B,WAA4B;AACvD,UAAM,yBAAyB,SAAS,GAAG;AADhB;AAE3B,SAAK,OAAO;AAAA,EACb;AACD;AAkFA,MAAM,eAAe;AACrB,MAAM,kBAAkB;AAAA;AAIxB,MAAM,iBAA0C,CAAC,UAAU,YAAY,UAAU,cAAc,SAAS,MAAM;AAQvG,SAAS,kBAAkB,KAAwC;AACzE,SAAO;AAAA,IACN,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,MAAM,IAAI,SAAS;AAAA,IACnB,QAAQ,IAAI;AAAA,IACZ,gBAAgB,IAAI,oBAAoB;AAAA,IACxC,UAAU,IAAI,cAAc;AAAA,EAC7B;AACD;AAQO,SAAS,cAAc,KAAgC;AAC7D,SAAO;AAAA,IACN,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI;AAAA,IACd,QAAQ,IAAI;AAAA,IACZ,YAAY,IAAI;AAAA,IAChB,OAAO,IAAI;AAAA,IACX,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,cAAc,IAAI;AAAA,IAClB,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,EAChB;AACD;AASA,SAAS,YAAY,OAAe,OAAuB;AAC1D,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,KAAC,0BAAa,OAAO,GAAG;AAC3B,UAAM,IAAI,8BAAgB,GAAG,KAAK,KAAK,KAAK,8BAA8B;AAAA,EAC3E;AACA,SAAO;AACR;AAQA,SAAS,kBAAkB,OAAuB;AACjD,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,SAAS,KAAK,QAAQ,GAAG;AACvD,UAAM,IAAI,8BAAgB,wDAAwD,KAAK,GAAG;AAAA,EAC3F;AACA,SAAO;AACR;AAQO,SAAS,yBAAyB,IAA4B;AACpE,QAAM,iBAAiB,GAAG,QAAQ,UAAU,YAAY,kCAAkC;AAC1F,QAAM,qBAAqB,GAAG,QAAQ,UAAU,YAAY,oDAAoD;AAChH,QAAM,uBAAuB,GAAG;AAAA,IAC/B,UAAU,YAAY;AAAA,EACvB;AACA,QAAM,oBAAoB,GAAG,QAAQ,UAAU,eAAe,6BAA6B;AAC3F,QAAM,eAAe,GAAG;AAAA,IACvB,UAAU,eAAe;AAAA;AAAA;AAAA,EAG1B;AACA,QAAM,kBAAkB,GAAG;AAAA,IAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOD;AAEA,QAAM,qBAAqB,GAAG;AAAA,IAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOD;AACA,QAAM,gBAAgB,GAAG;AAAA,IACxB;AAAA;AAAA;AAAA;AAAA,EAID;AACA,QAAM,gBAAgB,GAAG;AAAA,IACxB;AAAA;AAAA,EAED;AACA,QAAM,wBAAwB,GAAG,QAAQ,6CAA6C;AACtF,QAAM,0BAA0B,GAAG;AAAA,IAClC;AAAA,EACD;AACA,QAAM,gBAAgB,GAAG,QAAQ,mCAAmC;AACpE,QAAM,aAAa,GAAG;AAAA,IACrB;AAAA;AAAA,EAED;AACA,QAAM,aAAa,GAAG;AAAA,IACrB;AAAA,EACD;AAEA,QAAM,OAAO,CAAC,OAAqC;AAClD,UAAM,MAAM,kBAAkB,IAAI,EAAE;AACpC,WAAO,MAAM,cAAc,GAAG,IAAI;AAAA,EACnC;AASA,QAAM,cAAc,CAAC,WAA4B,WAAqD;AACrG,QAAI,OAAO,cAAc,UAAU;AAClC,YAAM,MAAM,eAAe,IAAI,SAAS;AACxC,aAAO,MAAM,kBAAkB,GAAG,IAAI;AAAA,IACvC;AACA,QAAI,UAAU,MAAM;AACnB,YAAM,MAAM,mBAAmB,IAAI,WAAW,MAAM;AACpD,UAAI,KAAK;AACR,eAAO,kBAAkB,GAAG;AAAA,MAC7B;AAAA,IACD;AACA,UAAM,SAAS,qBAAqB,IAAI,SAAS;AACjD,WAAO,SAAS,kBAAkB,MAAM,IAAI;AAAA,EAC7C;AAWA,QAAM,cAAc,CAAC,UAAqF;AAtc3G;AAucE,UAAM,aAAY,WAAM,WAAN,YAAgB,MAAM;AACxC,QAAI,cAAc,QAAW;AAC5B,YAAM,IAAI,wBAAwB,QAAQ;AAAA,IAC3C;AACA,UAAM,OAAO,YAAY,WAAW,MAAM,MAAM;AAChD,QAAI,CAAC,MAAM;AACV,YAAM,IAAI,wBAAwB,SAAS;AAAA,IAC5C;AACA,WAAO;AAAA,EACR;AAEA,SAAO;AAAA,IACN,MAAM,SAAsF;AAnd9F;AAodG,YAAM,cAAa,mCAAS,qBAAoB,OAAO,KAAK;AAC5D,YAAM,UAAS,wCAAS,WAAT,YAAmB;AAClC,YAAM,OAAO,GACX;AAAA,QACA,UAAU,YAAY;AAAA,8CACmB,UAAU;AAAA;AAAA,MAEpD,EACC,IAAI,MAAM;AACZ,aAAO,KAAK,IAAI,iBAAiB;AAAA,IAClC;AAAA,IAEA,SAAS,WAA4B,QAAkD;AACtF,aAAO,YAAY,WAAW,MAAM;AAAA,IACrC;AAAA,IAEA,WAAW,OAA8E;AApe3F;AAqeG,YAAM,UAAS,WAAM,WAAN,YAAgB;AAC/B,YAAM,OAAO,MAAM,KAAK,KAAK,EAAE,YAAY;AAC3C,UAAI,CAAC,uBAAuB,KAAK,IAAI,GAAG;AACvC,cAAM,IAAI,8BAAgB,yDAAyD,MAAM,IAAI,IAAI;AAAA,MAClG;AACA,YAAM,OAAO,MAAM,KAAK,KAAK;AAC7B,UAAI,CAAC,MAAM;AACV,cAAM,IAAI,8BAAgB,kBAAkB;AAAA,MAC7C;AACA,YAAM,UAAS,WAAM,WAAN,YAAgB;AAC/B,UAAI,CAAC,OAAO,UAAU,MAAM,KAAK,SAAS,KAAK,SAAS,KAAK;AAC5D,cAAM,IAAI,8BAAgB,wDAAwD,MAAM,MAAM,GAAG;AAAA,MAClG;AACA,YAAM,QAAO,WAAM,SAAN,YAAc;AAC3B,YAAM,kBAAiB,WAAM,mBAAN,YAAwB;AAC/C,YAAM,YAAW,WAAM,aAAN,YAAkB;AACnC,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAErD,YAAM,WAAW,YAAY,MAAM,MAAM;AACzC,UAAI,UAAS,0CAAU,OAAV,YAAgB;AAE7B,YAAM,MAAM,GAAG,YAAY,MAAY;AA1f1C,YAAAA;AA2fI,YAAI,UAAU;AACb,qBAAW,IAAI,MAAM,OAAO,IAAI,GAAG,QAAQ,iBAAiB,IAAI,GAAG,WAAW,IAAI,GAAG,SAAS,EAAE;AAAA,QACjG,OAAO;AACN,gBAAM,SAAS,WAAW;AAAA,YACzB;AAAA,YACA;AAAA,YACA;AAAA,YACA,OAAO,IAAI;AAAA,YACX;AAAA,YACA,iBAAiB,IAAI;AAAA,YACrB,WAAW,IAAI;AAAA,UAChB;AACA,mBAAS,OAAO,OAAO,eAAe;AAAA,QACvC;AAEA,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,KAAIA,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,UACrB,QAAQ,WAAW,wBAAwB;AAAA,UAC3C,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ,EAAE,MAAM,QAAQ,MAAM,MAAM,QAAQ,gBAAgB,SAAS;AAAA,QACtE,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAEJ,YAAM,OAAO,WACV,EAAE,GAAG,UAAU,MAAM,MAAM,QAAQ,gBAAgB,SAAS,IAC5D;AAAA,QACA,IAAI;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACD;AACF,aAAO,EAAE,MAAM,SAAS,aAAa,KAAK;AAAA,IAC3C;AAAA,IAEA,UAAU;AAAA,IAEV,OAAO,OAA0C;AAviBnD;AAwiBG,YAAM,OAAO,YAAY,KAAK;AAC9B,YAAM,WAAW,YAAY,MAAM,UAAU,UAAU;AACvD,YAAM,SAAS,aAAY,WAAM,WAAN,YAAgB,UAAU,QAAQ;AAC7D,UAAI,WAAW,QAAQ;AACtB,cAAM,IAAI,8BAAgB,aAAa,QAAQ,+BAA+B,MAAM,GAAG;AAAA,MACxF;AACA,YAAM,aAAa,mBAAkB,WAAM,eAAN,YAAoB,CAAC;AAC1D,YAAM,UAAwB,WAAM,WAAN,YAAgB;AAE9C,YAAM,YAA4B,WAAM,aAAN,YAAkB;AACpD,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,YAAY,aAAa,cAAc,OAAO;AACpD,YAAM,YAAY,aAAa,cAAc,OAAO,MAAM;AAE1D,UAAI,YAAY;AAChB,YAAM,MAAM,GAAG,YAAY,MAAY;AAvjB1C,YAAAA,KAAAC,KAAAC;AAwjBI,cAAM,SAAS,cAAc;AAAA,UAC5B,MAAM;AAAA,UACN,KAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA;AAAA,WACAF,MAAA,MAAM,UAAN,OAAAA,MAAe;AAAA,UACf;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,WACAC,MAAA,MAAM,SAAN,OAAAA,MAAc;AAAA,UACd;AAAA,UACA,MAAM;AAAA,QACP;AACA,oBAAY,OAAO,OAAO,eAAe;AACzC,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ;AAAA,YACP,QAAQ,MAAM;AAAA,YACd,UAAU,KAAK;AAAA,YACf;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACD;AAAA,UACA,KAAIC,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAEJ,YAAM,UAAU,KAAK,SAAS;AAC9B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,MAAM,WAAW,SAAS,mCAAmC;AAAA,MACxE;AACA,aAAO;AAAA,IACR;AAAA,IAEA,OAAO,OAA0C;AAnmBnD;AAomBG,YAAM,UAAU,KAAK,MAAM,EAAE;AAC7B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,4BAAc,WAAW,MAAM,EAAE,YAAY;AAAA,MACxD;AAEA,YAAM,OACL,MAAM,MAAM,WAAW,UAAa,MAAM,MAAM,aAAa,SAC1D,OACA,YAAY,EAAE,GAAG,MAAM,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAC1D,YAAM,OAAsB;AAAA,QAC3B,GAAG;AAAA,QACH,SAAQ,kCAAM,OAAN,YAAY,QAAQ;AAAA,QAC5B,UAAU,MAAM,MAAM,WAAW,YAAY,MAAM,MAAM,UAAU,UAAU,IAAI,QAAQ;AAAA,QACzF,QAAQ,MAAM,MAAM,SAAS,YAAY,MAAM,MAAM,QAAQ,QAAQ,IAAI,QAAQ;AAAA,QACjF,YACC,MAAM,MAAM,eAAe,SACxB,QAAQ,aACR,kBAAkB,MAAM,MAAM,UAAU;AAAA,QAC5C,OAAO,MAAM,MAAM,UAAU,SAAY,QAAQ,QAAQ,MAAM,MAAM;AAAA,QACrE,MAAM,MAAM,MAAM,SAAS,SAAY,QAAQ,OAAO,MAAM,MAAM;AAAA,MACnE;AACA,UAAI,KAAK,WAAW,KAAK,QAAQ;AAChC,cAAM,IAAI,8BAAgB,aAAa,KAAK,QAAQ,+BAA+B,KAAK,MAAM,GAAG;AAAA,MAClG;AAEA,YAAM,cAAU;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,MACD;AACA,UAAI,OAAO,KAAK,OAAO,EAAE,WAAW,GAAG;AACtC,eAAO;AAAA,MACR;AAEA,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,MAAM,GAAG,YAAY,MAAY;AAvoB1C,YAAAF,KAAAC;AAwoBI,sBAAc;AAAA,UACb,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,MAAM;AAAA,QACP;AACA,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU,MAAM;AAAA,UAChB,QAAQ,EAAE,SAAS,SAAQD,MAAA,MAAM,WAAN,OAAAA,MAAgB,KAAK;AAAA,UAChD,KAAIC,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAEJ,YAAM,UAAU,KAAK,MAAM,EAAE;AAC7B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,MAAM,WAAW,MAAM,EAAE,qCAAqC;AAAA,MACzE;AACA,aAAO;AAAA,IACR;AAAA,IAEA,UAAU,OAMQ;AA1qBpB;AA2qBG,YAAM,UAAU,KAAK,MAAM,EAAE;AAC7B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,4BAAc,WAAW,MAAM,EAAE,YAAY;AAAA,MACxD;AACA,UAAI,QAAQ,WAAW,MAAM,QAAQ;AACpC,eAAO;AAAA,MACR;AAEA,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,MAAM,GAAG,YAAY,MAAY;AAprB1C,YAAAD;AAqrBI,8BAAsB,IAAI,MAAM,QAAQ,MAAM,EAAE;AAChD,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU,MAAM;AAAA,UAChB,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,QAAQ,QAAQ,KAAK,MAAM,OAAO,EAAE,EAAE;AAAA,UAC1E,KAAIA,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAEJ,YAAM,UAAU,KAAK,MAAM,EAAE;AAC7B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,MAAM,WAAW,MAAM,EAAE,4CAA4C;AAAA,MAChF;AACA,aAAO;AAAA,IACR;AAAA,IAEA,YAAY,OAOM;AAhtBpB;AAitBG,YAAM,UAAU,KAAK,MAAM,EAAE;AAC7B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,4BAAc,WAAW,MAAM,EAAE,YAAY;AAAA,MACxD;AACA,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAErD,YAAM,MAAM,GAAG,YAAY,MAAY;AAvtB1C,YAAAA,KAAA;AAwtBI,gCAAwB,IAAI,MAAM,UAAU,KAAK,MAAM,UAASA,MAAA,MAAM,SAAN,OAAAA,MAAc,MAAM,MAAM,EAAE;AAC5F,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU,MAAM;AAAA,UAChB,QAAQ;AAAA,YACP,SAAS,EAAE,UAAU,EAAE,KAAK,QAAQ,UAAU,KAAK,MAAM,SAAS,EAAE;AAAA,YACpE,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;AAAA,UAC1C;AAAA,UACA,KAAI,WAAM,YAAN,YAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAEJ,YAAM,UAAU,KAAK,MAAM,EAAE;AAC7B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,MAAM,WAAW,MAAM,EAAE,uCAAuC;AAAA,MAC3E;AACA,aAAO;AAAA,IACR;AAAA,IAEA,OAAO,OAAwF;AA/uBjG;AAgvBG,YAAM,UAAU,KAAK,MAAM,EAAE;AAC7B,UAAI,CAAC,SAAS;AACb,eAAO;AAAA,MACR;AACA,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAErD,YAAM,MAAM,GAAG,YAAY,MAAY;AAtvB1C,YAAAA;AAuvBI,sBAAc,IAAI,MAAM,EAAE;AAC1B,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU,MAAM;AAAA,UAChB,QAAQ;AAAA,YACP,QAAQ,QAAQ;AAAA,YAChB,UAAU,QAAQ;AAAA,YAClB,QAAQ,QAAQ;AAAA,YAChB,QAAQ,QAAQ;AAAA,UACjB;AAAA,UACA,KAAIA,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AACJ,aAAO;AAAA,IACR;AAAA,IAEA,WAAW,QAAgB,SAA0E;AA3wBvG;AA4wBG,YAAM,QAAO,wCAAS,SAAT,aAAkB,mCAAS,UAAS,SAAY,eAAe,GAAG,QAAQ,IAAI;AAC3F,YAAM,MAAK,wCAAS,OAAT,aAAgB,mCAAS,UAAS,SAAY,eAAe,GAAG,QAAQ,IAAI;AACvF,YAAM,OAAO,aAAa,IAAI,QAAQ,MAAM,EAAE;AAC9C,aAAO,KAAK,IAAI,aAAa;AAAA,IAC9B;AAAA,IAEA,QAAQ,QAAgB,MAA+B;AACtD,YAAM,MAAM,YAAY,MAAM,MAAM;AACpC,YAAM,OAAO,aAAa,IAAI,QAAQ,KAAK,GAAG;AAC9C,aAAO,KAAK,IAAI,aAAa;AAAA,IAC9B;AAAA,IAEA,iBAAiB,QAAgB,MAAc,IAA+B;AAC7E,YAAM,OAAO,gBAAgB;AAAA,QAC5B;AAAA,QACA,YAAY,MAAM,MAAM;AAAA,QACxB,YAAY,IAAI,IAAI;AAAA,MACrB;AACA,aAAO,KAAK,IAAI,UAAQ;AAAA,QACvB,GAAG,cAAc,GAAG;AAAA,QACpB,UAAU,IAAI;AAAA,QACd,UAAU,IAAI;AAAA,QACd,MAAM,IAAI,cAAc;AAAA,QACxB,QAAQ,IAAI;AAAA,QACZ,gBAAgB,IAAI,yBAAyB;AAAA,MAC9C,EAAE;AAAA,IACH;AAAA,IAEA,WAAW,MAAc,IAA+B;AACvD,YAAM,OAAO,mBAAmB;AAAA,QAC/B,YAAY,MAAM,MAAM;AAAA,QACxB,YAAY,IAAI,IAAI;AAAA,MACrB;AACA,aAAO,KAAK,IAAI,UAAQ;AAAA,QACvB,GAAG,cAAc,GAAG;AAAA,QACpB,UAAU,IAAI;AAAA,QACd,UAAU,IAAI;AAAA,QACd,MAAM,IAAI,cAAc;AAAA,QACxB,QAAQ,IAAI;AAAA,QACZ,gBAAgB,IAAI,yBAAyB;AAAA,MAC9C,EAAE;AAAA,IACH;AAAA,EACD;AACD;",
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/**\n * Approval of an absence: the employee requests, the administration decides.\n *\n * Independent of {@link AbsenceStatus}, which says whether the days are planned or already taken \u2014 a request can be\n * planned and still wait for its decision. Rows that exist before migration 21 count as `approved`, so nothing\n * changes for a running installation.\n */\nexport type AbsenceApproval = \"requested\" | \"approved\" | \"rejected\";\n\n/**\n * True when an absence counts for the calculation.\n *\n * A row counts unless it is a request that still waits for its decision or a rejected one. Everything else is\n * effective \u2014 including rows that predate migration 21 and therefore carry no value at all.\n *\n * @param absence - the absence to check\n * @param absence.approval - approval of the absence, missing on rows that predate migration 21\n * @returns true when the days count\n */\nexport function isApproved(absence: { approval?: AbsenceApproval }): boolean {\n\treturn absence.approval !== \"requested\" && absence.approval !== \"rejected\";\n}\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/** Approval of the administration: requested, approved or rejected */\n\tapproval: AbsenceApproval;\n\t/** Instant of the decision, UTC epoch seconds (`null` while nobody decided) */\n\tdecidedAt: number | null;\n\t/** Who decided about the request */\n\tdecidedBy: number | null;\n\t/** Reason of the decision, shown to the employee */\n\tdecisionNote: string | null;\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/** Approval, default `approved` (the administration enters dates, an employee requests them) */\n\tapproval?: AbsenceApproval;\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/** Removes an absence type that no absence uses */\n\tremoveType(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): 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/** Approves or rejects a request of an employee and audits the decision */\n\tsetApproval(input: {\n\t\tid: number;\n\t\tapproval: AbsenceApproval;\n\t\tnote?: string | null;\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\t/** Absences of **all** employees overlapping a range \u2014 the overview of the administration */\n\tallInRange(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\tapproval: AbsenceApproval;\n\tdecided_at: number | null;\n\tdecided_by: number | null;\n\tdecision_note: string | null;\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, approval, decided_at, decided_by, decision_note, 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\tapproval: row.approval,\n\t\tdecidedAt: row.decided_at,\n\t\tdecidedBy: row.decided_by,\n\t\tdecisionNote: row.decision_note,\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.approval,\n\t\t a.decided_at, a.decided_by, a.decision_note, a.note, 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\t// the same list for the administration: every employee in one answer, so the overview needs one request\n\tconst selectAllWithTypes = 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.approval,\n\t\t a.decided_at, a.decided_by, a.decision_note, a.note, 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.date_to >= ? AND a.date_from <= ?\n\t\t ORDER BY a.date_from, a.date_to, a.user_id, 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, approval, decided_at, decided_by,\n\t\t decision_note, 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 updateApprovalStatement = db.prepare(\n\t\t\"UPDATE absences SET approval = ?, decided_at = ?, decided_by = ?, decision_note = ? WHERE id = ?\",\n\t);\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\tconst deleteType = db.prepare(\"DELETE FROM absence_types WHERE id = ?\");\n\tconst countAbsencesOfType = db.prepare(\"SELECT COUNT(*) AS count FROM absences WHERE type_id = ?\");\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\tremoveType(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst existing = resolveType(input.id);\n\t\t\tif (!existing) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t// A type that absences use cannot go: they would lose their meaning (and the foreign key would refuse the\n\t\t\t// delete anyway). The caller turns this into a message the administration can act on.\n\t\t\tconst used = (countAbsencesOfType.get(input.id) as { count: number }).count;\n\t\t\tif (used > 0) {\n\t\t\t\tthrow new ValidationError(`absence type ${existing.code} is still used by ${used} absence(s)`);\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\tdeleteType.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.type.remove\",\n\t\t\t\t\tentity: \"absence_type\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { code: existing.code, name: existing.name },\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\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\t// the administration enters dates straight away, a request of an employee waits for its decision\n\t\t\tconst approval: AbsenceApproval = input.approval ?? \"approved\";\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst decidedAt = approval === \"requested\" ? null : now;\n\t\t\tconst decidedBy = approval === \"requested\" ? null : input.actorId;\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\tapproval,\n\t\t\t\t\tdecidedAt,\n\t\t\t\t\tdecidedBy,\n\t\t\t\t\tnull,\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\tsetApproval(input: {\n\t\t\tid: number;\n\t\t\tapproval: AbsenceApproval;\n\t\t\tnote?: string | null;\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\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupdateApprovalStatement.run(input.approval, now, input.actorId, input.note ?? null, 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.approval\",\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\tchanges: { approval: { old: current.approval, new: input.approval } },\n\t\t\t\t\t\t...(input.note ? { note: input.note } : {}),\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 updated = read(input.id);\n\t\t\tif (!updated) {\n\t\t\t\tthrow new Error(`absence ${input.id} disappeared right after the decision`);\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\n\t\tallInRange(from: string, to: string): AbsenceWithType[] {\n\t\t\tconst rows = selectAllWithTypes.all(\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
6
  "names": ["_a", "_b", "_c"]
7
7
  }
@@ -76,13 +76,13 @@ const ROLE_PERMISSIONS = {
76
76
  employee: ["time.punch", "time.edit_own", "absence.request", "report.view_own"]
77
77
  };
78
78
  const ABSENCE_TYPES = [
79
- { code: "F", name: "Ferien", factor: 100, reduceVacation: true },
80
- { code: "K", name: "Krankheit", factor: 100, reduceVacation: false },
81
- { code: "U", name: "Unfall", factor: 100, reduceVacation: false },
82
- { code: "M", name: "Milit\xE4r", factor: 100, reduceVacation: false },
83
- { code: "I", name: "Intern", factor: 100, reduceVacation: false },
84
- { code: "W", name: "Weiterbildung", factor: 50, reduceVacation: false },
85
- { code: "E", name: "Extern", factor: 50, reduceVacation: false }
79
+ { code: "F", name: "Ferien", factor: 100, reduceVacation: true, active: true },
80
+ { code: "K", name: "Krankheit", factor: 100, reduceVacation: false, active: false },
81
+ { code: "U", name: "Unfall", factor: 100, reduceVacation: false, active: false },
82
+ { code: "M", name: "Milit\xE4r", factor: 100, reduceVacation: false, active: false },
83
+ { code: "I", name: "Intern", factor: 100, reduceVacation: false, active: true },
84
+ { code: "W", name: "Weiterbildung", factor: 50, reduceVacation: false, active: true },
85
+ { code: "E", name: "Extern", factor: 50, reduceVacation: false, active: true }
86
86
  ];
87
87
  const SETTING_DEFAULTS = {
88
88
  edit_window_days: "7",
@@ -136,10 +136,10 @@ function seed(db, options = {}) {
136
136
  }
137
137
  const insertAbsenceType = db.prepare(
138
138
  `INSERT OR IGNORE INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active)
139
- VALUES (NULL, ?, ?, 1, ?, ?, 1)`
139
+ VALUES (NULL, ?, ?, 1, ?, ?, ?)`
140
140
  );
141
141
  for (const type of ABSENCE_TYPES) {
142
- insertAbsenceType.run(type.code, type.name, type.factor, type.reduceVacation ? 1 : 0);
142
+ insertAbsenceType.run(type.code, type.name, type.factor, type.reduceVacation ? 1 : 0, type.active ? 1 : 0);
143
143
  }
144
144
  const insertSetting = db.prepare(
145
145
  "INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/db/seed.ts"],
4
- "sourcesContent": ["/**\n * Initial data (seeds): roles, permission catalogue, absence types, instance settings and holidays.\n *\n * All seed operations are idempotent (`INSERT OR IGNORE`), so `seed()` can run on every adapter start.\n */\n\nimport type { Db } from \"./database\";\nimport { holidaysForYear, type HolidayCountry } from \"../domain/holidays\";\n\n/** Permission catalogue (keys are stored in `permissions.key`). */\nexport const PERMISSIONS = {\n\ttime: [\"time.punch\", \"time.edit_own\", \"time.edit_other\", \"time.delete\", \"time.import\", \"time.resolve_conflict\"],\n\tabsence: [\"absence.request\", \"absence.approve\", \"absence.edit_other\", \"absence.manage_types\"],\n\tuser: [\"user.view\", \"user.create\", \"user.edit\", \"user.deactivate\", \"user.manage_roles\"],\n\treport: [\"report.view_own\", \"report.view_other\", \"report.export\", \"report.statistics\", \"report.print_locked\"],\n\tpayout: [\"payout.view\", \"payout.create\"],\n\tsystem: [\n\t\t\"settings.view\",\n\t\t\"settings.edit\",\n\t\t\"rfid.manage\",\n\t\t\"terminal.manage\",\n\t\t\"holiday.manage\",\n\t\t\"import.run\",\n\t\t\"audit.view\",\n\t\t\"backup.run\",\n\t],\n\tcommand: [\"command.punch\", \"command.close_month\", \"command.recalc\"],\n} as const;\n\nexport const ALL_PERMISSIONS: string[] = Object.values(PERMISSIONS).flat();\n\n/** Role \u2192 permissions (specification section 4.8). */\nexport const ROLE_PERMISSIONS: Record<string, string[]> = {\n\tadmin: ALL_PERMISSIONS,\n\tmanager: [\n\t\t\"time.punch\",\n\t\t\"time.edit_own\",\n\t\t\"time.edit_other\",\n\t\t\"time.delete\",\n\t\t\"time.resolve_conflict\",\n\t\t\"absence.request\",\n\t\t\"absence.approve\",\n\t\t\"absence.edit_other\",\n\t\t\"absence.manage_types\",\n\t\t\"user.view\",\n\t\t\"report.view_own\",\n\t\t\"report.view_other\",\n\t\t\"report.export\",\n\t\t\"report.statistics\",\n\t\t\"report.print_locked\",\n\t\t\"payout.view\",\n\t\t\"settings.view\",\n\t\t\"holiday.manage\",\n\t\t\"audit.view\",\n\t\t\"command.punch\",\n\t\t\"command.close_month\",\n\t\t\"command.recalc\",\n\t],\n\temployee: [\"time.punch\", \"time.edit_own\", \"absence.request\", \"report.view_own\"],\n};\n\n/** Default absence types of the time tracking: code, name, factor, reduces vacation. */\nexport const ABSENCE_TYPES: { code: string; name: string; factor: number; reduceVacation: boolean }[] = [\n\t{ code: \"F\", name: \"Ferien\", factor: 100, reduceVacation: true },\n\t{ code: \"K\", name: \"Krankheit\", factor: 100, reduceVacation: false },\n\t{ code: \"U\", name: \"Unfall\", factor: 100, reduceVacation: false },\n\t{ code: \"M\", name: \"Milit\u00E4r\", factor: 100, reduceVacation: false },\n\t{ code: \"I\", name: \"Intern\", factor: 100, reduceVacation: false },\n\t{ code: \"W\", name: \"Weiterbildung\", factor: 50, reduceVacation: false },\n\t{ code: \"E\", name: \"Extern\", factor: 50, reduceVacation: false },\n];\n\n/** Default instance settings (specification section 2.9.9); existing values are never overwritten. */\nexport const SETTING_DEFAULTS: Record<string, string> = {\n\tedit_window_days: \"7\",\n\tquick_round_minutes: \"0\",\n\tabsence_calc_until_today: \"1\",\n\tabsence_deduct_worktime: \"0\",\n\tprint_limit_days: \"0\",\n\tholiday_country: \"DE\",\n\tdefault_language: \"de\",\n\tsession_ttl_minutes: \"720\",\n\tbackup_retention_days: \"30\",\n\tattendance_list_visible: \"0\",\n\t// how the pause of a day is determined: `auto` = the punched break, otherwise the graduated rules\n\tpause_mode: \"auto\",\n\t// path of a Unicode font for the PDF statement; empty uses the built-in Latin fonts\n\treport_font_path: \"\",\n\t// branding of the installation: logo and background as data URLs (empty = not set), accent colour as hex\n\tbrand_logo: \"\",\n\tbrand_background: \"\",\n\tbrand_color: \"\",\n};\n\n/**\n * Options controlling which initial data is written.\n */\nexport interface SeedOptions {\n\t/** Country used to generate public holidays (default `CH`) */\n\tholidayCountry?: HolidayCountry;\n\t/** Years for which holidays are generated (default: current and next year) */\n\tholidayYears?: number[];\n}\n\n/**\n * Writes all initial data. Idempotent: repeated calls change nothing and never overwrite\n * user-modified settings.\n *\n * @param db - open database handle\n * @param options - seed options\n * @returns summary of what is now present\n */\nexport function seed(db: Db, options: SeedOptions = {}): { permissions: number; holidays: number } {\n\tconst country: HolidayCountry = options.holidayCountry ?? \"DE\";\n\tconst currentYear = new Date().getUTCFullYear();\n\tconst years = options.holidayYears ?? [currentYear, currentYear + 1];\n\n\tconst run = db.transaction((): void => {\n\t\tconst now = Math.floor(Date.now() / 1000);\n\n\t\tconst insertRole = db.prepare(\"INSERT OR IGNORE INTO roles (key, name) VALUES (?, ?)\");\n\t\tinsertRole.run(\"admin\", \"Administrator\");\n\t\tinsertRole.run(\"manager\", \"Manager\");\n\t\tinsertRole.run(\"employee\", \"Employee\");\n\n\t\tconst insertPermission = db.prepare(\"INSERT OR IGNORE INTO permissions (key) VALUES (?)\");\n\t\tfor (const permission of ALL_PERMISSIONS) {\n\t\t\tinsertPermission.run(permission);\n\t\t}\n\n\t\tconst findRole = db.prepare(\"SELECT id FROM roles WHERE key = ?\");\n\t\tconst findPermission = db.prepare(\"SELECT id FROM permissions WHERE key = ?\");\n\t\tconst linkRole = db.prepare(\"INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)\");\n\t\tfor (const [roleKey, permissions] of Object.entries(ROLE_PERMISSIONS)) {\n\t\t\tconst role = findRole.get(roleKey) as { id: number } | undefined;\n\t\t\tif (!role) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const permission of permissions) {\n\t\t\t\tconst row = findPermission.get(permission) as { id: number } | undefined;\n\t\t\t\tif (row) {\n\t\t\t\t\tlinkRole.run(role.id, row.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst insertAbsenceType = db.prepare(\n\t\t\t`INSERT OR IGNORE INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active)\n\t\t\t VALUES (NULL, ?, ?, 1, ?, ?, 1)`,\n\t\t);\n\t\tfor (const type of ABSENCE_TYPES) {\n\t\t\tinsertAbsenceType.run(type.code, type.name, type.factor, type.reduceVacation ? 1 : 0);\n\t\t}\n\n\t\tconst insertSetting = db.prepare(\n\t\t\t\"INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)\",\n\t\t);\n\t\tfor (const [key, value] of Object.entries(SETTING_DEFAULTS)) {\n\t\t\tinsertSetting.run(key, key === \"holiday_country\" ? country : value, now);\n\t\t}\n\t});\n\trun();\n\n\tlet holidayCount = 0;\n\tfor (const year of years) {\n\t\tholidayCount += ensureHolidaysForYear(db, country, year);\n\t}\n\n\tconst permissionCount = (db.prepare(\"SELECT COUNT(*) AS c FROM permissions\").get() as { c: number }).c;\n\treturn { permissions: permissionCount, holidays: holidayCount };\n}\n\n/**\n * Generates and stores the public holidays of one year for a country (idempotent).\n *\n * @param db - open database handle\n * @param country - supported country code\n * @param year - four digit year\n * @returns number of inserted holiday rows\n */\nexport function ensureHolidaysForYear(db: Db, country: HolidayCountry, year: number): number {\n\tconst insert = db.prepare(\"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\");\n\tlet inserted = 0;\n\tconst run = db.transaction((): void => {\n\t\tfor (const holiday of holidaysForYear(year, country)) {\n\t\t\tinserted += insert.run(country, year, holiday.date, holiday.name, holiday.key).changes;\n\t\t}\n\t});\n\trun();\n\treturn inserted;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Initial data (seeds): roles, permission catalogue, absence types, instance settings and holidays.\n *\n * All seed operations are idempotent (`INSERT OR IGNORE`), so `seed()` can run on every adapter start.\n */\n\nimport type { Db } from \"./database\";\nimport { holidaysForYear, type HolidayCountry } from \"../domain/holidays\";\n\n/** Permission catalogue (keys are stored in `permissions.key`). */\nexport const PERMISSIONS = {\n\ttime: [\"time.punch\", \"time.edit_own\", \"time.edit_other\", \"time.delete\", \"time.import\", \"time.resolve_conflict\"],\n\tabsence: [\"absence.request\", \"absence.approve\", \"absence.edit_other\", \"absence.manage_types\"],\n\tuser: [\"user.view\", \"user.create\", \"user.edit\", \"user.deactivate\", \"user.manage_roles\"],\n\treport: [\"report.view_own\", \"report.view_other\", \"report.export\", \"report.statistics\", \"report.print_locked\"],\n\tpayout: [\"payout.view\", \"payout.create\"],\n\tsystem: [\n\t\t\"settings.view\",\n\t\t\"settings.edit\",\n\t\t\"rfid.manage\",\n\t\t\"terminal.manage\",\n\t\t\"holiday.manage\",\n\t\t\"import.run\",\n\t\t\"audit.view\",\n\t\t\"backup.run\",\n\t],\n\tcommand: [\"command.punch\", \"command.close_month\", \"command.recalc\"],\n} as const;\n\nexport const ALL_PERMISSIONS: string[] = Object.values(PERMISSIONS).flat();\n\n/** Role \u2192 permissions (specification section 4.8). */\nexport const ROLE_PERMISSIONS: Record<string, string[]> = {\n\tadmin: ALL_PERMISSIONS,\n\tmanager: [\n\t\t\"time.punch\",\n\t\t\"time.edit_own\",\n\t\t\"time.edit_other\",\n\t\t\"time.delete\",\n\t\t\"time.resolve_conflict\",\n\t\t\"absence.request\",\n\t\t\"absence.approve\",\n\t\t\"absence.edit_other\",\n\t\t\"absence.manage_types\",\n\t\t\"user.view\",\n\t\t\"report.view_own\",\n\t\t\"report.view_other\",\n\t\t\"report.export\",\n\t\t\"report.statistics\",\n\t\t\"report.print_locked\",\n\t\t\"payout.view\",\n\t\t\"settings.view\",\n\t\t\"holiday.manage\",\n\t\t\"audit.view\",\n\t\t\"command.punch\",\n\t\t\"command.close_month\",\n\t\t\"command.recalc\",\n\t],\n\temployee: [\"time.punch\", \"time.edit_own\", \"absence.request\", \"report.view_own\"],\n};\n\n/**\n * Default absence types of the time tracking: code, name, factor, reduces vacation, visible for everybody.\n *\n * \u201CVisible for everybody\u201D (`active`) says whether an employee may pick the type in the app. Sickness, accident and\n * military service start switched off: a company gets those notes on the same day and books them itself, while\n * vacation, further training and the like are requested by the employee.\n */\nexport const ABSENCE_TYPES: {\n\tcode: string;\n\tname: string;\n\tfactor: number;\n\treduceVacation: boolean;\n\tactive: boolean;\n}[] = [\n\t{ code: \"F\", name: \"Ferien\", factor: 100, reduceVacation: true, active: true },\n\t{ code: \"K\", name: \"Krankheit\", factor: 100, reduceVacation: false, active: false },\n\t{ code: \"U\", name: \"Unfall\", factor: 100, reduceVacation: false, active: false },\n\t{ code: \"M\", name: \"Milit\u00E4r\", factor: 100, reduceVacation: false, active: false },\n\t{ code: \"I\", name: \"Intern\", factor: 100, reduceVacation: false, active: true },\n\t{ code: \"W\", name: \"Weiterbildung\", factor: 50, reduceVacation: false, active: true },\n\t{ code: \"E\", name: \"Extern\", factor: 50, reduceVacation: false, active: true },\n];\n\n/** Default instance settings (specification section 2.9.9); existing values are never overwritten. */\nexport const SETTING_DEFAULTS: Record<string, string> = {\n\tedit_window_days: \"7\",\n\tquick_round_minutes: \"0\",\n\tabsence_calc_until_today: \"1\",\n\tabsence_deduct_worktime: \"0\",\n\tprint_limit_days: \"0\",\n\tholiday_country: \"DE\",\n\tdefault_language: \"de\",\n\tsession_ttl_minutes: \"720\",\n\tbackup_retention_days: \"30\",\n\tattendance_list_visible: \"0\",\n\t// how the pause of a day is determined: `auto` = the punched break, otherwise the graduated rules\n\tpause_mode: \"auto\",\n\t// path of a Unicode font for the PDF statement; empty uses the built-in Latin fonts\n\treport_font_path: \"\",\n\t// branding of the installation: logo and background as data URLs (empty = not set), accent colour as hex\n\tbrand_logo: \"\",\n\tbrand_background: \"\",\n\tbrand_color: \"\",\n};\n\n/**\n * Options controlling which initial data is written.\n */\nexport interface SeedOptions {\n\t/** Country used to generate public holidays (default `CH`) */\n\tholidayCountry?: HolidayCountry;\n\t/** Years for which holidays are generated (default: current and next year) */\n\tholidayYears?: number[];\n}\n\n/**\n * Writes all initial data. Idempotent: repeated calls change nothing and never overwrite\n * user-modified settings.\n *\n * @param db - open database handle\n * @param options - seed options\n * @returns summary of what is now present\n */\nexport function seed(db: Db, options: SeedOptions = {}): { permissions: number; holidays: number } {\n\tconst country: HolidayCountry = options.holidayCountry ?? \"DE\";\n\tconst currentYear = new Date().getUTCFullYear();\n\tconst years = options.holidayYears ?? [currentYear, currentYear + 1];\n\n\tconst run = db.transaction((): void => {\n\t\tconst now = Math.floor(Date.now() / 1000);\n\n\t\tconst insertRole = db.prepare(\"INSERT OR IGNORE INTO roles (key, name) VALUES (?, ?)\");\n\t\tinsertRole.run(\"admin\", \"Administrator\");\n\t\tinsertRole.run(\"manager\", \"Manager\");\n\t\tinsertRole.run(\"employee\", \"Employee\");\n\n\t\tconst insertPermission = db.prepare(\"INSERT OR IGNORE INTO permissions (key) VALUES (?)\");\n\t\tfor (const permission of ALL_PERMISSIONS) {\n\t\t\tinsertPermission.run(permission);\n\t\t}\n\n\t\tconst findRole = db.prepare(\"SELECT id FROM roles WHERE key = ?\");\n\t\tconst findPermission = db.prepare(\"SELECT id FROM permissions WHERE key = ?\");\n\t\tconst linkRole = db.prepare(\"INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)\");\n\t\tfor (const [roleKey, permissions] of Object.entries(ROLE_PERMISSIONS)) {\n\t\t\tconst role = findRole.get(roleKey) as { id: number } | undefined;\n\t\t\tif (!role) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const permission of permissions) {\n\t\t\t\tconst row = findPermission.get(permission) as { id: number } | undefined;\n\t\t\t\tif (row) {\n\t\t\t\t\tlinkRole.run(role.id, row.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst insertAbsenceType = db.prepare(\n\t\t\t`INSERT OR IGNORE INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active)\n\t\t\t VALUES (NULL, ?, ?, 1, ?, ?, ?)`,\n\t\t);\n\t\tfor (const type of ABSENCE_TYPES) {\n\t\t\tinsertAbsenceType.run(type.code, type.name, type.factor, type.reduceVacation ? 1 : 0, type.active ? 1 : 0);\n\t\t}\n\n\t\tconst insertSetting = db.prepare(\n\t\t\t\"INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)\",\n\t\t);\n\t\tfor (const [key, value] of Object.entries(SETTING_DEFAULTS)) {\n\t\t\tinsertSetting.run(key, key === \"holiday_country\" ? country : value, now);\n\t\t}\n\t});\n\trun();\n\n\tlet holidayCount = 0;\n\tfor (const year of years) {\n\t\tholidayCount += ensureHolidaysForYear(db, country, year);\n\t}\n\n\tconst permissionCount = (db.prepare(\"SELECT COUNT(*) AS c FROM permissions\").get() as { c: number }).c;\n\treturn { permissions: permissionCount, holidays: holidayCount };\n}\n\n/**\n * Generates and stores the public holidays of one year for a country (idempotent).\n *\n * @param db - open database handle\n * @param country - supported country code\n * @param year - four digit year\n * @returns number of inserted holiday rows\n */\nexport function ensureHolidaysForYear(db: Db, country: HolidayCountry, year: number): number {\n\tconst insert = db.prepare(\"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\");\n\tlet inserted = 0;\n\tconst run = db.transaction((): void => {\n\t\tfor (const holiday of holidaysForYear(year, country)) {\n\t\t\tinserted += insert.run(country, year, holiday.date, holiday.name, holiday.key).changes;\n\t\t}\n\t});\n\trun();\n\treturn inserted;\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,sBAAqD;AAG9C,MAAM,cAAc;AAAA,EAC1B,MAAM,CAAC,cAAc,iBAAiB,mBAAmB,eAAe,eAAe,uBAAuB;AAAA,EAC9G,SAAS,CAAC,mBAAmB,mBAAmB,sBAAsB,sBAAsB;AAAA,EAC5F,MAAM,CAAC,aAAa,eAAe,aAAa,mBAAmB,mBAAmB;AAAA,EACtF,QAAQ,CAAC,mBAAmB,qBAAqB,iBAAiB,qBAAqB,qBAAqB;AAAA,EAC5G,QAAQ,CAAC,eAAe,eAAe;AAAA,EACvC,QAAQ;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AAAA,EACA,SAAS,CAAC,iBAAiB,uBAAuB,gBAAgB;AACnE;AAEO,MAAM,kBAA4B,OAAO,OAAO,WAAW,EAAE,KAAK;AAGlE,MAAM,mBAA6C;AAAA,EACzD,OAAO;AAAA,EACP,SAAS;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AAAA,EACA,UAAU,CAAC,cAAc,iBAAiB,mBAAmB,iBAAiB;AAC/E;AASO,MAAM,gBAMP;AAAA,EACL,EAAE,MAAM,KAAK,MAAM,UAAU,QAAQ,KAAK,gBAAgB,MAAM,QAAQ,KAAK;AAAA,EAC7E,EAAE,MAAM,KAAK,MAAM,aAAa,QAAQ,KAAK,gBAAgB,OAAO,QAAQ,MAAM;AAAA,EAClF,EAAE,MAAM,KAAK,MAAM,UAAU,QAAQ,KAAK,gBAAgB,OAAO,QAAQ,MAAM;AAAA,EAC/E,EAAE,MAAM,KAAK,MAAM,cAAW,QAAQ,KAAK,gBAAgB,OAAO,QAAQ,MAAM;AAAA,EAChF,EAAE,MAAM,KAAK,MAAM,UAAU,QAAQ,KAAK,gBAAgB,OAAO,QAAQ,KAAK;AAAA,EAC9E,EAAE,MAAM,KAAK,MAAM,iBAAiB,QAAQ,IAAI,gBAAgB,OAAO,QAAQ,KAAK;AAAA,EACpF,EAAE,MAAM,KAAK,MAAM,UAAU,QAAQ,IAAI,gBAAgB,OAAO,QAAQ,KAAK;AAC9E;AAGO,MAAM,mBAA2C;AAAA,EACvD,kBAAkB;AAAA,EAClB,qBAAqB;AAAA,EACrB,0BAA0B;AAAA,EAC1B,yBAAyB;AAAA,EACzB,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,qBAAqB;AAAA,EACrB,uBAAuB;AAAA,EACvB,yBAAyB;AAAA;AAAA,EAEzB,YAAY;AAAA;AAAA,EAEZ,kBAAkB;AAAA;AAAA,EAElB,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,aAAa;AACd;AAoBO,SAAS,KAAK,IAAQ,UAAuB,CAAC,GAA8C;AA5HnG;AA6HC,QAAM,WAA0B,aAAQ,mBAAR,YAA0B;AAC1D,QAAM,eAAc,oBAAI,KAAK,GAAE,eAAe;AAC9C,QAAM,SAAQ,aAAQ,iBAAR,YAAwB,CAAC,aAAa,cAAc,CAAC;AAEnE,QAAM,MAAM,GAAG,YAAY,MAAY;AACtC,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAExC,UAAM,aAAa,GAAG,QAAQ,uDAAuD;AACrF,eAAW,IAAI,SAAS,eAAe;AACvC,eAAW,IAAI,WAAW,SAAS;AACnC,eAAW,IAAI,YAAY,UAAU;AAErC,UAAM,mBAAmB,GAAG,QAAQ,oDAAoD;AACxF,eAAW,cAAc,iBAAiB;AACzC,uBAAiB,IAAI,UAAU;AAAA,IAChC;AAEA,UAAM,WAAW,GAAG,QAAQ,oCAAoC;AAChE,UAAM,iBAAiB,GAAG,QAAQ,0CAA0C;AAC5E,UAAM,WAAW,GAAG,QAAQ,+EAA+E;AAC3G,eAAW,CAAC,SAAS,WAAW,KAAK,OAAO,QAAQ,gBAAgB,GAAG;AACtE,YAAM,OAAO,SAAS,IAAI,OAAO;AACjC,UAAI,CAAC,MAAM;AACV;AAAA,MACD;AACA,iBAAW,cAAc,aAAa;AACrC,cAAM,MAAM,eAAe,IAAI,UAAU;AACzC,YAAI,KAAK;AACR,mBAAS,IAAI,KAAK,IAAI,IAAI,EAAE;AAAA,QAC7B;AAAA,MACD;AAAA,IACD;AAEA,UAAM,oBAAoB,GAAG;AAAA,MAC5B;AAAA;AAAA,IAED;AACA,eAAW,QAAQ,eAAe;AACjC,wBAAkB,IAAI,KAAK,MAAM,KAAK,MAAM,KAAK,QAAQ,KAAK,iBAAiB,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC;AAAA,IAC1G;AAEA,UAAM,gBAAgB,GAAG;AAAA,MACxB;AAAA,IACD;AACA,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,gBAAgB,GAAG;AAC5D,oBAAc,IAAI,KAAK,QAAQ,oBAAoB,UAAU,OAAO,GAAG;AAAA,IACxE;AAAA,EACD,CAAC;AACD,MAAI;AAEJ,MAAI,eAAe;AACnB,aAAW,QAAQ,OAAO;AACzB,oBAAgB,sBAAsB,IAAI,SAAS,IAAI;AAAA,EACxD;AAEA,QAAM,kBAAmB,GAAG,QAAQ,uCAAuC,EAAE,IAAI,EAAoB;AACrG,SAAO,EAAE,aAAa,iBAAiB,UAAU,aAAa;AAC/D;AAUO,SAAS,sBAAsB,IAAQ,SAAyB,MAAsB;AAC5F,QAAM,SAAS,GAAG,QAAQ,uFAAuF;AACjH,MAAI,WAAW;AACf,QAAM,MAAM,GAAG,YAAY,MAAY;AACtC,eAAW,eAAW,iCAAgB,MAAM,OAAO,GAAG;AACrD,kBAAY,OAAO,IAAI,SAAS,MAAM,QAAQ,MAAM,QAAQ,MAAM,QAAQ,GAAG,EAAE;AAAA,IAChF;AAAA,EACD,CAAC;AACD,MAAI;AACJ,SAAO;AACR;",
6
6
  "names": []
7
7
  }