iobroker.time-tracker 0.7.7 → 0.7.9
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 +44 -35
- package/build/lib/db/migrations.js +12 -0
- package/build/lib/db/migrations.js.map +2 -2
- package/build/lib/db/repositories/absences.js +87 -4
- package/build/lib/db/repositories/absences.js.map +2 -2
- package/build/lib/db/repositories/automations.js +7 -0
- package/build/lib/db/repositories/automations.js.map +2 -2
- package/build/lib/db/repositories/holidays.js +5 -1
- package/build/lib/db/repositories/holidays.js.map +2 -2
- package/build/lib/web/api.js +63 -4
- package/build/lib/web/api.js.map +2 -2
- package/io-package.json +27 -27
- package/package.json +1 -1
- package/src-www/src/i18n/de.json +24 -1
- package/src-www/src/i18n/en.json +24 -1
- package/src-www/src/i18n/es.json +24 -1
- package/src-www/src/i18n/fr.json +24 -1
- package/src-www/src/i18n/it.json +24 -1
- package/src-www/src/i18n/nl.json +24 -1
- package/src-www/src/i18n/pl.json +24 -1
- package/src-www/src/i18n/pt.json +24 -1
- package/src-www/src/i18n/ru.json +24 -1
- package/src-www/src/i18n/uk.json +24 -1
- package/src-www/src/i18n/zh-cn.json +24 -1
- package/www/assets/index-D5c1Xz-S.js +119 -0
- package/www/assets/index-D5c1Xz-S.js.map +1 -0
- package/www/index.html +1 -1
- package/www/sw.js +1 -1
- package/www/sw.js.map +1 -1
- package/www/assets/index-hqZFDQvU.js +0 -119
- package/www/assets/index-hqZFDQvU.js.map +0 -1
|
@@ -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\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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",
|
|
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 * A request of an employee is decided by the administration (`approval`); an absence that is already approved is not\n * cancelled silently either \u2014 `cancel_requested_at` notes the wish, and the administration deletes the absence or\n * declines the request. The absence keeps counting while the cancellation waits.\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/** Instant the employee asked for the cancellation of an approved absence, `null` while none is open */\n\tcancelRequestedAt: number | null;\n\t/** Reason the employee gave for that cancellation request */\n\tcancelNote: 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/** Notes that the employee wants an approved absence cancelled and audits it */\n\trequestCancel(input: {\n\t\tid: number;\n\t\tnote?: string | null;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): AbsenceRecord;\n\t/** Ends a pending cancellation request: the employee withdraws it or the administration declines it */\n\tclearCancel(input: {\n\t\tid: number;\n\t\t/** True when the administration declined the request instead of the employee withdrawing it */\n\t\tdeclined?: boolean;\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\tcancel_requested_at: number | null;\n\tcancel_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, cancel_requested_at, cancel_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\tcancelRequestedAt: row.cancel_requested_at,\n\t\tcancelNote: row.cancel_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.cancel_requested_at, a.cancel_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.cancel_requested_at, a.cancel_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 updateCancelRequest = db.prepare(\"UPDATE absences SET cancel_requested_at = ?, cancel_note = ? WHERE id = ?\");\n\tconst clearCancelRequest = db.prepare(\n\t\t\"UPDATE absences SET cancel_requested_at = NULL, cancel_note = NULL 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\tif (input.approval !== \"approved\") {\n\t\t\t\t\t// the days stop counting with this decision, so a cancellation that waits for one is pointless\n\t\t\t\t\tclearCancelRequest.run(input.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.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\trequestCancel(input: {\n\t\t\tid: number;\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\t// only an approved absence is cancelled: a request that still waits for its decision is withdrawn\n\t\t\tif (current.approval !== \"approved\") {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t`only an approved absence can be cancelled (approval is ${current.approval})`,\n\t\t\t\t);\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\tupdateCancelRequest.run(now, 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.cancel_request\",\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\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 cancellation request`);\n\t\t\t}\n\t\t\treturn updated;\n\t\t},\n\n\t\tclearCancel(input: {\n\t\t\tid: number;\n\t\t\tdeclined?: boolean;\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\tif (current.cancelRequestedAt === null) {\n\t\t\t\tthrow new ValidationError(`absence ${input.id} has no open cancellation request`);\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\tclearCancelRequest.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: input.declined ? \"absence.cancel_decline\" : \"absence.cancel_withdraw\",\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\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 cancellation was taken back`);\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\t// the log then shows whether this deletion was the answer to a cancellation request\n\t\t\t\t\t\tcancelRequested: current.cancelRequestedAt !== null,\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
|
}
|
|
@@ -162,6 +162,10 @@ function createAutomationsRepository(db) {
|
|
|
162
162
|
const selectAllRuns = db.prepare(
|
|
163
163
|
`SELECT ${RUN_COLUMNS} FROM automation_runs ORDER BY fired_at DESC, rule_id LIMIT ?`
|
|
164
164
|
);
|
|
165
|
+
const selectLatestRuns = db.prepare(
|
|
166
|
+
`SELECT rule_id, user_id, period, MAX(fired_at) AS fired_at, action
|
|
167
|
+
FROM automation_runs GROUP BY rule_id ORDER BY fired_at DESC, rule_id`
|
|
168
|
+
);
|
|
165
169
|
const selectRun = db.prepare(
|
|
166
170
|
"SELECT 1 AS found FROM automation_runs WHERE rule_id = ? AND user_id = ? AND period = ?"
|
|
167
171
|
);
|
|
@@ -325,6 +329,9 @@ function createAutomationsRepository(db) {
|
|
|
325
329
|
);
|
|
326
330
|
return rows.map(mapAutomationRunRow);
|
|
327
331
|
},
|
|
332
|
+
latestRuns() {
|
|
333
|
+
return selectLatestRuns.all().map(mapAutomationRunRow);
|
|
334
|
+
},
|
|
328
335
|
hasRun(input) {
|
|
329
336
|
return selectRun.get(input.ruleId, input.userId, input.period) !== void 0;
|
|
330
337
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/lib/db/repositories/automations.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Automation rules (`automation_rules`, `automation_runs`).\n *\n * The rules of the adapter itself \u2014 not of a foreign state like the trigger rules. Three kinds are supported:\n *\n * - `clockOut`: at a certain local time the adapter punches out an employee who is still clocked in. The punch is\n * written like any other one (`source: \"api\"`, note `auto.clock_out`), so it appears in the day, in the report\n * and in the audit trail \u2014 the employee can correct it afterwards like any other punch.\n * - `missingPunch`: the same moment, but the adapter only reports it. Nothing is written, so a shop that wants to\n * decide case by case gets a warning instead of a silent correction.\n * - `breakReminder`: an employee whose running work block reached the configured length gets a reminder \u2014 after a\n * break, the block starts again.\n *\n * `user_id` is `NULL` for a company rule and set for a rule of one employee. A rule runs at most once per employee\n * and local date: `automation_runs` holds that decision, which also gives the administration a small log (`action`\n * and `fired_at`) to look at.\n */\n\nimport type { Db } from \"../database\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { diffFields, writeAuditLog } from \"./audit\";\n\n/** Kinds an automation rule can have. */\nexport type AutomationKind = \"clockIn\" | \"clockOut\" | \"missingPunch\" | \"breakReminder\";\n\n/** How often a rule may act for one employee. */\nexport type AutomationRepeat = \"day\" | \"week\";\n\n/** An automation rule as it is stored. */\nexport interface AutomationRuleRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Free-form label, shown in the administration */\n\tlabel: string | null;\n\t/** What the rule does */\n\tkind: AutomationKind;\n\t/** Employee the rule applies to, `null` for every employee */\n\tuserId: number | null;\n\t/** Minute of the local day the rule acts at (kinds `clockOut` and `missingPunch`) */\n\tatMinute: number | null;\n\t/** Length of the running work block in minutes (kind `breakReminder`) */\n\tafterMinutes: number | null;\n\t/** Days of the week the rule runs on, ISO numbers 1 (Monday) to 7 (Sunday) */\n\tweekdays: number[];\n\t/** Whether the rule may act once a day or once an ISO week per employee */\n\trepeat: AutomationRepeat;\n\t/** Inactive rules are ignored but keep their history */\n\tisActive: boolean;\n\t/** First local day the rule is valid on (`YYYY-MM-DD`), `null` for \u201Cfrom now on\u201D */\n\tactiveFrom: string | null;\n\t/** Last local day the rule is valid on (`YYYY-MM-DD`, inclusive), `null` for \u201Cwithout an end\u201D */\n\tactiveUntil: string | null;\n}\n\n/** Input for creating or changing a rule. */\nexport interface SaveAutomationRuleInput {\n\t/** Id of an existing rule, omitted when a new rule is created */\n\tid?: number;\n\t/** Free-form label */\n\tlabel?: string | null;\n\t/** What the rule does */\n\tkind: AutomationKind;\n\t/** Employee the rule applies to, `null` or omitted for every employee */\n\tuserId?: number | null;\n\t/** Minute of the local day (kinds `clockOut` and `missingPunch`) */\n\tatMinute?: number | null;\n\t/** Length of the running work block in minutes (kind `breakReminder`) */\n\tafterMinutes?: number | null;\n\t/** Days of the week the rule runs on (ISO 1..7), omitted keeps the stored selection */\n\tweekdays?: number[];\n\t/** How often the rule may act, omitted keeps the stored value */\n\trepeat?: AutomationRepeat;\n\t/** `false` disables the rule without deleting it */\n\tisActive?: boolean;\n\t/** First local day the rule is valid on, omitted keeps the stored value, `null`/empty clears it */\n\tactiveFrom?: string | null;\n\t/** Last local day the rule is valid on (inclusive), omitted keeps the stored value, `null`/empty clears it */\n\tactiveUntil?: string | null;\n\t/** Who changes the rule */\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/** One run of a rule. */\nexport interface AutomationRunRecord {\n\t/** Rule that ran */\n\truleId: number;\n\t/** Employee it ran for */\n\tuserId: number;\n\t/** Period it ran in: the local date, or the ISO week (`2026-W38`) for a weekly rule */\n\tperiod: string;\n\t/** Instant it ran */\n\tfiredAt: number;\n\t/** Short description of what happened */\n\taction: string;\n}\n\n/** Automation rule storage operations. */\nexport interface AutomationsRepository {\n\t/** All rules, optionally including the disabled ones */\n\tlist(options?: { includeInactive?: boolean }): AutomationRuleRecord[];\n\t/** Reads one rule */\n\tfindById(id: number): AutomationRuleRecord | null;\n\t/** Creates or updates a rule */\n\tsave(input: SaveAutomationRuleInput): AutomationRuleRecord;\n\t/** Deletes a rule (its runs go with it) */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n\t/** Runs of the rules, newest first */\n\truns(input?: { ruleId?: number; period?: string; limit?: number }): AutomationRunRecord[];\n\t/** True when the rule already ran for that employee in that period */\n\thasRun(input: { ruleId: number; userId: number; period: string }): boolean;\n\t/** Notes a run; a second call for the same rule, employee and period changes nothing and returns `false` */\n\trecordRun(input: { ruleId: number; userId: number; period: string; action: string; now?: number }): boolean;\n\t/** Forgets a run again \u2014 used when an action failed, so the next check can retry it */\n\tforgetRun(input: { ruleId: number; userId: number; period: string }): boolean;\n}\n\n/** Raw database row of a rule. */\ninterface AutomationRuleRow {\n\tid: number;\n\tlabel: string | null;\n\tkind: string;\n\tuser_id: number | null;\n\tat_minute: number | null;\n\tafter_minutes: number | null;\n\tweekdays: number;\n\trepeat: string;\n\tis_active: number;\n\tactive_from: string | null;\n\tactive_until: string | null;\n}\n\n/** Raw database row of a run. */\ninterface AutomationRunRow {\n\trule_id: number;\n\tuser_id: number;\n\tperiod: string;\n\tfired_at: number;\n\taction: string;\n}\n\nconst RULE_COLUMNS =\n\t\"id, label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, active_from, active_until\";\nconst RUN_COLUMNS = \"rule_id, user_id, period, fired_at, action\";\n\n/**\n * Fields of a rule that are compared for the audit trail.\n *\n * `weekdays` is not in here: it is a list, and the comparison of the audit trail works on single values. The saved\n * selection is written to the audit detail separately.\n */\nconst AUDITED_FIELDS: (keyof AutomationRuleRecord)[] = [\n\t\"label\",\n\t\"kind\",\n\t\"userId\",\n\t\"atMinute\",\n\t\"afterMinutes\",\n\t\"repeat\",\n\t\"isActive\",\n\t\"activeFrom\",\n\t\"activeUntil\",\n];\n\n/** Kinds a rule may use. */\nconst KINDS: AutomationKind[] = [\"clockIn\", \"clockOut\", \"missingPunch\", \"breakReminder\"];\n\n/** Kinds that act at a fixed local time. */\nconst TIMED_KINDS: AutomationKind[] = [\"clockIn\", \"clockOut\", \"missingPunch\"];\n\n/**\n * Maps a database row to a rule record.\n *\n * @param row - raw database row\n * @returns rule record\n */\nexport function mapAutomationRuleRow(row: AutomationRuleRow): AutomationRuleRecord {\n\treturn {\n\t\tid: row.id,\n\t\tlabel: row.label,\n\t\tkind: KINDS.includes(row.kind as AutomationKind) ? (row.kind as AutomationKind) : \"clockOut\",\n\t\tuserId: row.user_id,\n\t\tatMinute: row.at_minute,\n\t\tafterMinutes: row.after_minutes,\n\t\t// a row written before the migration has no selection: every day is the sensible default\n\t\tweekdays: maskToWeekdays(row.weekdays ?? 127),\n\t\trepeat: row.repeat === \"week\" ? \"week\" : \"day\",\n\t\tisActive: row.is_active !== 0,\n\t\tactiveFrom: row.active_from ?? null,\n\t\tactiveUntil: row.active_until ?? null,\n\t};\n}\n\n/**\n * Maps a database row to a run record.\n *\n * @param row - raw database row\n * @returns run record\n */\nexport function mapAutomationRunRow(row: AutomationRunRow): AutomationRunRecord {\n\treturn {\n\t\truleId: row.rule_id,\n\t\tuserId: row.user_id,\n\t\tperiod: row.period,\n\t\tfiredAt: row.fired_at,\n\t\taction: row.action,\n\t};\n}\n\n/**\n * Validates the kind of a rule.\n *\n * @param value - kind from the request\n * @returns the validated kind\n */\nfunction requireKind(value: string): AutomationKind {\n\tif (!KINDS.includes(value as AutomationKind)) {\n\t\tthrow new ValidationError(`kind must be one of ${KINDS.join(\", \")} (got ${value})`);\n\t}\n\treturn value as AutomationKind;\n}\n\n/**\n * Validates the time of a rule: a timed kind needs a minute of the day, the reminder needs a length.\n *\n * @param kind - kind of the rule\n * @param atMinute - minute of the local day\n * @param afterMinutes - length of the running work block\n * @returns the validated values\n */\nfunction requireWindow(\n\tkind: AutomationKind,\n\tatMinute: number | null,\n\tafterMinutes: number | null,\n): { atMinute: number | null; afterMinutes: number | null } {\n\tif (TIMED_KINDS.includes(kind)) {\n\t\tif (atMinute === null || !Number.isInteger(atMinute) || atMinute < 0 || atMinute > 1439) {\n\t\t\tthrow new ValidationError(`atMinute must be a minute of the day between 0 and 1439 (got ${atMinute})`);\n\t\t}\n\t\treturn { atMinute, afterMinutes: null };\n\t}\n\n\tif (afterMinutes === null || !Number.isInteger(afterMinutes) || afterMinutes < 1 || afterMinutes > 1440) {\n\t\tthrow new ValidationError(`afterMinutes must be between 1 and 1440 (got ${afterMinutes})`);\n\t}\n\treturn { atMinute: null, afterMinutes };\n}\n\n/**\n * Validates one end of the validity period of a rule.\n *\n * `undefined` keeps the stored value, an empty string or `null` clears the limit (that is the \u201Cfrom now on\u201D and\n * \u201Cwithout an end\u201D of the dialog), anything else has to be a real calendar date in `YYYY-MM-DD`.\n *\n * @param value - value from the request\n * @param fallback - value to keep when the request says nothing\n * @param field - field name for the error message\n * @returns the validated date or `null`\n */\nfunction requireValidityDate(value: string | null | undefined, fallback: string | null, field: string): string | null {\n\tif (value === undefined) {\n\t\treturn fallback;\n\t}\n\tconst text = (value ?? \"\").trim();\n\tif (text === \"\") {\n\t\treturn null;\n\t}\n\tconst parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(text);\n\tconst date = parts ? new Date(Date.UTC(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))) : null;\n\tif (\n\t\t!parts ||\n\t\t!date ||\n\t\tdate.getUTCFullYear() !== Number(parts[1]) ||\n\t\tdate.getUTCMonth() !== Number(parts[2]) - 1 ||\n\t\tdate.getUTCDate() !== Number(parts[3])\n\t) {\n\t\tthrow new ValidationError(`${field} must be a date like 2026-10-01 (got ${text})`);\n\t}\n\treturn text;\n}\n\n/** Days a rule runs on when nothing else is chosen. */\nexport const ALL_WEEKDAYS: number[] = [1, 2, 3, 4, 5, 6, 7];\n\n/**\n * Turns the stored bitmask into ISO weekdays.\n *\n * @param mask - bits 0..6, bit 0 is Monday\n * @returns the days, at least every day when the mask is empty\n */\nexport function maskToWeekdays(mask: number): number[] {\n\tconst days: number[] = [];\n\tfor (let day = 1; day <= 7; day += 1) {\n\t\tif ((mask & (1 << (day - 1))) !== 0) {\n\t\t\tdays.push(day);\n\t\t}\n\t}\n\treturn days.length > 0 ? days : [...ALL_WEEKDAYS];\n}\n\n/**\n * Turns ISO weekdays into the stored bitmask.\n *\n * @param days - the days, ISO numbers 1..7\n * @returns the mask\n */\nexport function weekdaysToMask(days: number[]): number {\n\tlet mask = 0;\n\tfor (const day of days) {\n\t\tmask |= 1 << (day - 1);\n\t}\n\treturn mask;\n}\n\n/**\n * Validates the weekday selection of a rule.\n *\n * @param value - selection from the request, `undefined` keeps the stored one\n * @param fallback - the stored selection\n * @returns the validated, sorted selection\n */\nfunction requireWeekdays(value: number[] | undefined, fallback: number[]): number[] {\n\tconst days = value ?? fallback;\n\tif (!Array.isArray(days) || days.length === 0) {\n\t\tthrow new ValidationError(\"weekdays must name at least one day (1 = Monday \u2026 7 = Sunday)\");\n\t}\n\tfor (const day of days) {\n\t\tif (!Number.isInteger(day) || day < 1 || day > 7) {\n\t\t\tthrow new ValidationError(`weekdays must be ISO days 1..7 (got ${day})`);\n\t\t}\n\t}\n\treturn [...new Set(days)].sort((left, right) => left - right);\n}\n\n/**\n * Validates how often a rule may act.\n *\n * @param value - value from the request, `undefined` keeps the stored one\n * @param fallback - the stored value\n * @returns the validated value\n */\nfunction requireRepeat(value: AutomationRepeat | undefined, fallback: AutomationRepeat): AutomationRepeat {\n\tconst repeat = value ?? fallback;\n\tif (repeat !== \"day\" && repeat !== \"week\") {\n\t\t// the type already rules it out, this is for a caller that is not TypeScript\n\t\tthrow new ValidationError(`repeat must be \"day\" or \"week\" (got ${String(value)})`);\n\t}\n\treturn repeat;\n}\n\n/**\n * Creates the automation rule repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createAutomationsRepository(db: Db): AutomationsRepository {\n\tconst selectById = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules WHERE id = ?`);\n\tconst selectAll = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules ORDER BY id`);\n\tconst selectActive = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules WHERE is_active = 1 ORDER BY id`);\n\tconst insertRule = db.prepare(\n\t\t`INSERT INTO automation_rules (label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, active_from, active_until, created_at, updated_at)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateRule = db.prepare(\n\t\t`UPDATE automation_rules\n\t\t SET label = ?, kind = ?, user_id = ?, at_minute = ?, after_minutes = ?, weekdays = ?, repeat = ?,\n\t\t is_active = ?, active_from = ?, active_until = ?, updated_at = ?\n\t\t WHERE id = ?`,\n\t);\n\tconst deleteRule = db.prepare(\"DELETE FROM automation_rules WHERE id = ?\");\n\t// the runs are deleted with their rule: the rebuilt table of migration 18 has no foreign key for that\n\tconst deleteRunsOfRule = db.prepare(\"DELETE FROM automation_runs WHERE rule_id = ?\");\n\tconst selectRuns = db.prepare(\n\t\t`SELECT ${RUN_COLUMNS} FROM automation_runs\n\t\t WHERE rule_id = ? AND (? IS NULL OR period = ?)\n\t\t ORDER BY period DESC, rule_id LIMIT ?`,\n\t);\n\tconst selectAllRuns = db.prepare(\n\t\t`SELECT ${RUN_COLUMNS} FROM automation_runs ORDER BY fired_at DESC, rule_id LIMIT ?`,\n\t);\n\tconst selectRun = db.prepare(\n\t\t\"SELECT 1 AS found FROM automation_runs WHERE rule_id = ? AND user_id = ? AND period = ?\",\n\t);\n\tconst insertRun = db.prepare(\n\t\t`INSERT OR IGNORE INTO automation_runs (rule_id, user_id, period, fired_at, action)\n\t\t VALUES (?, ?, ?, ?, ?)`,\n\t);\n\tconst deleteRun = db.prepare(\"DELETE FROM automation_runs WHERE rule_id = ? AND user_id = ? AND period = ?\");\n\n\t/**\n\t * Reads one rule.\n\t *\n\t * @param id - rule id\n\t * @returns the rule or `null`\n\t */\n\tconst read = (id: number): AutomationRuleRecord | null => {\n\t\tconst row = selectById.get(id) as AutomationRuleRow | undefined;\n\t\treturn row ? mapAutomationRuleRow(row) : null;\n\t};\n\n\treturn {\n\t\tlist(options?: { includeInactive?: boolean }): AutomationRuleRecord[] {\n\t\t\tconst rows = (options?.includeInactive === true ? selectAll : selectActive).all() as AutomationRuleRow[];\n\t\t\treturn rows.map(mapAutomationRuleRow);\n\t\t},\n\n\t\tfindById: read,\n\n\t\tsave(input: SaveAutomationRuleInput): AutomationRuleRecord {\n\t\t\tconst kind = requireKind(input.kind);\n\t\t\tconst window = requireWindow(kind, input.atMinute ?? null, input.afterMinutes ?? null);\n\t\t\tconst label = (input.label ?? \"\").trim() || null;\n\t\t\tconst userId = input.userId ?? null;\n\t\t\tif (userId !== null && (!Number.isInteger(userId) || userId <= 0)) {\n\t\t\t\tthrow new ValidationError(`userId must reference an existing employee (got ${input.userId})`);\n\t\t\t}\n\t\t\tconst isActive = input.isActive !== false;\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst current = input.id === undefined ? null : read(input.id);\n\t\t\tif (input.id !== undefined && !current) {\n\t\t\t\tthrow new NotFoundError(`automation rule ${input.id} not found`);\n\t\t\t}\n\t\t\tconst weekdays = requireWeekdays(input.weekdays, current?.weekdays ?? ALL_WEEKDAYS);\n\t\t\tconst repeat = requireRepeat(input.repeat, current?.repeat ?? \"day\");\n\t\t\tconst activeFrom = requireValidityDate(input.activeFrom, current?.activeFrom ?? null, \"activeFrom\");\n\t\t\tconst activeUntil = requireValidityDate(input.activeUntil, current?.activeUntil ?? null, \"activeUntil\");\n\t\t\tif (activeFrom !== null && activeUntil !== null && activeUntil < activeFrom) {\n\t\t\t\tthrow new ValidationError(`activeUntil must not be before activeFrom (${activeFrom} \u2026 ${activeUntil})`);\n\t\t\t}\n\n\t\t\tconst next: AutomationRuleRecord = {\n\t\t\t\tid: current?.id ?? 0,\n\t\t\t\tlabel,\n\t\t\t\tkind,\n\t\t\t\tuserId,\n\t\t\t\tatMinute: window.atMinute,\n\t\t\t\tafterMinutes: window.afterMinutes,\n\t\t\t\tweekdays,\n\t\t\t\trepeat,\n\t\t\t\tisActive,\n\t\t\t\tactiveFrom,\n\t\t\t\tactiveUntil,\n\t\t\t};\n\t\t\tconst changes = current\n\t\t\t\t? diffFields(\n\t\t\t\t\t\tcurrent as unknown as Record<string, unknown>,\n\t\t\t\t\t\tnext as unknown as Record<string, unknown>,\n\t\t\t\t\t\tAUDITED_FIELDS as unknown as (keyof Record<string, unknown>)[],\n\t\t\t\t\t)\n\t\t\t\t: null;\n\t\t\t// the days are a list, so they are compared by their mask \u2014 that is also what the audit trail shows\n\t\t\tif (changes && weekdaysToMask(current?.weekdays ?? ALL_WEEKDAYS) !== weekdaysToMask(weekdays)) {\n\t\t\t\tchanges.weekdays = { old: current?.weekdays ?? [], new: weekdays };\n\t\t\t}\n\t\t\tif (current && Object.keys(changes ?? {}).length === 0) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tlet ruleId = current?.id ?? 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tif (current) {\n\t\t\t\t\tupdateRule.run(\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\twindow.atMinute,\n\t\t\t\t\t\twindow.afterMinutes,\n\t\t\t\t\t\tweekdaysToMask(weekdays),\n\t\t\t\t\t\trepeat,\n\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\tactiveFrom,\n\t\t\t\t\t\tactiveUntil,\n\t\t\t\t\t\tnow,\n\t\t\t\t\t\tcurrent.id,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\truleId = Number(\n\t\t\t\t\t\tinsertRule.run(\n\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tuserId,\n\t\t\t\t\t\t\twindow.atMinute,\n\t\t\t\t\t\t\twindow.afterMinutes,\n\t\t\t\t\t\t\tweekdaysToMask(weekdays),\n\t\t\t\t\t\t\trepeat,\n\t\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\t\tactiveFrom,\n\t\t\t\t\t\t\tactiveUntil,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t).lastInsertRowid,\n\t\t\t\t\t);\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: current ? \"automation_rule.update\" : \"automation_rule.create\",\n\t\t\t\t\tentity: \"automation_rule\",\n\t\t\t\t\tentityId: ruleId,\n\t\t\t\t\tdetail: current\n\t\t\t\t\t\t? { changes }\n\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\t\tuserId,\n\t\t\t\t\t\t\t\tatMinute: window.atMinute,\n\t\t\t\t\t\t\t\tafterMinutes: window.afterMinutes,\n\t\t\t\t\t\t\t\tweekdays,\n\t\t\t\t\t\t\t\trepeat,\n\t\t\t\t\t\t\t\tisActive,\n\t\t\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 saved = read(ruleId);\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`automation rule ${ruleId} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn saved;\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\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteRunsOfRule.run(input.id);\n\t\t\t\tdeleteRule.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: \"automation_rule.delete\",\n\t\t\t\t\tentity: \"automation_rule\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { kind: current.kind, userId: current.userId, label: current.label },\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\truns(input?: { ruleId?: number; period?: string; limit?: number }): AutomationRunRecord[] {\n\t\t\tconst limit = Math.min(Math.max(input?.limit ?? 50, 1), 500);\n\t\t\tif (input?.ruleId === undefined) {\n\t\t\t\treturn (selectAllRuns.all(limit) as AutomationRunRow[]).map(mapAutomationRunRow);\n\t\t\t}\n\t\t\tconst rows = selectRuns.all(\n\t\t\t\tinput.ruleId,\n\t\t\t\tinput.period ?? null,\n\t\t\t\tinput.period ?? null,\n\t\t\t\tlimit,\n\t\t\t) as AutomationRunRow[];\n\t\t\treturn rows.map(mapAutomationRunRow);\n\t\t},\n\n\t\thasRun(input: { ruleId: number; userId: number; period: string }): boolean {\n\t\t\treturn selectRun.get(input.ruleId, input.userId, input.period) !== undefined;\n\t\t},\n\n\t\trecordRun(input: { ruleId: number; userId: number; period: string; action: string; now?: number }): boolean {\n\t\t\tconst result = insertRun.run(\n\t\t\t\tinput.ruleId,\n\t\t\t\tinput.userId,\n\t\t\t\tinput.period,\n\t\t\t\tinput.now ?? Math.floor(Date.now() / 1000),\n\t\t\t\tinput.action,\n\t\t\t);\n\t\t\treturn result.changes > 0;\n\t\t},\n\n\t\tforgetRun(input: { ruleId: number; userId: number; period: string }): boolean {\n\t\t\treturn deleteRun.run(input.ruleId, input.userId, input.period).changes > 0;\n\t\t},\n\t};\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBA,oBAA+C;AAC/C,mBAA0C;
|
|
4
|
+
"sourcesContent": ["/**\n * Automation rules (`automation_rules`, `automation_runs`).\n *\n * The rules of the adapter itself \u2014 not of a foreign state like the trigger rules. Three kinds are supported:\n *\n * - `clockOut`: at a certain local time the adapter punches out an employee who is still clocked in. The punch is\n * written like any other one (`source: \"api\"`, note `auto.clock_out`), so it appears in the day, in the report\n * and in the audit trail \u2014 the employee can correct it afterwards like any other punch.\n * - `missingPunch`: the same moment, but the adapter only reports it. Nothing is written, so a shop that wants to\n * decide case by case gets a warning instead of a silent correction.\n * - `breakReminder`: an employee whose running work block reached the configured length gets a reminder \u2014 after a\n * break, the block starts again.\n *\n * `user_id` is `NULL` for a company rule and set for a rule of one employee. A rule runs at most once per employee\n * and local date: `automation_runs` holds that decision, which also gives the administration a small log (`action`\n * and `fired_at`) to look at.\n */\n\nimport type { Db } from \"../database\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { diffFields, writeAuditLog } from \"./audit\";\n\n/** Kinds an automation rule can have. */\nexport type AutomationKind = \"clockIn\" | \"clockOut\" | \"missingPunch\" | \"breakReminder\";\n\n/** How often a rule may act for one employee. */\nexport type AutomationRepeat = \"day\" | \"week\";\n\n/** An automation rule as it is stored. */\nexport interface AutomationRuleRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Free-form label, shown in the administration */\n\tlabel: string | null;\n\t/** What the rule does */\n\tkind: AutomationKind;\n\t/** Employee the rule applies to, `null` for every employee */\n\tuserId: number | null;\n\t/** Minute of the local day the rule acts at (kinds `clockOut` and `missingPunch`) */\n\tatMinute: number | null;\n\t/** Length of the running work block in minutes (kind `breakReminder`) */\n\tafterMinutes: number | null;\n\t/** Days of the week the rule runs on, ISO numbers 1 (Monday) to 7 (Sunday) */\n\tweekdays: number[];\n\t/** Whether the rule may act once a day or once an ISO week per employee */\n\trepeat: AutomationRepeat;\n\t/** Inactive rules are ignored but keep their history */\n\tisActive: boolean;\n\t/** First local day the rule is valid on (`YYYY-MM-DD`), `null` for \u201Cfrom now on\u201D */\n\tactiveFrom: string | null;\n\t/** Last local day the rule is valid on (`YYYY-MM-DD`, inclusive), `null` for \u201Cwithout an end\u201D */\n\tactiveUntil: string | null;\n}\n\n/** Input for creating or changing a rule. */\nexport interface SaveAutomationRuleInput {\n\t/** Id of an existing rule, omitted when a new rule is created */\n\tid?: number;\n\t/** Free-form label */\n\tlabel?: string | null;\n\t/** What the rule does */\n\tkind: AutomationKind;\n\t/** Employee the rule applies to, `null` or omitted for every employee */\n\tuserId?: number | null;\n\t/** Minute of the local day (kinds `clockOut` and `missingPunch`) */\n\tatMinute?: number | null;\n\t/** Length of the running work block in minutes (kind `breakReminder`) */\n\tafterMinutes?: number | null;\n\t/** Days of the week the rule runs on (ISO 1..7), omitted keeps the stored selection */\n\tweekdays?: number[];\n\t/** How often the rule may act, omitted keeps the stored value */\n\trepeat?: AutomationRepeat;\n\t/** `false` disables the rule without deleting it */\n\tisActive?: boolean;\n\t/** First local day the rule is valid on, omitted keeps the stored value, `null`/empty clears it */\n\tactiveFrom?: string | null;\n\t/** Last local day the rule is valid on (inclusive), omitted keeps the stored value, `null`/empty clears it */\n\tactiveUntil?: string | null;\n\t/** Who changes the rule */\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/** One run of a rule. */\nexport interface AutomationRunRecord {\n\t/** Rule that ran */\n\truleId: number;\n\t/** Employee it ran for */\n\tuserId: number;\n\t/** Period it ran in: the local date, or the ISO week (`2026-W38`) for a weekly rule */\n\tperiod: string;\n\t/** Instant it ran */\n\tfiredAt: number;\n\t/** Short description of what happened */\n\taction: string;\n}\n\n/** Automation rule storage operations. */\nexport interface AutomationsRepository {\n\t/** All rules, optionally including the disabled ones */\n\tlist(options?: { includeInactive?: boolean }): AutomationRuleRecord[];\n\t/** Reads one rule */\n\tfindById(id: number): AutomationRuleRecord | null;\n\t/** Creates or updates a rule */\n\tsave(input: SaveAutomationRuleInput): AutomationRuleRecord;\n\t/** Deletes a rule (its runs go with it) */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n\t/** Runs of the rules, newest first \u2014 the log behind the overview */\n\truns(input?: { ruleId?: number; period?: string; limit?: number }): AutomationRunRecord[];\n\t/** The newest run of every rule, newest first \u2014 one line per rule for the overview */\n\tlatestRuns(): AutomationRunRecord[];\n\t/** True when the rule already ran for that employee in that period */\n\thasRun(input: { ruleId: number; userId: number; period: string }): boolean;\n\t/** Notes a run; a second call for the same rule, employee and period changes nothing and returns `false` */\n\trecordRun(input: { ruleId: number; userId: number; period: string; action: string; now?: number }): boolean;\n\t/** Forgets a run again \u2014 used when an action failed, so the next check can retry it */\n\tforgetRun(input: { ruleId: number; userId: number; period: string }): boolean;\n}\n\n/** Raw database row of a rule. */\ninterface AutomationRuleRow {\n\tid: number;\n\tlabel: string | null;\n\tkind: string;\n\tuser_id: number | null;\n\tat_minute: number | null;\n\tafter_minutes: number | null;\n\tweekdays: number;\n\trepeat: string;\n\tis_active: number;\n\tactive_from: string | null;\n\tactive_until: string | null;\n}\n\n/** Raw database row of a run. */\ninterface AutomationRunRow {\n\trule_id: number;\n\tuser_id: number;\n\tperiod: string;\n\tfired_at: number;\n\taction: string;\n}\n\nconst RULE_COLUMNS =\n\t\"id, label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, active_from, active_until\";\nconst RUN_COLUMNS = \"rule_id, user_id, period, fired_at, action\";\n\n/**\n * Fields of a rule that are compared for the audit trail.\n *\n * `weekdays` is not in here: it is a list, and the comparison of the audit trail works on single values. The saved\n * selection is written to the audit detail separately.\n */\nconst AUDITED_FIELDS: (keyof AutomationRuleRecord)[] = [\n\t\"label\",\n\t\"kind\",\n\t\"userId\",\n\t\"atMinute\",\n\t\"afterMinutes\",\n\t\"repeat\",\n\t\"isActive\",\n\t\"activeFrom\",\n\t\"activeUntil\",\n];\n\n/** Kinds a rule may use. */\nconst KINDS: AutomationKind[] = [\"clockIn\", \"clockOut\", \"missingPunch\", \"breakReminder\"];\n\n/** Kinds that act at a fixed local time. */\nconst TIMED_KINDS: AutomationKind[] = [\"clockIn\", \"clockOut\", \"missingPunch\"];\n\n/**\n * Maps a database row to a rule record.\n *\n * @param row - raw database row\n * @returns rule record\n */\nexport function mapAutomationRuleRow(row: AutomationRuleRow): AutomationRuleRecord {\n\treturn {\n\t\tid: row.id,\n\t\tlabel: row.label,\n\t\tkind: KINDS.includes(row.kind as AutomationKind) ? (row.kind as AutomationKind) : \"clockOut\",\n\t\tuserId: row.user_id,\n\t\tatMinute: row.at_minute,\n\t\tafterMinutes: row.after_minutes,\n\t\t// a row written before the migration has no selection: every day is the sensible default\n\t\tweekdays: maskToWeekdays(row.weekdays ?? 127),\n\t\trepeat: row.repeat === \"week\" ? \"week\" : \"day\",\n\t\tisActive: row.is_active !== 0,\n\t\tactiveFrom: row.active_from ?? null,\n\t\tactiveUntil: row.active_until ?? null,\n\t};\n}\n\n/**\n * Maps a database row to a run record.\n *\n * @param row - raw database row\n * @returns run record\n */\nexport function mapAutomationRunRow(row: AutomationRunRow): AutomationRunRecord {\n\treturn {\n\t\truleId: row.rule_id,\n\t\tuserId: row.user_id,\n\t\tperiod: row.period,\n\t\tfiredAt: row.fired_at,\n\t\taction: row.action,\n\t};\n}\n\n/**\n * Validates the kind of a rule.\n *\n * @param value - kind from the request\n * @returns the validated kind\n */\nfunction requireKind(value: string): AutomationKind {\n\tif (!KINDS.includes(value as AutomationKind)) {\n\t\tthrow new ValidationError(`kind must be one of ${KINDS.join(\", \")} (got ${value})`);\n\t}\n\treturn value as AutomationKind;\n}\n\n/**\n * Validates the time of a rule: a timed kind needs a minute of the day, the reminder needs a length.\n *\n * @param kind - kind of the rule\n * @param atMinute - minute of the local day\n * @param afterMinutes - length of the running work block\n * @returns the validated values\n */\nfunction requireWindow(\n\tkind: AutomationKind,\n\tatMinute: number | null,\n\tafterMinutes: number | null,\n): { atMinute: number | null; afterMinutes: number | null } {\n\tif (TIMED_KINDS.includes(kind)) {\n\t\tif (atMinute === null || !Number.isInteger(atMinute) || atMinute < 0 || atMinute > 1439) {\n\t\t\tthrow new ValidationError(`atMinute must be a minute of the day between 0 and 1439 (got ${atMinute})`);\n\t\t}\n\t\treturn { atMinute, afterMinutes: null };\n\t}\n\n\tif (afterMinutes === null || !Number.isInteger(afterMinutes) || afterMinutes < 1 || afterMinutes > 1440) {\n\t\tthrow new ValidationError(`afterMinutes must be between 1 and 1440 (got ${afterMinutes})`);\n\t}\n\treturn { atMinute: null, afterMinutes };\n}\n\n/**\n * Validates one end of the validity period of a rule.\n *\n * `undefined` keeps the stored value, an empty string or `null` clears the limit (that is the \u201Cfrom now on\u201D and\n * \u201Cwithout an end\u201D of the dialog), anything else has to be a real calendar date in `YYYY-MM-DD`.\n *\n * @param value - value from the request\n * @param fallback - value to keep when the request says nothing\n * @param field - field name for the error message\n * @returns the validated date or `null`\n */\nfunction requireValidityDate(value: string | null | undefined, fallback: string | null, field: string): string | null {\n\tif (value === undefined) {\n\t\treturn fallback;\n\t}\n\tconst text = (value ?? \"\").trim();\n\tif (text === \"\") {\n\t\treturn null;\n\t}\n\tconst parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(text);\n\tconst date = parts ? new Date(Date.UTC(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))) : null;\n\tif (\n\t\t!parts ||\n\t\t!date ||\n\t\tdate.getUTCFullYear() !== Number(parts[1]) ||\n\t\tdate.getUTCMonth() !== Number(parts[2]) - 1 ||\n\t\tdate.getUTCDate() !== Number(parts[3])\n\t) {\n\t\tthrow new ValidationError(`${field} must be a date like 2026-10-01 (got ${text})`);\n\t}\n\treturn text;\n}\n\n/** Days a rule runs on when nothing else is chosen. */\nexport const ALL_WEEKDAYS: number[] = [1, 2, 3, 4, 5, 6, 7];\n\n/**\n * Turns the stored bitmask into ISO weekdays.\n *\n * @param mask - bits 0..6, bit 0 is Monday\n * @returns the days, at least every day when the mask is empty\n */\nexport function maskToWeekdays(mask: number): number[] {\n\tconst days: number[] = [];\n\tfor (let day = 1; day <= 7; day += 1) {\n\t\tif ((mask & (1 << (day - 1))) !== 0) {\n\t\t\tdays.push(day);\n\t\t}\n\t}\n\treturn days.length > 0 ? days : [...ALL_WEEKDAYS];\n}\n\n/**\n * Turns ISO weekdays into the stored bitmask.\n *\n * @param days - the days, ISO numbers 1..7\n * @returns the mask\n */\nexport function weekdaysToMask(days: number[]): number {\n\tlet mask = 0;\n\tfor (const day of days) {\n\t\tmask |= 1 << (day - 1);\n\t}\n\treturn mask;\n}\n\n/**\n * Validates the weekday selection of a rule.\n *\n * @param value - selection from the request, `undefined` keeps the stored one\n * @param fallback - the stored selection\n * @returns the validated, sorted selection\n */\nfunction requireWeekdays(value: number[] | undefined, fallback: number[]): number[] {\n\tconst days = value ?? fallback;\n\tif (!Array.isArray(days) || days.length === 0) {\n\t\tthrow new ValidationError(\"weekdays must name at least one day (1 = Monday \u2026 7 = Sunday)\");\n\t}\n\tfor (const day of days) {\n\t\tif (!Number.isInteger(day) || day < 1 || day > 7) {\n\t\t\tthrow new ValidationError(`weekdays must be ISO days 1..7 (got ${day})`);\n\t\t}\n\t}\n\treturn [...new Set(days)].sort((left, right) => left - right);\n}\n\n/**\n * Validates how often a rule may act.\n *\n * @param value - value from the request, `undefined` keeps the stored one\n * @param fallback - the stored value\n * @returns the validated value\n */\nfunction requireRepeat(value: AutomationRepeat | undefined, fallback: AutomationRepeat): AutomationRepeat {\n\tconst repeat = value ?? fallback;\n\tif (repeat !== \"day\" && repeat !== \"week\") {\n\t\t// the type already rules it out, this is for a caller that is not TypeScript\n\t\tthrow new ValidationError(`repeat must be \"day\" or \"week\" (got ${String(value)})`);\n\t}\n\treturn repeat;\n}\n\n/**\n * Creates the automation rule repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createAutomationsRepository(db: Db): AutomationsRepository {\n\tconst selectById = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules WHERE id = ?`);\n\tconst selectAll = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules ORDER BY id`);\n\tconst selectActive = db.prepare(`SELECT ${RULE_COLUMNS} FROM automation_rules WHERE is_active = 1 ORDER BY id`);\n\tconst insertRule = db.prepare(\n\t\t`INSERT INTO automation_rules (label, kind, user_id, at_minute, after_minutes, weekdays, repeat, is_active, active_from, active_until, created_at, updated_at)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateRule = db.prepare(\n\t\t`UPDATE automation_rules\n\t\t SET label = ?, kind = ?, user_id = ?, at_minute = ?, after_minutes = ?, weekdays = ?, repeat = ?,\n\t\t is_active = ?, active_from = ?, active_until = ?, updated_at = ?\n\t\t WHERE id = ?`,\n\t);\n\tconst deleteRule = db.prepare(\"DELETE FROM automation_rules WHERE id = ?\");\n\t// the runs are deleted with their rule: the rebuilt table of migration 18 has no foreign key for that\n\tconst deleteRunsOfRule = db.prepare(\"DELETE FROM automation_runs WHERE rule_id = ?\");\n\tconst selectRuns = db.prepare(\n\t\t`SELECT ${RUN_COLUMNS} FROM automation_runs\n\t\t WHERE rule_id = ? AND (? IS NULL OR period = ?)\n\t\t ORDER BY period DESC, rule_id LIMIT ?`,\n\t);\n\tconst selectAllRuns = db.prepare(\n\t\t`SELECT ${RUN_COLUMNS} FROM automation_runs ORDER BY fired_at DESC, rule_id LIMIT ?`,\n\t);\n\t// one line per rule: SQLite takes the bare columns of the row that carries the `MAX()` when a query has exactly\n\t// one aggregate, so `user_id` and `period` belong to the newest run of their rule\n\tconst selectLatestRuns = db.prepare(\n\t\t`SELECT rule_id, user_id, period, MAX(fired_at) AS fired_at, action\n\t\t FROM automation_runs GROUP BY rule_id ORDER BY fired_at DESC, rule_id`,\n\t);\n\tconst selectRun = db.prepare(\n\t\t\"SELECT 1 AS found FROM automation_runs WHERE rule_id = ? AND user_id = ? AND period = ?\",\n\t);\n\tconst insertRun = db.prepare(\n\t\t`INSERT OR IGNORE INTO automation_runs (rule_id, user_id, period, fired_at, action)\n\t\t VALUES (?, ?, ?, ?, ?)`,\n\t);\n\tconst deleteRun = db.prepare(\"DELETE FROM automation_runs WHERE rule_id = ? AND user_id = ? AND period = ?\");\n\n\t/**\n\t * Reads one rule.\n\t *\n\t * @param id - rule id\n\t * @returns the rule or `null`\n\t */\n\tconst read = (id: number): AutomationRuleRecord | null => {\n\t\tconst row = selectById.get(id) as AutomationRuleRow | undefined;\n\t\treturn row ? mapAutomationRuleRow(row) : null;\n\t};\n\n\treturn {\n\t\tlist(options?: { includeInactive?: boolean }): AutomationRuleRecord[] {\n\t\t\tconst rows = (options?.includeInactive === true ? selectAll : selectActive).all() as AutomationRuleRow[];\n\t\t\treturn rows.map(mapAutomationRuleRow);\n\t\t},\n\n\t\tfindById: read,\n\n\t\tsave(input: SaveAutomationRuleInput): AutomationRuleRecord {\n\t\t\tconst kind = requireKind(input.kind);\n\t\t\tconst window = requireWindow(kind, input.atMinute ?? null, input.afterMinutes ?? null);\n\t\t\tconst label = (input.label ?? \"\").trim() || null;\n\t\t\tconst userId = input.userId ?? null;\n\t\t\tif (userId !== null && (!Number.isInteger(userId) || userId <= 0)) {\n\t\t\t\tthrow new ValidationError(`userId must reference an existing employee (got ${input.userId})`);\n\t\t\t}\n\t\t\tconst isActive = input.isActive !== false;\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst current = input.id === undefined ? null : read(input.id);\n\t\t\tif (input.id !== undefined && !current) {\n\t\t\t\tthrow new NotFoundError(`automation rule ${input.id} not found`);\n\t\t\t}\n\t\t\tconst weekdays = requireWeekdays(input.weekdays, current?.weekdays ?? ALL_WEEKDAYS);\n\t\t\tconst repeat = requireRepeat(input.repeat, current?.repeat ?? \"day\");\n\t\t\tconst activeFrom = requireValidityDate(input.activeFrom, current?.activeFrom ?? null, \"activeFrom\");\n\t\t\tconst activeUntil = requireValidityDate(input.activeUntil, current?.activeUntil ?? null, \"activeUntil\");\n\t\t\tif (activeFrom !== null && activeUntil !== null && activeUntil < activeFrom) {\n\t\t\t\tthrow new ValidationError(`activeUntil must not be before activeFrom (${activeFrom} \u2026 ${activeUntil})`);\n\t\t\t}\n\n\t\t\tconst next: AutomationRuleRecord = {\n\t\t\t\tid: current?.id ?? 0,\n\t\t\t\tlabel,\n\t\t\t\tkind,\n\t\t\t\tuserId,\n\t\t\t\tatMinute: window.atMinute,\n\t\t\t\tafterMinutes: window.afterMinutes,\n\t\t\t\tweekdays,\n\t\t\t\trepeat,\n\t\t\t\tisActive,\n\t\t\t\tactiveFrom,\n\t\t\t\tactiveUntil,\n\t\t\t};\n\t\t\tconst changes = current\n\t\t\t\t? diffFields(\n\t\t\t\t\t\tcurrent as unknown as Record<string, unknown>,\n\t\t\t\t\t\tnext as unknown as Record<string, unknown>,\n\t\t\t\t\t\tAUDITED_FIELDS as unknown as (keyof Record<string, unknown>)[],\n\t\t\t\t\t)\n\t\t\t\t: null;\n\t\t\t// the days are a list, so they are compared by their mask \u2014 that is also what the audit trail shows\n\t\t\tif (changes && weekdaysToMask(current?.weekdays ?? ALL_WEEKDAYS) !== weekdaysToMask(weekdays)) {\n\t\t\t\tchanges.weekdays = { old: current?.weekdays ?? [], new: weekdays };\n\t\t\t}\n\t\t\tif (current && Object.keys(changes ?? {}).length === 0) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tlet ruleId = current?.id ?? 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tif (current) {\n\t\t\t\t\tupdateRule.run(\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t\tkind,\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\twindow.atMinute,\n\t\t\t\t\t\twindow.afterMinutes,\n\t\t\t\t\t\tweekdaysToMask(weekdays),\n\t\t\t\t\t\trepeat,\n\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\tactiveFrom,\n\t\t\t\t\t\tactiveUntil,\n\t\t\t\t\t\tnow,\n\t\t\t\t\t\tcurrent.id,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\truleId = Number(\n\t\t\t\t\t\tinsertRule.run(\n\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tuserId,\n\t\t\t\t\t\t\twindow.atMinute,\n\t\t\t\t\t\t\twindow.afterMinutes,\n\t\t\t\t\t\t\tweekdaysToMask(weekdays),\n\t\t\t\t\t\t\trepeat,\n\t\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\t\tactiveFrom,\n\t\t\t\t\t\t\tactiveUntil,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t).lastInsertRowid,\n\t\t\t\t\t);\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: current ? \"automation_rule.update\" : \"automation_rule.create\",\n\t\t\t\t\tentity: \"automation_rule\",\n\t\t\t\t\tentityId: ruleId,\n\t\t\t\t\tdetail: current\n\t\t\t\t\t\t? { changes }\n\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\t\tuserId,\n\t\t\t\t\t\t\t\tatMinute: window.atMinute,\n\t\t\t\t\t\t\t\tafterMinutes: window.afterMinutes,\n\t\t\t\t\t\t\t\tweekdays,\n\t\t\t\t\t\t\t\trepeat,\n\t\t\t\t\t\t\t\tisActive,\n\t\t\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 saved = read(ruleId);\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`automation rule ${ruleId} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn saved;\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\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteRunsOfRule.run(input.id);\n\t\t\t\tdeleteRule.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: \"automation_rule.delete\",\n\t\t\t\t\tentity: \"automation_rule\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { kind: current.kind, userId: current.userId, label: current.label },\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\truns(input?: { ruleId?: number; period?: string; limit?: number }): AutomationRunRecord[] {\n\t\t\tconst limit = Math.min(Math.max(input?.limit ?? 50, 1), 500);\n\t\t\tif (input?.ruleId === undefined) {\n\t\t\t\treturn (selectAllRuns.all(limit) as AutomationRunRow[]).map(mapAutomationRunRow);\n\t\t\t}\n\t\t\tconst rows = selectRuns.all(\n\t\t\t\tinput.ruleId,\n\t\t\t\tinput.period ?? null,\n\t\t\t\tinput.period ?? null,\n\t\t\t\tlimit,\n\t\t\t) as AutomationRunRow[];\n\t\t\treturn rows.map(mapAutomationRunRow);\n\t\t},\n\n\t\tlatestRuns(): AutomationRunRecord[] {\n\t\t\treturn (selectLatestRuns.all() as AutomationRunRow[]).map(mapAutomationRunRow);\n\t\t},\n\n\t\thasRun(input: { ruleId: number; userId: number; period: string }): boolean {\n\t\t\treturn selectRun.get(input.ruleId, input.userId, input.period) !== undefined;\n\t\t},\n\n\t\trecordRun(input: { ruleId: number; userId: number; period: string; action: string; now?: number }): boolean {\n\t\t\tconst result = insertRun.run(\n\t\t\t\tinput.ruleId,\n\t\t\t\tinput.userId,\n\t\t\t\tinput.period,\n\t\t\t\tinput.now ?? Math.floor(Date.now() / 1000),\n\t\t\t\tinput.action,\n\t\t\t);\n\t\t\treturn result.changes > 0;\n\t\t},\n\n\t\tforgetRun(input: { ruleId: number; userId: number; period: string }): boolean {\n\t\t\treturn deleteRun.run(input.ruleId, input.userId, input.period).changes > 0;\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["_a"]
|
|
7
7
|
}
|
|
@@ -36,6 +36,7 @@ function requireDate(date) {
|
|
|
36
36
|
}
|
|
37
37
|
function createHolidaysRepository(db) {
|
|
38
38
|
const selectByYear = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? AND region = ? ORDER BY date`);
|
|
39
|
+
const selectByYearAllRegions = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? ORDER BY date, region`);
|
|
39
40
|
const selectByDate = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE date = ? AND region = ?`);
|
|
40
41
|
const selectYears = db.prepare("SELECT DISTINCT year FROM holidays ORDER BY year");
|
|
41
42
|
const selectById = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE id = ?`);
|
|
@@ -49,7 +50,10 @@ function createHolidaysRepository(db) {
|
|
|
49
50
|
return row != null ? row : null;
|
|
50
51
|
};
|
|
51
52
|
return {
|
|
52
|
-
listByYear(year, region
|
|
53
|
+
listByYear(year, region) {
|
|
54
|
+
if (region === void 0) {
|
|
55
|
+
return selectByYearAllRegions.all(year);
|
|
56
|
+
}
|
|
53
57
|
return selectByYear.all(year, region);
|
|
54
58
|
},
|
|
55
59
|
years() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/lib/db/repositories/holidays.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Holiday repository (`holidays`).\n *\n * Holidays are stored per region, the region is the country code of the instance (`CH`, `DE`, `AT`).\n * `ensureYear` generates the holidays of a country idempotently and never touches edited rows, so\n * administrators can rename or delete single days.\n */\n\nimport type { Db } from \"../database\";\nimport { holidaysForYear, type HolidayCountry } from \"../../domain/holidays\";\nimport { isDateString } from \"../../util/time\";\nimport { ValidationError } from \"../../errors\";\nimport { writeAuditLog } from \"./audit\";\n\n/** A public holiday as stored in the database. */\nexport interface HolidayRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Region (country code) the holiday belongs to */\n\tregion: string;\n\t/** Year of the holiday (derived from `date`) */\n\tyear: number;\n\t/** Local date, `YYYY-MM-DD` */\n\tdate: string;\n\t/** Display name */\n\tname: string;\n\t/** Key of a seeded holiday (`null` for one that was added by hand) */\n\tkey: string | null;\n}\n\n/** Holiday storage operations. */\nexport interface HolidaysRepository {\n\t/** Holidays of one year, sorted by date */\n\tlistByYear(year: number, region?: string): HolidayRecord[];\n\t/** Years for which holidays are stored */\n\tyears(): number[];\n\t/** True when the date is a holiday of the region */\n\tisHoliday(date: string, region?: string): boolean;\n\t/** All holiday dates of a year as a set (input for the calculation service) */\n\tdateSet(year: number, region?: string): Set<string>;\n\t/** Generates the missing holidays of a country for one year */\n\tensureYear(input: { year: number; country: HolidayCountry; actorId?: number | null; now?: number }): {\n\t\tyear: number;\n\t\tinserted: number;\n\t};\n\t/** Adds a holiday; an existing date of the region is renamed */\n\tadd(input: {\n\t\tdate: string;\n\t\tname: string;\n\t\tregion?: string;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): HolidayRecord;\n\t/** Deletes a holiday by id */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n}\n\ninterface HolidayRow {\n\tid: number;\n\tregion: string;\n\tyear: number;\n\tdate: string;\n\tname: string;\n\tkey: string | null;\n}\n\n/** Default region used when the caller does not pass one. */\nconst DEFAULT_REGION = \"DE\";\n\nconst COLUMNS = \"id, region, year, date, name, key\";\n\n/**\n * Checks and normalises a holiday date.\n *\n * @param date - date to validate\n * @returns the validated date\n */\nfunction requireDate(date: string): string {\n\tconst trimmed = date.trim();\n\tif (!isDateString(trimmed)) {\n\t\tthrow new ValidationError(`invalid date \"${date}\", expected YYYY-MM-DD`);\n\t}\n\treturn trimmed;\n}\n\n/**\n * Creates the holiday repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createHolidaysRepository(db: Db): HolidaysRepository {\n\tconst selectByYear = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? AND region = ? ORDER BY date`);\n\tconst selectByDate = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE date = ? AND region = ?`);\n\tconst selectYears = db.prepare(\"SELECT DISTINCT year FROM holidays ORDER BY year\");\n\tconst selectById = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE id = ?`);\n\tconst insertHoliday = db.prepare(\n\t\t`INSERT INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\n\t\t ON CONFLICT(region, date) DO UPDATE SET name = excluded.name, year = excluded.year, key = excluded.key`,\n\t);\n\tconst deleteHoliday = db.prepare(\"DELETE FROM holidays WHERE id = ?\");\n\n\tconst read = (id: number): HolidayRecord | null => {\n\t\tconst row = selectById.get(id) as HolidayRow | undefined;\n\t\treturn row ?? null;\n\t};\n\n\treturn {\n\t\tlistByYear(year: number, region
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,sBAAqD;AACrD,kBAA6B;AAC7B,oBAAgC;AAChC,mBAA8B;AAwD9B,MAAM,iBAAiB;AAEvB,MAAM,UAAU;AAQhB,SAAS,YAAY,MAAsB;AAC1C,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,KAAC,0BAAa,OAAO,GAAG;AAC3B,UAAM,IAAI,8BAAgB,iBAAiB,IAAI,wBAAwB;AAAA,EACxE;AACA,SAAO;AACR;AAQO,SAAS,yBAAyB,IAA4B;AACpE,QAAM,eAAe,GAAG,QAAQ,UAAU,OAAO,4DAA4D;AAC7G,QAAM,eAAe,GAAG,QAAQ,UAAU,OAAO,8CAA8C;AAC/F,QAAM,cAAc,GAAG,QAAQ,kDAAkD;AACjF,QAAM,aAAa,GAAG,QAAQ,UAAU,OAAO,6BAA6B;AAC5E,QAAM,gBAAgB,GAAG;AAAA,IACxB;AAAA;AAAA,EAED;AACA,QAAM,gBAAgB,GAAG,QAAQ,mCAAmC;AAEpE,QAAM,OAAO,CAAC,OAAqC;AAClD,UAAM,MAAM,WAAW,IAAI,EAAE;AAC7B,WAAO,oBAAO;AAAA,EACf;AAEA,SAAO;AAAA,IACN,WAAW,MAAc,
|
|
4
|
+
"sourcesContent": ["/**\n * Holiday repository (`holidays`).\n *\n * Holidays are stored per region, the region is the country code of the instance (`CH`, `DE`, `AT`).\n * `ensureYear` generates the holidays of a country idempotently and never touches edited rows, so\n * administrators can rename or delete single days.\n */\n\nimport type { Db } from \"../database\";\nimport { holidaysForYear, type HolidayCountry } from \"../../domain/holidays\";\nimport { isDateString } from \"../../util/time\";\nimport { ValidationError } from \"../../errors\";\nimport { writeAuditLog } from \"./audit\";\n\n/** A public holiday as stored in the database. */\nexport interface HolidayRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Region (country code) the holiday belongs to */\n\tregion: string;\n\t/** Year of the holiday (derived from `date`) */\n\tyear: number;\n\t/** Local date, `YYYY-MM-DD` */\n\tdate: string;\n\t/** Display name */\n\tname: string;\n\t/** Key of a seeded holiday (`null` for one that was added by hand) */\n\tkey: string | null;\n}\n\n/** Holiday storage operations. */\nexport interface HolidaysRepository {\n\t/** Holidays of one year, sorted by date; without a region the days of every region are returned */\n\tlistByYear(year: number, region?: string): HolidayRecord[];\n\t/** Years for which holidays are stored */\n\tyears(): number[];\n\t/** True when the date is a holiday of the region */\n\tisHoliday(date: string, region?: string): boolean;\n\t/** All holiday dates of a year as a set (input for the calculation service) */\n\tdateSet(year: number, region?: string): Set<string>;\n\t/** Generates the missing holidays of a country for one year */\n\tensureYear(input: { year: number; country: HolidayCountry; actorId?: number | null; now?: number }): {\n\t\tyear: number;\n\t\tinserted: number;\n\t};\n\t/** Adds a holiday; an existing date of the region is renamed */\n\tadd(input: {\n\t\tdate: string;\n\t\tname: string;\n\t\tregion?: string;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): HolidayRecord;\n\t/** Deletes a holiday by id */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n}\n\ninterface HolidayRow {\n\tid: number;\n\tregion: string;\n\tyear: number;\n\tdate: string;\n\tname: string;\n\tkey: string | null;\n}\n\n/** Default region used when the caller does not pass one. */\nconst DEFAULT_REGION = \"DE\";\n\nconst COLUMNS = \"id, region, year, date, name, key\";\n\n/**\n * Checks and normalises a holiday date.\n *\n * @param date - date to validate\n * @returns the validated date\n */\nfunction requireDate(date: string): string {\n\tconst trimmed = date.trim();\n\tif (!isDateString(trimmed)) {\n\t\tthrow new ValidationError(`invalid date \"${date}\", expected YYYY-MM-DD`);\n\t}\n\treturn trimmed;\n}\n\n/**\n * Creates the holiday repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createHolidaysRepository(db: Db): HolidaysRepository {\n\tconst selectByYear = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? AND region = ? ORDER BY date`);\n\tconst selectByYearAllRegions = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE year = ? ORDER BY date, region`);\n\tconst selectByDate = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE date = ? AND region = ?`);\n\tconst selectYears = db.prepare(\"SELECT DISTINCT year FROM holidays ORDER BY year\");\n\tconst selectById = db.prepare(`SELECT ${COLUMNS} FROM holidays WHERE id = ?`);\n\tconst insertHoliday = db.prepare(\n\t\t`INSERT INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\n\t\t ON CONFLICT(region, date) DO UPDATE SET name = excluded.name, year = excluded.year, key = excluded.key`,\n\t);\n\tconst deleteHoliday = db.prepare(\"DELETE FROM holidays WHERE id = ?\");\n\n\tconst read = (id: number): HolidayRecord | null => {\n\t\tconst row = selectById.get(id) as HolidayRow | undefined;\n\t\treturn row ?? null;\n\t};\n\n\treturn {\n\t\tlistByYear(year: number, region?: string): HolidayRecord[] {\n\t\t\t// without a region the whole year is listed: the administration also sees the days of another region,\n\t\t\t// which keeps them at hand after a change of the holiday country\n\t\t\tif (region === undefined) {\n\t\t\t\treturn selectByYearAllRegions.all(year) as HolidayRecord[];\n\t\t\t}\n\t\t\treturn selectByYear.all(year, region) as HolidayRecord[];\n\t\t},\n\n\t\tyears(): number[] {\n\t\t\treturn (selectYears.all() as { year: number }[]).map(row => row.year);\n\t\t},\n\n\t\tisHoliday(date: string, region: string = DEFAULT_REGION): boolean {\n\t\t\treturn selectByDate.get(date, region) !== undefined;\n\t\t},\n\n\t\tdateSet(year: number, region: string = DEFAULT_REGION): Set<string> {\n\t\t\tconst rows = selectByYear.all(year, region) as HolidayRow[];\n\t\t\treturn new Set(rows.map(row => row.date));\n\t\t},\n\n\t\tensureYear(input: { year: number; country: HolidayCountry; actorId?: number | null; now?: number }): {\n\t\t\tyear: number;\n\t\t\tinserted: number;\n\t\t} {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tlet inserted = 0;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst insertMissing = db.prepare(\n\t\t\t\t\t\"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\",\n\t\t\t\t);\n\t\t\t\tfor (const holiday of holidaysForYear(input.year, input.country)) {\n\t\t\t\t\tinserted += insertMissing.run(\n\t\t\t\t\t\tinput.country,\n\t\t\t\t\t\tinput.year,\n\t\t\t\t\t\tholiday.date,\n\t\t\t\t\t\tholiday.name,\n\t\t\t\t\t\tholiday.key,\n\t\t\t\t\t).changes;\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\n\t\t\tif (inserted > 0) {\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId ?? null,\n\t\t\t\t\taction: \"holiday.ensure\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: input.year,\n\t\t\t\t\tdetail: { year: input.year, country: input.country, inserted },\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn { year: input.year, inserted };\n\t\t},\n\n\t\tadd(input: {\n\t\t\tdate: string;\n\t\t\tname: string;\n\t\t\tregion?: string;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): HolidayRecord {\n\t\t\tconst date = requireDate(input.date);\n\t\t\tconst name = input.name.trim();\n\t\t\tif (!name) {\n\t\t\t\tthrow new ValidationError(\"name must not be empty\");\n\t\t\t}\n\t\t\tconst region = input.region ?? DEFAULT_REGION;\n\t\t\tconst year = Number(date.slice(0, 4));\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst existing = selectByDate.get(date, region) as HolidayRow | undefined;\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\t// a day somebody adds by hand has no key: it keeps the name it was given\n\t\t\t\tinsertHoliday.run(region, year, date, name, null);\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: existing ? \"holiday.update\" : \"holiday.create\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: date,\n\t\t\t\t\tdetail: existing\n\t\t\t\t\t\t? { changes: { name: { old: existing.name, new: name } } }\n\t\t\t\t\t\t: { date, name, region },\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 saved = selectByDate.get(date, region) as HolidayRow | undefined;\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`holiday ${date} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn saved;\n\t\t},\n\n\t\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst existing = read(input.id);\n\t\t\tif (!existing) {\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\tdeleteHoliday.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: \"holiday.delete\",\n\t\t\t\t\tentity: \"holiday\",\n\t\t\t\t\tentityId: existing.date,\n\t\t\t\t\tdetail: { region: existing.region, 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\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["_a"]
|
|
7
7
|
}
|
package/build/lib/web/api.js
CHANGED
|
@@ -1008,6 +1008,63 @@ function createApi(deps) {
|
|
|
1008
1008
|
emit({ type: "absence.change", userId: absence.userId, data: { absenceId: absence.id, action: "deleted" } });
|
|
1009
1009
|
return (0, import_router.noContent)();
|
|
1010
1010
|
});
|
|
1011
|
+
route("POST", "/absences/:id/cancellation", { permission: "absence.request", csrf: true }, (context) => {
|
|
1012
|
+
var _a2, _b2, _c2;
|
|
1013
|
+
const absence = changeableAbsence(context);
|
|
1014
|
+
const body = context.optionalJsonBody();
|
|
1015
|
+
const updated = absences.requestCancel({
|
|
1016
|
+
id: absence.id,
|
|
1017
|
+
note: optionalString(body, "note"),
|
|
1018
|
+
actorId: (_b2 = (_a2 = context.auth) == null ? void 0 : _a2.user.id) != null ? _b2 : 0,
|
|
1019
|
+
actorIp: (_c2 = context.request.remoteAddress) != null ? _c2 : null,
|
|
1020
|
+
now: now()
|
|
1021
|
+
});
|
|
1022
|
+
emit({
|
|
1023
|
+
type: "absence.change",
|
|
1024
|
+
userId: updated.userId,
|
|
1025
|
+
data: { absenceId: updated.id, action: "cancellation" }
|
|
1026
|
+
});
|
|
1027
|
+
return (0, import_router.json)(200, { absence: publicAbsence(updated) });
|
|
1028
|
+
});
|
|
1029
|
+
route("DELETE", "/absences/:id/cancellation", { permission: "absence.request", csrf: true }, (context) => {
|
|
1030
|
+
var _a2, _b2, _c2;
|
|
1031
|
+
const absence = changeableAbsence(context);
|
|
1032
|
+
const updated = absences.clearCancel({
|
|
1033
|
+
id: absence.id,
|
|
1034
|
+
actorId: (_b2 = (_a2 = context.auth) == null ? void 0 : _a2.user.id) != null ? _b2 : 0,
|
|
1035
|
+
actorIp: (_c2 = context.request.remoteAddress) != null ? _c2 : null,
|
|
1036
|
+
now: now()
|
|
1037
|
+
});
|
|
1038
|
+
emit({
|
|
1039
|
+
type: "absence.change",
|
|
1040
|
+
userId: updated.userId,
|
|
1041
|
+
data: { absenceId: updated.id, action: "cancellationWithdrawn" }
|
|
1042
|
+
});
|
|
1043
|
+
return (0, import_router.json)(200, { absence: publicAbsence(updated) });
|
|
1044
|
+
});
|
|
1045
|
+
route("POST", "/absences/:id/cancellation/decline", { permission: "absence.approve", csrf: true }, (context) => {
|
|
1046
|
+
var _a2, _b2, _c2;
|
|
1047
|
+
const id = numberParam(context, "id");
|
|
1048
|
+
const absence = absences.findById(id);
|
|
1049
|
+
if (!absence) {
|
|
1050
|
+
throw new import_errors.NotFoundError(`absence ${id} not found`);
|
|
1051
|
+
}
|
|
1052
|
+
const body = context.optionalJsonBody();
|
|
1053
|
+
const updated = absences.clearCancel({
|
|
1054
|
+
id,
|
|
1055
|
+
declined: true,
|
|
1056
|
+
note: optionalString(body, "note"),
|
|
1057
|
+
actorId: (_b2 = (_a2 = context.auth) == null ? void 0 : _a2.user.id) != null ? _b2 : 0,
|
|
1058
|
+
actorIp: (_c2 = context.request.remoteAddress) != null ? _c2 : null,
|
|
1059
|
+
now: now()
|
|
1060
|
+
});
|
|
1061
|
+
emit({
|
|
1062
|
+
type: "absence.change",
|
|
1063
|
+
userId: updated.userId,
|
|
1064
|
+
data: { absenceId: updated.id, action: "cancellationDeclined" }
|
|
1065
|
+
});
|
|
1066
|
+
return (0, import_router.json)(200, { absence: publicAbsence(updated) });
|
|
1067
|
+
});
|
|
1011
1068
|
route(
|
|
1012
1069
|
"GET",
|
|
1013
1070
|
"/calendar.ics",
|
|
@@ -1112,7 +1169,7 @@ function createApi(deps) {
|
|
|
1112
1169
|
return (0, import_router.json)(200, { year, region: region != null ? region : null, holidays: holidays.listByYear(year, region) });
|
|
1113
1170
|
});
|
|
1114
1171
|
route("POST", "/holidays", { permission: "holiday.manage", csrf: true }, (context) => {
|
|
1115
|
-
var _a2, _b2;
|
|
1172
|
+
var _a2, _b2, _c2;
|
|
1116
1173
|
if (!context.auth) {
|
|
1117
1174
|
throw (0, import_problem.problem)(401, "no_session", "request rejected (no_session)");
|
|
1118
1175
|
}
|
|
@@ -1120,9 +1177,11 @@ function createApi(deps) {
|
|
|
1120
1177
|
const created = holidays.add({
|
|
1121
1178
|
date: requireString(body, "date"),
|
|
1122
1179
|
name: requireString(body, "name"),
|
|
1123
|
-
region
|
|
1180
|
+
// without a region the day belongs to the country of the instance: the calculation looks only there, so a
|
|
1181
|
+
// day filed under a foreign region would never count
|
|
1182
|
+
region: (_b2 = (_a2 = optionalString(body, "region")) != null ? _a2 : settings.get("holiday_country")) != null ? _b2 : void 0,
|
|
1124
1183
|
actorId: context.auth.user.id,
|
|
1125
|
-
actorIp: (
|
|
1184
|
+
actorIp: (_c2 = context.request.remoteAddress) != null ? _c2 : null,
|
|
1126
1185
|
now: now()
|
|
1127
1186
|
});
|
|
1128
1187
|
return (0, import_router.json)(201, { holiday: created }, { location: `/holidays/${created.id}` });
|
|
@@ -1405,7 +1464,7 @@ function createApi(deps) {
|
|
|
1405
1464
|
"GET",
|
|
1406
1465
|
"/automation-rules/runs",
|
|
1407
1466
|
{ permission: "settings.view" },
|
|
1408
|
-
() => (0, import_router.json)(200, { runs: automations.
|
|
1467
|
+
() => (0, import_router.json)(200, { runs: automations.latestRuns() })
|
|
1409
1468
|
);
|
|
1410
1469
|
route(
|
|
1411
1470
|
"PUT",
|