iobroker.time-tracker 0.6.0 → 0.7.1

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.
Files changed (48) hide show
  1. package/README.md +55 -26
  2. package/admin/i18n/de.json +1 -1
  3. package/admin/i18n/en.json +1 -1
  4. package/admin/i18n/es.json +1 -1
  5. package/admin/i18n/fr.json +1 -1
  6. package/admin/i18n/it.json +1 -1
  7. package/admin/i18n/nl.json +1 -1
  8. package/admin/i18n/pl.json +1 -1
  9. package/admin/i18n/pt.json +1 -1
  10. package/admin/i18n/ru.json +1 -1
  11. package/admin/i18n/uk.json +1 -1
  12. package/admin/i18n/zh-cn.json +1 -1
  13. package/admin/jsonConfig.json +1 -1
  14. package/build/lib/db/migrations.js +55 -0
  15. package/build/lib/db/migrations.js.map +2 -2
  16. package/build/lib/db/repositories/absences.js +29 -13
  17. package/build/lib/db/repositories/absences.js.map +2 -2
  18. package/build/lib/db/repositories/dayNotes.js +183 -0
  19. package/build/lib/db/repositories/dayNotes.js.map +7 -0
  20. package/build/lib/db/seed.js +18 -11
  21. package/build/lib/db/seed.js.map +2 -2
  22. package/build/lib/services/sync.js +11 -9
  23. package/build/lib/services/sync.js.map +2 -2
  24. package/build/lib/web/api.js +89 -12
  25. package/build/lib/web/api.js.map +2 -2
  26. package/build/lib/web/events.js.map +2 -2
  27. package/build/main.js +4 -0
  28. package/build/main.js.map +2 -2
  29. package/io-package.json +27 -27
  30. package/package.json +1 -1
  31. package/src-pwa/src/i18n/de.json +15 -3
  32. package/src-pwa/src/i18n/en.json +15 -3
  33. package/src-pwa/src/i18n/es.json +15 -3
  34. package/src-pwa/src/i18n/fr.json +15 -3
  35. package/src-pwa/src/i18n/it.json +15 -3
  36. package/src-pwa/src/i18n/nl.json +15 -3
  37. package/src-pwa/src/i18n/pl.json +15 -3
  38. package/src-pwa/src/i18n/pt.json +15 -3
  39. package/src-pwa/src/i18n/ru.json +15 -3
  40. package/src-pwa/src/i18n/uk.json +15 -3
  41. package/src-pwa/src/i18n/zh-cn.json +15 -3
  42. package/www/assets/index-D5fSC8CA.js +119 -0
  43. package/www/assets/index-D5fSC8CA.js.map +1 -0
  44. package/www/index.html +1 -1
  45. package/www/sw.js +1 -1
  46. package/www/sw.js.map +1 -1
  47. package/www/assets/index-DtdFg3O-.js +0 -119
  48. package/www/assets/index-DtdFg3O-.js.map +0 -1
@@ -43,7 +43,7 @@ class UnknownAbsenceTypeError extends Error {
43
43
  this.name = "UnknownAbsenceTypeError";
44
44
  }
45
45
  }
46
- const TYPE_COLUMNS = "id, user_id, code, name, paid, factor, reduce_vacation, is_active";
46
+ const TYPE_COLUMNS = "id, user_id, code, name, paid, factor, reduce_vacation, is_active, color";
47
47
  const ABSENCE_COLUMNS = `id, user_id, type_id, date_from, date_to, day_portion, hours, status, approval, decided_at, decided_by, decision_note, note,
48
48
  created_at, created_by`;
49
49
  const AUDITED_FIELDS = ["typeId", "dateFrom", "dateTo", "dayPortion", "hours", "note"];
@@ -56,7 +56,8 @@ function mapAbsenceTypeRow(row) {
56
56
  paid: row.paid !== 0,
57
57
  factor: row.factor,
58
58
  reduceVacation: row.reduce_vacation !== 0,
59
- isActive: row.is_active !== 0
59
+ isActive: row.is_active !== 0,
60
+ color: row.color
60
61
  };
61
62
  }
62
63
  function mapAbsenceRow(row) {
@@ -137,11 +138,12 @@ function createAbsencesRepository(db) {
137
138
  );
138
139
  const deleteAbsence = db.prepare("DELETE FROM absences WHERE id = ?");
139
140
  const insertType = db.prepare(
140
- `INSERT INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active)
141
- VALUES (?, ?, ?, ?, ?, ?, ?)`
141
+ `INSERT INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active, color)
142
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
142
143
  );
143
144
  const updateType = db.prepare(
144
- `UPDATE absence_types SET name = ?, paid = ?, factor = ?, reduce_vacation = ?, is_active = ? WHERE id = ?`
145
+ `UPDATE absence_types SET name = ?, paid = ?, factor = ?, reduce_vacation = ?, is_active = ?, color = ?
146
+ WHERE id = ?`
145
147
  );
146
148
  const deleteType = db.prepare("DELETE FROM absence_types WHERE id = ?");
147
149
  const countAbsencesOfType = db.prepare("SELECT COUNT(*) AS count FROM absences WHERE type_id = ?");
@@ -191,7 +193,7 @@ function createAbsencesRepository(db) {
191
193
  return resolveType(reference, userId);
192
194
  },
193
195
  upsertType(input) {
194
- var _a, _b, _c, _d, _e, _f, _g;
196
+ var _a, _b, _c, _d, _e, _f, _g, _h;
195
197
  const userId = (_a = input.userId) != null ? _a : null;
196
198
  const code = input.code.trim().toUpperCase();
197
199
  if (!/^[A-Za-z0-9_-]{1,8}$/.test(code)) {
@@ -208,13 +210,25 @@ function createAbsencesRepository(db) {
208
210
  const paid = (_c = input.paid) != null ? _c : true;
209
211
  const reduceVacation = (_d = input.reduceVacation) != null ? _d : false;
210
212
  const isActive = (_e = input.isActive) != null ? _e : true;
211
- const now = (_f = input.now) != null ? _f : Math.floor(Date.now() / 1e3);
213
+ const color = (_f = input.color) != null ? _f : null;
214
+ if (color !== null && !/^#[0-9a-fA-F]{6}$/.test(color)) {
215
+ throw new import_errors.ValidationError(`color must be a hex value like #2e7d32 (got "${input.color}")`);
216
+ }
217
+ const now = (_g = input.now) != null ? _g : Math.floor(Date.now() / 1e3);
212
218
  const existing = resolveType(code, userId);
213
- let typeId = (_g = existing == null ? void 0 : existing.id) != null ? _g : 0;
219
+ let typeId = (_h = existing == null ? void 0 : existing.id) != null ? _h : 0;
214
220
  const run = db.transaction(() => {
215
221
  var _a2;
216
222
  if (existing) {
217
- updateType.run(name, paid ? 1 : 0, factor, reduceVacation ? 1 : 0, isActive ? 1 : 0, existing.id);
223
+ updateType.run(
224
+ name,
225
+ paid ? 1 : 0,
226
+ factor,
227
+ reduceVacation ? 1 : 0,
228
+ isActive ? 1 : 0,
229
+ color,
230
+ existing.id
231
+ );
218
232
  } else {
219
233
  const result = insertType.run(
220
234
  userId,
@@ -223,7 +237,8 @@ function createAbsencesRepository(db) {
223
237
  paid ? 1 : 0,
224
238
  factor,
225
239
  reduceVacation ? 1 : 0,
226
- isActive ? 1 : 0
240
+ isActive ? 1 : 0,
241
+ color
227
242
  );
228
243
  typeId = Number(result.lastInsertRowid);
229
244
  }
@@ -234,11 +249,11 @@ function createAbsencesRepository(db) {
234
249
  action: existing ? "absence_type.update" : "absence_type.create",
235
250
  entity: "absence_types",
236
251
  entityId: typeId,
237
- detail: { code, userId, name, paid, factor, reduceVacation, isActive }
252
+ detail: { code, userId, name, paid, factor, reduceVacation, isActive, color }
238
253
  });
239
254
  });
240
255
  run();
241
- const type = existing ? { ...existing, name, paid, factor, reduceVacation, isActive } : {
256
+ const type = existing ? { ...existing, name, paid, factor, reduceVacation, isActive, color } : {
242
257
  id: typeId,
243
258
  userId,
244
259
  code,
@@ -246,7 +261,8 @@ function createAbsencesRepository(db) {
246
261
  paid,
247
262
  factor,
248
263
  reduceVacation,
249
- isActive
264
+ isActive,
265
+ color
250
266
  };
251
267
  return { type, created: existing === null };
252
268
  },
@@ -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/** 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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",
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\t/** Colour of the type in the calendar (`#rrggbb`), `null` when none is set */\n\tcolor: string | null;\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/** Colour in the calendar (`#rrggbb`), `null` clears it */\n\tcolor?: string | null;\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\tcolor: string | null;\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, color\";\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\tcolor: row.color,\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, color)\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 = ?, color = ?\n\t\t 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 color = input.color ?? null;\n\t\t\tif (color !== null && !/^#[0-9a-fA-F]{6}$/.test(color)) {\n\t\t\t\tthrow new ValidationError(`color must be a hex value like #2e7d32 (got \"${input.color}\")`);\n\t\t\t}\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(\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\tcolor,\n\t\t\t\t\t\texisting.id,\n\t\t\t\t\t);\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\tcolor,\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, color },\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, color }\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\tcolor,\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
  }
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var dayNotes_exports = {};
20
+ __export(dayNotes_exports, {
21
+ MAX_DAY_NOTE_LENGTH: () => MAX_DAY_NOTE_LENGTH,
22
+ createDayNotesRepository: () => createDayNotesRepository
23
+ });
24
+ module.exports = __toCommonJS(dayNotes_exports);
25
+ var import_time = require("../../util/time");
26
+ var import_errors = require("../../errors");
27
+ var import_audit = require("./audit");
28
+ const MAX_DAY_NOTE_LENGTH = 500;
29
+ function createDayNotesRepository(db) {
30
+ const COLUMNS = `id, user_id, local_date, note, created_by, created_at, updated_by, updated_at, handled_at,
31
+ handled_by`;
32
+ const selectByRange = db.prepare(
33
+ `SELECT ${COLUMNS} FROM day_notes WHERE user_id = ? AND local_date BETWEEN ? AND ? ORDER BY local_date, id`
34
+ );
35
+ const selectByDate = db.prepare(`SELECT ${COLUMNS} FROM day_notes WHERE user_id = ? AND local_date = ?`);
36
+ const insert = db.prepare(
37
+ `INSERT INTO day_notes (user_id, local_date, note, created_by, created_at, updated_by, updated_at)
38
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
39
+ );
40
+ const updateText = db.prepare(`UPDATE day_notes SET note = ?, updated_by = ?, updated_at = ? WHERE id = ?`);
41
+ const removeRow = db.prepare(`DELETE FROM day_notes WHERE id = ?`);
42
+ const updateHandled = db.prepare(
43
+ `UPDATE day_notes SET handled_at = ?, handled_by = ?, updated_by = ?, updated_at = ? WHERE id = ?`
44
+ );
45
+ const mapRow = (row) => ({
46
+ id: row.id,
47
+ userId: row.user_id,
48
+ localDate: row.local_date,
49
+ note: row.note,
50
+ createdBy: row.created_by,
51
+ createdAt: row.created_at,
52
+ updatedBy: row.updated_by,
53
+ updatedAt: row.updated_at,
54
+ handledAt: row.handled_at,
55
+ handledBy: row.handled_by
56
+ });
57
+ const read = (userId, localDate) => {
58
+ const row = selectByDate.get(userId, localDate);
59
+ return row ? mapRow(row) : null;
60
+ };
61
+ const requireDate = (localDate) => {
62
+ if (!(0, import_time.isDateString)(localDate)) {
63
+ throw new import_errors.ValidationError("localDate must be a date (YYYY-MM-DD)");
64
+ }
65
+ };
66
+ return {
67
+ listByRange(userId, fromDate, toDate) {
68
+ const rows = selectByRange.all(userId, fromDate, toDate);
69
+ return rows.map(mapRow);
70
+ },
71
+ find: read,
72
+ save(input) {
73
+ var _a;
74
+ requireDate(input.localDate);
75
+ const note = input.note.trim();
76
+ if (note.length > MAX_DAY_NOTE_LENGTH) {
77
+ throw new import_errors.ValidationError(
78
+ `note must not be longer than ${MAX_DAY_NOTE_LENGTH} characters (got ${note.length})`
79
+ );
80
+ }
81
+ const atUtc = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
82
+ const existing = read(input.userId, input.localDate);
83
+ if (note === "") {
84
+ if (!existing) {
85
+ return null;
86
+ }
87
+ const run2 = db.transaction(() => {
88
+ var _a2;
89
+ removeRow.run(existing.id);
90
+ (0, import_audit.writeAuditLog)(db, {
91
+ atUtc,
92
+ actorId: input.actorId,
93
+ action: "day_note.delete",
94
+ entity: "day_notes",
95
+ entityId: existing.id,
96
+ detail: { userId: input.userId, localDate: input.localDate, note: existing.note },
97
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
98
+ });
99
+ });
100
+ run2();
101
+ return null;
102
+ }
103
+ if (existing && existing.note === note) {
104
+ return existing;
105
+ }
106
+ const run = db.transaction(() => {
107
+ var _a2, _b;
108
+ if (existing) {
109
+ updateText.run(note, input.actorId, atUtc, existing.id);
110
+ (0, import_audit.writeAuditLog)(db, {
111
+ atUtc,
112
+ actorId: input.actorId,
113
+ action: "day_note.update",
114
+ entity: "day_notes",
115
+ entityId: existing.id,
116
+ detail: {
117
+ userId: input.userId,
118
+ localDate: input.localDate,
119
+ old: existing.note,
120
+ new: note
121
+ },
122
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
123
+ });
124
+ return;
125
+ }
126
+ const result = insert.run(
127
+ input.userId,
128
+ input.localDate,
129
+ note,
130
+ input.actorId,
131
+ atUtc,
132
+ input.actorId,
133
+ atUtc
134
+ );
135
+ (0, import_audit.writeAuditLog)(db, {
136
+ atUtc,
137
+ actorId: input.actorId,
138
+ action: "day_note.create",
139
+ entity: "day_notes",
140
+ entityId: Number(result.lastInsertRowid),
141
+ detail: { userId: input.userId, localDate: input.localDate, note },
142
+ ip: (_b = input.actorIp) != null ? _b : null
143
+ });
144
+ });
145
+ run();
146
+ return read(input.userId, input.localDate);
147
+ },
148
+ setHandled(input) {
149
+ var _a, _b;
150
+ requireDate(input.localDate);
151
+ const existing = read(input.userId, input.localDate);
152
+ if (!existing) {
153
+ throw new import_errors.NotFoundError(`day note of ${input.localDate} not found`);
154
+ }
155
+ const atUtc = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
156
+ const wanted = input.handled ? atUtc : null;
157
+ if (existing.handledAt === wanted) {
158
+ return existing;
159
+ }
160
+ const run = db.transaction(() => {
161
+ var _a2;
162
+ updateHandled.run(wanted, wanted === null ? null : input.actorId, input.actorId, atUtc, existing.id);
163
+ (0, import_audit.writeAuditLog)(db, {
164
+ atUtc,
165
+ actorId: input.actorId,
166
+ action: "day_note.handled",
167
+ entity: "day_notes",
168
+ entityId: existing.id,
169
+ detail: { userId: input.userId, localDate: input.localDate, handled: input.handled },
170
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
171
+ });
172
+ });
173
+ run();
174
+ return (_b = read(input.userId, input.localDate)) != null ? _b : existing;
175
+ }
176
+ };
177
+ }
178
+ // Annotate the CommonJS export names for ESM import in node:
179
+ 0 && (module.exports = {
180
+ MAX_DAY_NOTE_LENGTH,
181
+ createDayNotesRepository
182
+ });
183
+ //# sourceMappingURL=dayNotes.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/db/repositories/dayNotes.ts"],
4
+ "sourcesContent": ["/**\n * Day notes (`day_notes`): what an employee wants the administration to know about one day.\n *\n * An employee does not change punches; the day of a forgotten punch is corrected by the administration. What the\n * employee may do is leave a note like \u201Cforgot to clock in or out\u201D. That message lives outside the punches, because\n * a day without a punch is exactly the case it is meant for. One note per employee and day: writing a second one\n * replaces the first, an empty text takes it back. The administration marks a note as handled and keeps it.\n */\n\nimport type { Db } from \"../database\";\nimport { isDateString } from \"../../util/time\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { writeAuditLog } from \"./audit\";\n\n/** Longest text a note may carry. */\nexport const MAX_DAY_NOTE_LENGTH = 500;\n\n/** A note an employee left for one of his days. */\nexport interface DayNoteRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Owner of the day */\n\tuserId: number;\n\t/** Local date the note belongs to (`YYYY-MM-DD`) */\n\tlocalDate: string;\n\t/** Text of the note */\n\tnote: string;\n\t/** Who wrote it */\n\tcreatedBy: number | null;\n\t/** Instant it was written, UTC epoch seconds */\n\tcreatedAt: number;\n\t/** Who changed it last */\n\tupdatedBy: number | null;\n\t/** Instant of the last change, UTC epoch seconds */\n\tupdatedAt: number;\n\t/** Instant the administration marked it as handled, `null` while it waits */\n\thandledAt: number | null;\n\t/** Who marked it as handled */\n\thandledBy: number | null;\n}\n\n/** Input for writing (or clearing) the note of one day. */\nexport interface SaveDayNoteInput {\n\t/** Owner of the day */\n\tuserId: number;\n\t/** Local date the note belongs to */\n\tlocalDate: string;\n\t/** Text of the note; an empty text removes the stored note */\n\tnote: string;\n\t/** Who writes it */\n\tactorId: number;\n\t/** Client IP address */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** Input for marking a note as handled (or open again). */\nexport interface HandleDayNoteInput {\n\t/** Owner of the day */\n\tuserId: number;\n\t/** Local date the note belongs to */\n\tlocalDate: string;\n\t/** True marks it as handled, false puts it back into the list of open notes */\n\thandled: boolean;\n\t/** Who decides */\n\tactorId: number;\n\t/** Client IP address */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** Typed access to the day notes. */\nexport interface DayNotesRepository {\n\t/** Notes of one employee in a local date range (inclusive), oldest first */\n\tlistByRange(userId: number, fromDate: string, toDate: string): DayNoteRecord[];\n\t/** Note of one day or `null` */\n\tfind(userId: number, localDate: string): DayNoteRecord | null;\n\t/**\n\t * Writes the note of one day.\n\t *\n\t * @param input - owner, day, text and actor\n\t * @returns the stored note, `null` when an empty text removed it\n\t */\n\tsave(input: SaveDayNoteInput): DayNoteRecord | null;\n\t/**\n\t * Marks a note as handled or open again.\n\t *\n\t * @param input - owner, day and decision\n\t * @returns the updated note\n\t */\n\tsetHandled(input: HandleDayNoteInput): DayNoteRecord;\n}\n\n/** One row of `day_notes` as it is stored. */\ninterface DayNoteRow {\n\tid: number;\n\tuser_id: number;\n\tlocal_date: string;\n\tnote: string;\n\tcreated_by: number | null;\n\tcreated_at: number;\n\tupdated_by: number | null;\n\tupdated_at: number;\n\thandled_at: number | null;\n\thandled_by: number | null;\n}\n\n/**\n * Creates the repository of the day notes.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createDayNotesRepository(db: Db): DayNotesRepository {\n\tconst COLUMNS = `id, user_id, local_date, note, created_by, created_at, updated_by, updated_at, handled_at,\n\t\thandled_by`;\n\tconst selectByRange = db.prepare(\n\t\t`SELECT ${COLUMNS} FROM day_notes WHERE user_id = ? AND local_date BETWEEN ? AND ? ORDER BY local_date, id`,\n\t);\n\tconst selectByDate = db.prepare(`SELECT ${COLUMNS} FROM day_notes WHERE user_id = ? AND local_date = ?`);\n\tconst insert = db.prepare(\n\t\t`INSERT INTO day_notes (user_id, local_date, note, created_by, created_at, updated_by, updated_at)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateText = db.prepare(`UPDATE day_notes SET note = ?, updated_by = ?, updated_at = ? WHERE id = ?`);\n\tconst removeRow = db.prepare(`DELETE FROM day_notes WHERE id = ?`);\n\tconst updateHandled = db.prepare(\n\t\t`UPDATE day_notes SET handled_at = ?, handled_by = ?, updated_by = ?, updated_at = ? WHERE id = ?`,\n\t);\n\n\t/**\n\t * Turns a stored row into the record the API returns.\n\t *\n\t * @param row - stored row\n\t * @returns the record\n\t */\n\tconst mapRow = (row: DayNoteRow): DayNoteRecord => ({\n\t\tid: row.id,\n\t\tuserId: row.user_id,\n\t\tlocalDate: row.local_date,\n\t\tnote: row.note,\n\t\tcreatedBy: row.created_by,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedBy: row.updated_by,\n\t\tupdatedAt: row.updated_at,\n\t\thandledAt: row.handled_at,\n\t\thandledBy: row.handled_by,\n\t});\n\n\t/**\n\t * Reads the note of one day.\n\t *\n\t * @param userId - owner of the day\n\t * @param localDate - the day\n\t * @returns the note or `null`\n\t */\n\tconst read = (userId: number, localDate: string): DayNoteRecord | null => {\n\t\tconst row = selectByDate.get(userId, localDate) as DayNoteRow | undefined;\n\t\treturn row ? mapRow(row) : null;\n\t};\n\n\t/**\n\t * Checks the day a note belongs to.\n\t *\n\t * @param localDate - the day\n\t */\n\tconst requireDate = (localDate: string): void => {\n\t\tif (!isDateString(localDate)) {\n\t\t\tthrow new ValidationError(\"localDate must be a date (YYYY-MM-DD)\");\n\t\t}\n\t};\n\n\treturn {\n\t\tlistByRange(userId: number, fromDate: string, toDate: string): DayNoteRecord[] {\n\t\t\tconst rows = selectByRange.all(userId, fromDate, toDate) as DayNoteRow[];\n\t\t\treturn rows.map(mapRow);\n\t\t},\n\n\t\tfind: read,\n\n\t\tsave(input: SaveDayNoteInput): DayNoteRecord | null {\n\t\t\trequireDate(input.localDate);\n\t\t\tconst note = input.note.trim();\n\t\t\tif (note.length > MAX_DAY_NOTE_LENGTH) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t`note must not be longer than ${MAX_DAY_NOTE_LENGTH} characters (got ${note.length})`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst atUtc = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst existing = read(input.userId, input.localDate);\n\n\t\t\t// an empty text is how a note is taken back\n\t\t\tif (note === \"\") {\n\t\t\t\tif (!existing) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst run = db.transaction((): void => {\n\t\t\t\t\tremoveRow.run(existing.id);\n\t\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\t\tatUtc,\n\t\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\t\taction: \"day_note.delete\",\n\t\t\t\t\t\tentity: \"day_notes\",\n\t\t\t\t\t\tentityId: existing.id,\n\t\t\t\t\t\tdetail: { userId: input.userId, localDate: input.localDate, note: existing.note },\n\t\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\trun();\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif (existing && existing.note === note) {\n\t\t\t\treturn existing;\n\t\t\t}\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tif (existing) {\n\t\t\t\t\tupdateText.run(note, input.actorId, atUtc, existing.id);\n\t\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\t\tatUtc,\n\t\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\t\taction: \"day_note.update\",\n\t\t\t\t\t\tentity: \"day_notes\",\n\t\t\t\t\t\tentityId: existing.id,\n\t\t\t\t\t\tdetail: {\n\t\t\t\t\t\t\tuserId: input.userId,\n\t\t\t\t\t\t\tlocalDate: input.localDate,\n\t\t\t\t\t\t\told: existing.note,\n\t\t\t\t\t\t\tnew: note,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = insert.run(\n\t\t\t\t\tinput.userId,\n\t\t\t\t\tinput.localDate,\n\t\t\t\t\tnote,\n\t\t\t\t\tinput.actorId,\n\t\t\t\t\tatUtc,\n\t\t\t\t\tinput.actorId,\n\t\t\t\t\tatUtc,\n\t\t\t\t);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"day_note.create\",\n\t\t\t\t\tentity: \"day_notes\",\n\t\t\t\t\tentityId: Number(result.lastInsertRowid),\n\t\t\t\t\tdetail: { userId: input.userId, localDate: input.localDate, note },\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 read(input.userId, input.localDate);\n\t\t},\n\n\t\tsetHandled(input: HandleDayNoteInput): DayNoteRecord {\n\t\t\trequireDate(input.localDate);\n\t\t\tconst existing = read(input.userId, input.localDate);\n\t\t\tif (!existing) {\n\t\t\t\tthrow new NotFoundError(`day note of ${input.localDate} not found`);\n\t\t\t}\n\t\t\tconst atUtc = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst wanted = input.handled ? atUtc : null;\n\t\t\tif (existing.handledAt === wanted) {\n\t\t\t\treturn existing;\n\t\t\t}\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupdateHandled.run(wanted, wanted === null ? null : input.actorId, input.actorId, atUtc, existing.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"day_note.handled\",\n\t\t\t\t\tentity: \"day_notes\",\n\t\t\t\t\tentityId: existing.id,\n\t\t\t\t\tdetail: { userId: input.userId, localDate: input.localDate, handled: input.handled },\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\treturn read(input.userId, input.localDate) ?? existing;\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["run", "_a"]
7
+ }
@@ -48,7 +48,7 @@ const PERMISSIONS = {
48
48
  };
49
49
  const ALL_PERMISSIONS = Object.values(PERMISSIONS).flat();
50
50
  const ROLE_PERMISSIONS = {
51
- admin: ALL_PERMISSIONS,
51
+ admin: ALL_PERMISSIONS.filter((permission) => permission !== "time.punch"),
52
52
  manager: [
53
53
  "time.punch",
54
54
  "time.edit_own",
@@ -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, 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 }
79
+ { code: "F", name: "Ferien", factor: 100, reduceVacation: true, active: true, color: "#2e7d32" },
80
+ { code: "K", name: "Krankheit", factor: 100, reduceVacation: false, active: false, color: "#c62828" },
81
+ { code: "U", name: "Unfall", factor: 100, reduceVacation: false, active: false, color: "#ef6c00" },
82
+ { code: "M", name: "Milit\xE4r", factor: 100, reduceVacation: false, active: false, color: "#455a64" },
83
+ { code: "I", name: "Intern", factor: 100, reduceVacation: false, active: true, color: "#1565c0" },
84
+ { code: "W", name: "Weiterbildung", factor: 50, reduceVacation: false, active: true, color: "#6a1b9a" },
85
+ { code: "E", name: "Extern", factor: 50, reduceVacation: false, active: true, color: "#795548" }
86
86
  ];
87
87
  const SETTING_DEFAULTS = {
88
88
  edit_window_days: "7",
@@ -135,11 +135,18 @@ function seed(db, options = {}) {
135
135
  }
136
136
  }
137
137
  const insertAbsenceType = db.prepare(
138
- `INSERT OR IGNORE INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active)
139
- VALUES (NULL, ?, ?, 1, ?, ?, ?)`
138
+ `INSERT OR IGNORE INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active, color)
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, type.active ? 1 : 0);
142
+ insertAbsenceType.run(
143
+ type.code,
144
+ type.name,
145
+ type.factor,
146
+ type.reduceVacation ? 1 : 0,
147
+ type.active ? 1 : 0,
148
+ type.color
149
+ );
143
150
  }
144
151
  const insertSetting = db.prepare(
145
152
  "INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)"