iobroker.time-tracker 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +67 -28
  2. package/admin/i18n/de.json +1 -1
  3. package/admin/i18n/en.json +1 -1
  4. package/admin/i18n/es.json +1 -1
  5. package/admin/i18n/fr.json +1 -1
  6. package/admin/i18n/it.json +1 -1
  7. package/admin/i18n/nl.json +1 -1
  8. package/admin/i18n/pl.json +1 -1
  9. package/admin/i18n/pt.json +1 -1
  10. package/admin/i18n/ru.json +1 -1
  11. package/admin/i18n/uk.json +1 -1
  12. package/admin/i18n/zh-cn.json +1 -1
  13. package/admin/jsonConfig.json +1 -1
  14. package/build/lib/db/migrations.js +39 -0
  15. package/build/lib/db/migrations.js.map +2 -2
  16. package/build/lib/db/repositories/dayNotes.js +183 -0
  17. package/build/lib/db/repositories/dayNotes.js.map +7 -0
  18. package/build/lib/db/seed.js +1 -1
  19. package/build/lib/db/seed.js.map +2 -2
  20. package/build/lib/reports/pdf.js +8 -2
  21. package/build/lib/reports/pdf.js.map +2 -2
  22. package/build/lib/services/sync.js +11 -9
  23. package/build/lib/services/sync.js.map +2 -2
  24. package/build/lib/web/api.js +112 -16
  25. package/build/lib/web/api.js.map +2 -2
  26. package/build/lib/web/events.js.map +2 -2
  27. package/build/lib/web/problem.js.map +2 -2
  28. package/build/main.js +4 -0
  29. package/build/main.js.map +2 -2
  30. package/io-package.json +27 -27
  31. package/package.json +1 -1
  32. package/src-pwa/src/i18n/de.json +16 -1
  33. package/src-pwa/src/i18n/en.json +16 -1
  34. package/src-pwa/src/i18n/es.json +16 -1
  35. package/src-pwa/src/i18n/fr.json +16 -1
  36. package/src-pwa/src/i18n/it.json +16 -1
  37. package/src-pwa/src/i18n/nl.json +16 -1
  38. package/src-pwa/src/i18n/pl.json +16 -1
  39. package/src-pwa/src/i18n/pt.json +16 -1
  40. package/src-pwa/src/i18n/ru.json +16 -1
  41. package/src-pwa/src/i18n/uk.json +16 -1
  42. package/src-pwa/src/i18n/zh-cn.json +16 -1
  43. package/www/assets/index-C0z1CkCi.js +119 -0
  44. package/www/assets/index-C0z1CkCi.js.map +1 -0
  45. package/www/index.html +1 -1
  46. package/www/sw.js +1 -1
  47. package/www/sw.js.map +1 -1
  48. package/www/assets/index-DIM5i7zh.js +0 -119
  49. package/www/assets/index-DIM5i7zh.js.map +0 -1
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var dayNotes_exports = {};
20
+ __export(dayNotes_exports, {
21
+ MAX_DAY_NOTE_LENGTH: () => MAX_DAY_NOTE_LENGTH,
22
+ createDayNotesRepository: () => createDayNotesRepository
23
+ });
24
+ module.exports = __toCommonJS(dayNotes_exports);
25
+ var import_time = require("../../util/time");
26
+ var import_errors = require("../../errors");
27
+ var import_audit = require("./audit");
28
+ const MAX_DAY_NOTE_LENGTH = 500;
29
+ function createDayNotesRepository(db) {
30
+ const COLUMNS = `id, user_id, local_date, note, created_by, created_at, updated_by, updated_at, handled_at,
31
+ handled_by`;
32
+ const selectByRange = db.prepare(
33
+ `SELECT ${COLUMNS} FROM day_notes WHERE user_id = ? AND local_date BETWEEN ? AND ? ORDER BY local_date, id`
34
+ );
35
+ const selectByDate = db.prepare(`SELECT ${COLUMNS} FROM day_notes WHERE user_id = ? AND local_date = ?`);
36
+ const insert = db.prepare(
37
+ `INSERT INTO day_notes (user_id, local_date, note, created_by, created_at, updated_by, updated_at)
38
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
39
+ );
40
+ const updateText = db.prepare(`UPDATE day_notes SET note = ?, updated_by = ?, updated_at = ? WHERE id = ?`);
41
+ const removeRow = db.prepare(`DELETE FROM day_notes WHERE id = ?`);
42
+ const updateHandled = db.prepare(
43
+ `UPDATE day_notes SET handled_at = ?, handled_by = ?, updated_by = ?, updated_at = ? WHERE id = ?`
44
+ );
45
+ const mapRow = (row) => ({
46
+ id: row.id,
47
+ userId: row.user_id,
48
+ localDate: row.local_date,
49
+ note: row.note,
50
+ createdBy: row.created_by,
51
+ createdAt: row.created_at,
52
+ updatedBy: row.updated_by,
53
+ updatedAt: row.updated_at,
54
+ handledAt: row.handled_at,
55
+ handledBy: row.handled_by
56
+ });
57
+ const read = (userId, localDate) => {
58
+ const row = selectByDate.get(userId, localDate);
59
+ return row ? mapRow(row) : null;
60
+ };
61
+ const requireDate = (localDate) => {
62
+ if (!(0, import_time.isDateString)(localDate)) {
63
+ throw new import_errors.ValidationError("localDate must be a date (YYYY-MM-DD)");
64
+ }
65
+ };
66
+ return {
67
+ listByRange(userId, fromDate, toDate) {
68
+ const rows = selectByRange.all(userId, fromDate, toDate);
69
+ return rows.map(mapRow);
70
+ },
71
+ find: read,
72
+ save(input) {
73
+ var _a;
74
+ requireDate(input.localDate);
75
+ const note = input.note.trim();
76
+ if (note.length > MAX_DAY_NOTE_LENGTH) {
77
+ throw new import_errors.ValidationError(
78
+ `note must not be longer than ${MAX_DAY_NOTE_LENGTH} characters (got ${note.length})`
79
+ );
80
+ }
81
+ const atUtc = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
82
+ const existing = read(input.userId, input.localDate);
83
+ if (note === "") {
84
+ if (!existing) {
85
+ return null;
86
+ }
87
+ const run2 = db.transaction(() => {
88
+ var _a2;
89
+ removeRow.run(existing.id);
90
+ (0, import_audit.writeAuditLog)(db, {
91
+ atUtc,
92
+ actorId: input.actorId,
93
+ action: "day_note.delete",
94
+ entity: "day_notes",
95
+ entityId: existing.id,
96
+ detail: { userId: input.userId, localDate: input.localDate, note: existing.note },
97
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
98
+ });
99
+ });
100
+ run2();
101
+ return null;
102
+ }
103
+ if (existing && existing.note === note) {
104
+ return existing;
105
+ }
106
+ const run = db.transaction(() => {
107
+ var _a2, _b;
108
+ if (existing) {
109
+ updateText.run(note, input.actorId, atUtc, existing.id);
110
+ (0, import_audit.writeAuditLog)(db, {
111
+ atUtc,
112
+ actorId: input.actorId,
113
+ action: "day_note.update",
114
+ entity: "day_notes",
115
+ entityId: existing.id,
116
+ detail: {
117
+ userId: input.userId,
118
+ localDate: input.localDate,
119
+ old: existing.note,
120
+ new: note
121
+ },
122
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
123
+ });
124
+ return;
125
+ }
126
+ const result = insert.run(
127
+ input.userId,
128
+ input.localDate,
129
+ note,
130
+ input.actorId,
131
+ atUtc,
132
+ input.actorId,
133
+ atUtc
134
+ );
135
+ (0, import_audit.writeAuditLog)(db, {
136
+ atUtc,
137
+ actorId: input.actorId,
138
+ action: "day_note.create",
139
+ entity: "day_notes",
140
+ entityId: Number(result.lastInsertRowid),
141
+ detail: { userId: input.userId, localDate: input.localDate, note },
142
+ ip: (_b = input.actorIp) != null ? _b : null
143
+ });
144
+ });
145
+ run();
146
+ return read(input.userId, input.localDate);
147
+ },
148
+ setHandled(input) {
149
+ var _a, _b;
150
+ requireDate(input.localDate);
151
+ const existing = read(input.userId, input.localDate);
152
+ if (!existing) {
153
+ throw new import_errors.NotFoundError(`day note of ${input.localDate} not found`);
154
+ }
155
+ const atUtc = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
156
+ const wanted = input.handled ? atUtc : null;
157
+ if (existing.handledAt === wanted) {
158
+ return existing;
159
+ }
160
+ const run = db.transaction(() => {
161
+ var _a2;
162
+ updateHandled.run(wanted, wanted === null ? null : input.actorId, input.actorId, atUtc, existing.id);
163
+ (0, import_audit.writeAuditLog)(db, {
164
+ atUtc,
165
+ actorId: input.actorId,
166
+ action: "day_note.handled",
167
+ entity: "day_notes",
168
+ entityId: existing.id,
169
+ detail: { userId: input.userId, localDate: input.localDate, handled: input.handled },
170
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
171
+ });
172
+ });
173
+ run();
174
+ return (_b = read(input.userId, input.localDate)) != null ? _b : existing;
175
+ }
176
+ };
177
+ }
178
+ // Annotate the CommonJS export names for ESM import in node:
179
+ 0 && (module.exports = {
180
+ MAX_DAY_NOTE_LENGTH,
181
+ createDayNotesRepository
182
+ });
183
+ //# sourceMappingURL=dayNotes.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/db/repositories/dayNotes.ts"],
4
+ "sourcesContent": ["/**\n * Day notes (`day_notes`): what an employee wants the administration to know about one day.\n *\n * An employee does not change punches; the day of a forgotten punch is corrected by the administration. What the\n * employee may do is leave a note like \u201Cforgot to clock in or out\u201D. That message lives outside the punches, because\n * a day without a punch is exactly the case it is meant for. One note per employee and day: writing a second one\n * replaces the first, an empty text takes it back. The administration marks a note as handled and keeps it.\n */\n\nimport type { Db } from \"../database\";\nimport { isDateString } from \"../../util/time\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { writeAuditLog } from \"./audit\";\n\n/** Longest text a note may carry. */\nexport const MAX_DAY_NOTE_LENGTH = 500;\n\n/** A note an employee left for one of his days. */\nexport interface DayNoteRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Owner of the day */\n\tuserId: number;\n\t/** Local date the note belongs to (`YYYY-MM-DD`) */\n\tlocalDate: string;\n\t/** Text of the note */\n\tnote: string;\n\t/** Who wrote it */\n\tcreatedBy: number | null;\n\t/** Instant it was written, UTC epoch seconds */\n\tcreatedAt: number;\n\t/** Who changed it last */\n\tupdatedBy: number | null;\n\t/** Instant of the last change, UTC epoch seconds */\n\tupdatedAt: number;\n\t/** Instant the administration marked it as handled, `null` while it waits */\n\thandledAt: number | null;\n\t/** Who marked it as handled */\n\thandledBy: number | null;\n}\n\n/** Input for writing (or clearing) the note of one day. */\nexport interface SaveDayNoteInput {\n\t/** Owner of the day */\n\tuserId: number;\n\t/** Local date the note belongs to */\n\tlocalDate: string;\n\t/** Text of the note; an empty text removes the stored note */\n\tnote: string;\n\t/** Who writes it */\n\tactorId: number;\n\t/** Client IP address */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** Input for marking a note as handled (or open again). */\nexport interface HandleDayNoteInput {\n\t/** Owner of the day */\n\tuserId: number;\n\t/** Local date the note belongs to */\n\tlocalDate: string;\n\t/** True marks it as handled, false puts it back into the list of open notes */\n\thandled: boolean;\n\t/** Who decides */\n\tactorId: number;\n\t/** Client IP address */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** Typed access to the day notes. */\nexport interface DayNotesRepository {\n\t/** Notes of one employee in a local date range (inclusive), oldest first */\n\tlistByRange(userId: number, fromDate: string, toDate: string): DayNoteRecord[];\n\t/** Note of one day or `null` */\n\tfind(userId: number, localDate: string): DayNoteRecord | null;\n\t/**\n\t * Writes the note of one day.\n\t *\n\t * @param input - owner, day, text and actor\n\t * @returns the stored note, `null` when an empty text removed it\n\t */\n\tsave(input: SaveDayNoteInput): DayNoteRecord | null;\n\t/**\n\t * Marks a note as handled or open again.\n\t *\n\t * @param input - owner, day and decision\n\t * @returns the updated note\n\t */\n\tsetHandled(input: HandleDayNoteInput): DayNoteRecord;\n}\n\n/** One row of `day_notes` as it is stored. */\ninterface DayNoteRow {\n\tid: number;\n\tuser_id: number;\n\tlocal_date: string;\n\tnote: string;\n\tcreated_by: number | null;\n\tcreated_at: number;\n\tupdated_by: number | null;\n\tupdated_at: number;\n\thandled_at: number | null;\n\thandled_by: number | null;\n}\n\n/**\n * Creates the repository of the day notes.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createDayNotesRepository(db: Db): DayNotesRepository {\n\tconst COLUMNS = `id, user_id, local_date, note, created_by, created_at, updated_by, updated_at, handled_at,\n\t\thandled_by`;\n\tconst selectByRange = db.prepare(\n\t\t`SELECT ${COLUMNS} FROM day_notes WHERE user_id = ? AND local_date BETWEEN ? AND ? ORDER BY local_date, id`,\n\t);\n\tconst selectByDate = db.prepare(`SELECT ${COLUMNS} FROM day_notes WHERE user_id = ? AND local_date = ?`);\n\tconst insert = db.prepare(\n\t\t`INSERT INTO day_notes (user_id, local_date, note, created_by, created_at, updated_by, updated_at)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateText = db.prepare(`UPDATE day_notes SET note = ?, updated_by = ?, updated_at = ? WHERE id = ?`);\n\tconst removeRow = db.prepare(`DELETE FROM day_notes WHERE id = ?`);\n\tconst updateHandled = db.prepare(\n\t\t`UPDATE day_notes SET handled_at = ?, handled_by = ?, updated_by = ?, updated_at = ? WHERE id = ?`,\n\t);\n\n\t/**\n\t * Turns a stored row into the record the API returns.\n\t *\n\t * @param row - stored row\n\t * @returns the record\n\t */\n\tconst mapRow = (row: DayNoteRow): DayNoteRecord => ({\n\t\tid: row.id,\n\t\tuserId: row.user_id,\n\t\tlocalDate: row.local_date,\n\t\tnote: row.note,\n\t\tcreatedBy: row.created_by,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedBy: row.updated_by,\n\t\tupdatedAt: row.updated_at,\n\t\thandledAt: row.handled_at,\n\t\thandledBy: row.handled_by,\n\t});\n\n\t/**\n\t * Reads the note of one day.\n\t *\n\t * @param userId - owner of the day\n\t * @param localDate - the day\n\t * @returns the note or `null`\n\t */\n\tconst read = (userId: number, localDate: string): DayNoteRecord | null => {\n\t\tconst row = selectByDate.get(userId, localDate) as DayNoteRow | undefined;\n\t\treturn row ? mapRow(row) : null;\n\t};\n\n\t/**\n\t * Checks the day a note belongs to.\n\t *\n\t * @param localDate - the day\n\t */\n\tconst requireDate = (localDate: string): void => {\n\t\tif (!isDateString(localDate)) {\n\t\t\tthrow new ValidationError(\"localDate must be a date (YYYY-MM-DD)\");\n\t\t}\n\t};\n\n\treturn {\n\t\tlistByRange(userId: number, fromDate: string, toDate: string): DayNoteRecord[] {\n\t\t\tconst rows = selectByRange.all(userId, fromDate, toDate) as DayNoteRow[];\n\t\t\treturn rows.map(mapRow);\n\t\t},\n\n\t\tfind: read,\n\n\t\tsave(input: SaveDayNoteInput): DayNoteRecord | null {\n\t\t\trequireDate(input.localDate);\n\t\t\tconst note = input.note.trim();\n\t\t\tif (note.length > MAX_DAY_NOTE_LENGTH) {\n\t\t\t\tthrow new ValidationError(\n\t\t\t\t\t`note must not be longer than ${MAX_DAY_NOTE_LENGTH} characters (got ${note.length})`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst atUtc = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst existing = read(input.userId, input.localDate);\n\n\t\t\t// an empty text is how a note is taken back\n\t\t\tif (note === \"\") {\n\t\t\t\tif (!existing) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tconst run = db.transaction((): void => {\n\t\t\t\t\tremoveRow.run(existing.id);\n\t\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\t\tatUtc,\n\t\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\t\taction: \"day_note.delete\",\n\t\t\t\t\t\tentity: \"day_notes\",\n\t\t\t\t\t\tentityId: existing.id,\n\t\t\t\t\t\tdetail: { userId: input.userId, localDate: input.localDate, note: existing.note },\n\t\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\trun();\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tif (existing && existing.note === note) {\n\t\t\t\treturn existing;\n\t\t\t}\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tif (existing) {\n\t\t\t\t\tupdateText.run(note, input.actorId, atUtc, existing.id);\n\t\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\t\tatUtc,\n\t\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\t\taction: \"day_note.update\",\n\t\t\t\t\t\tentity: \"day_notes\",\n\t\t\t\t\t\tentityId: existing.id,\n\t\t\t\t\t\tdetail: {\n\t\t\t\t\t\t\tuserId: input.userId,\n\t\t\t\t\t\t\tlocalDate: input.localDate,\n\t\t\t\t\t\t\told: existing.note,\n\t\t\t\t\t\t\tnew: note,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t\t});\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = insert.run(\n\t\t\t\t\tinput.userId,\n\t\t\t\t\tinput.localDate,\n\t\t\t\t\tnote,\n\t\t\t\t\tinput.actorId,\n\t\t\t\t\tatUtc,\n\t\t\t\t\tinput.actorId,\n\t\t\t\t\tatUtc,\n\t\t\t\t);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"day_note.create\",\n\t\t\t\t\tentity: \"day_notes\",\n\t\t\t\t\tentityId: Number(result.lastInsertRowid),\n\t\t\t\t\tdetail: { userId: input.userId, localDate: input.localDate, note },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn read(input.userId, input.localDate);\n\t\t},\n\n\t\tsetHandled(input: HandleDayNoteInput): DayNoteRecord {\n\t\t\trequireDate(input.localDate);\n\t\t\tconst existing = read(input.userId, input.localDate);\n\t\t\tif (!existing) {\n\t\t\t\tthrow new NotFoundError(`day note of ${input.localDate} not found`);\n\t\t\t}\n\t\t\tconst atUtc = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst wanted = input.handled ? atUtc : null;\n\t\t\tif (existing.handledAt === wanted) {\n\t\t\t\treturn existing;\n\t\t\t}\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupdateHandled.run(wanted, wanted === null ? null : input.actorId, input.actorId, atUtc, existing.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"day_note.handled\",\n\t\t\t\t\tentity: \"day_notes\",\n\t\t\t\t\tentityId: existing.id,\n\t\t\t\t\tdetail: { userId: input.userId, localDate: input.localDate, handled: input.handled },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\treturn read(input.userId, input.localDate) ?? existing;\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["run", "_a"]
7
+ }
@@ -48,7 +48,7 @@ const PERMISSIONS = {
48
48
  };
49
49
  const ALL_PERMISSIONS = Object.values(PERMISSIONS).flat();
50
50
  const ROLE_PERMISSIONS = {
51
- admin: ALL_PERMISSIONS,
51
+ admin: ALL_PERMISSIONS.filter((permission) => permission !== "time.punch"),
52
52
  manager: [
53
53
  "time.punch",
54
54
  "time.edit_own",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/db/seed.ts"],
4
- "sourcesContent": ["/**\n * Initial data (seeds): roles, permission catalogue, absence types, instance settings and holidays.\n *\n * All seed operations are idempotent (`INSERT OR IGNORE`), so `seed()` can run on every adapter start.\n */\n\nimport type { Db } from \"./database\";\nimport { holidaysForYear, type HolidayCountry } from \"../domain/holidays\";\n\n/** Permission catalogue (keys are stored in `permissions.key`). */\nexport const PERMISSIONS = {\n\ttime: [\"time.punch\", \"time.edit_own\", \"time.edit_other\", \"time.delete\", \"time.import\", \"time.resolve_conflict\"],\n\tabsence: [\"absence.request\", \"absence.approve\", \"absence.edit_other\", \"absence.manage_types\"],\n\tuser: [\"user.view\", \"user.create\", \"user.edit\", \"user.deactivate\", \"user.manage_roles\"],\n\treport: [\"report.view_own\", \"report.view_other\", \"report.export\", \"report.statistics\", \"report.print_locked\"],\n\tpayout: [\"payout.view\", \"payout.create\"],\n\tsystem: [\n\t\t\"settings.view\",\n\t\t\"settings.edit\",\n\t\t\"rfid.manage\",\n\t\t\"terminal.manage\",\n\t\t\"holiday.manage\",\n\t\t\"import.run\",\n\t\t\"audit.view\",\n\t\t\"backup.run\",\n\t],\n\tcommand: [\"command.punch\", \"command.close_month\", \"command.recalc\"],\n} as const;\n\nexport const ALL_PERMISSIONS: string[] = Object.values(PERMISSIONS).flat();\n\n/** Role \u2192 permissions (specification section 4.8). */\nexport const ROLE_PERMISSIONS: Record<string, string[]> = {\n\tadmin: ALL_PERMISSIONS,\n\tmanager: [\n\t\t\"time.punch\",\n\t\t\"time.edit_own\",\n\t\t\"time.edit_other\",\n\t\t\"time.delete\",\n\t\t\"time.resolve_conflict\",\n\t\t\"absence.request\",\n\t\t\"absence.approve\",\n\t\t\"absence.edit_other\",\n\t\t\"absence.manage_types\",\n\t\t\"user.view\",\n\t\t\"report.view_own\",\n\t\t\"report.view_other\",\n\t\t\"report.export\",\n\t\t\"report.statistics\",\n\t\t\"report.print_locked\",\n\t\t\"payout.view\",\n\t\t\"settings.view\",\n\t\t\"holiday.manage\",\n\t\t\"audit.view\",\n\t\t\"command.punch\",\n\t\t\"command.close_month\",\n\t\t\"command.recalc\",\n\t],\n\temployee: [\"time.punch\", \"time.edit_own\", \"absence.request\", \"report.view_own\"],\n};\n\n/**\n * Default absence types of the time tracking: code, name, factor, reduces vacation, visible for everybody.\n *\n * \u201CVisible for everybody\u201D (`active`) says whether an employee may pick the type in the app. Sickness, accident and\n * military service start switched off: a company gets those notes on the same day and books them itself, while\n * vacation, further training and the like are requested by the employee.\n */\nexport const ABSENCE_TYPES: {\n\tcode: string;\n\tname: string;\n\tfactor: number;\n\treduceVacation: boolean;\n\tactive: boolean;\n\tcolor: string;\n}[] = [\n\t{ code: \"F\", name: \"Ferien\", factor: 100, reduceVacation: true, active: true, color: \"#2e7d32\" },\n\t{ code: \"K\", name: \"Krankheit\", factor: 100, reduceVacation: false, active: false, color: \"#c62828\" },\n\t{ code: \"U\", name: \"Unfall\", factor: 100, reduceVacation: false, active: false, color: \"#ef6c00\" },\n\t{ code: \"M\", name: \"Milit\u00E4r\", factor: 100, reduceVacation: false, active: false, color: \"#455a64\" },\n\t{ code: \"I\", name: \"Intern\", factor: 100, reduceVacation: false, active: true, color: \"#1565c0\" },\n\t{ code: \"W\", name: \"Weiterbildung\", factor: 50, reduceVacation: false, active: true, color: \"#6a1b9a\" },\n\t{ code: \"E\", name: \"Extern\", factor: 50, reduceVacation: false, active: true, color: \"#795548\" },\n];\n\n/** Default instance settings (specification section 2.9.9); existing values are never overwritten. */\nexport const SETTING_DEFAULTS: Record<string, string> = {\n\tedit_window_days: \"7\",\n\tquick_round_minutes: \"0\",\n\tabsence_calc_until_today: \"1\",\n\tabsence_deduct_worktime: \"0\",\n\tprint_limit_days: \"0\",\n\tholiday_country: \"DE\",\n\tdefault_language: \"de\",\n\tsession_ttl_minutes: \"720\",\n\tbackup_retention_days: \"30\",\n\tattendance_list_visible: \"0\",\n\t// how the pause of a day is determined: `auto` = the punched break, otherwise the graduated rules\n\tpause_mode: \"auto\",\n\t// path of a Unicode font for the PDF statement; empty uses the built-in Latin fonts\n\treport_font_path: \"\",\n\t// branding of the installation: logo and background as data URLs (empty = not set), accent colour as hex\n\tbrand_logo: \"\",\n\tbrand_background: \"\",\n\tbrand_color: \"\",\n};\n\n/**\n * Options controlling which initial data is written.\n */\nexport interface SeedOptions {\n\t/** Country used to generate public holidays (default `CH`) */\n\tholidayCountry?: HolidayCountry;\n\t/** Years for which holidays are generated (default: current and next year) */\n\tholidayYears?: number[];\n}\n\n/**\n * Writes all initial data. Idempotent: repeated calls change nothing and never overwrite\n * user-modified settings.\n *\n * @param db - open database handle\n * @param options - seed options\n * @returns summary of what is now present\n */\nexport function seed(db: Db, options: SeedOptions = {}): { permissions: number; holidays: number } {\n\tconst country: HolidayCountry = options.holidayCountry ?? \"DE\";\n\tconst currentYear = new Date().getUTCFullYear();\n\tconst years = options.holidayYears ?? [currentYear, currentYear + 1];\n\n\tconst run = db.transaction((): void => {\n\t\tconst now = Math.floor(Date.now() / 1000);\n\n\t\tconst insertRole = db.prepare(\"INSERT OR IGNORE INTO roles (key, name) VALUES (?, ?)\");\n\t\tinsertRole.run(\"admin\", \"Administrator\");\n\t\tinsertRole.run(\"manager\", \"Manager\");\n\t\tinsertRole.run(\"employee\", \"Employee\");\n\n\t\tconst insertPermission = db.prepare(\"INSERT OR IGNORE INTO permissions (key) VALUES (?)\");\n\t\tfor (const permission of ALL_PERMISSIONS) {\n\t\t\tinsertPermission.run(permission);\n\t\t}\n\n\t\tconst findRole = db.prepare(\"SELECT id FROM roles WHERE key = ?\");\n\t\tconst findPermission = db.prepare(\"SELECT id FROM permissions WHERE key = ?\");\n\t\tconst linkRole = db.prepare(\"INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)\");\n\t\tfor (const [roleKey, permissions] of Object.entries(ROLE_PERMISSIONS)) {\n\t\t\tconst role = findRole.get(roleKey) as { id: number } | undefined;\n\t\t\tif (!role) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const permission of permissions) {\n\t\t\t\tconst row = findPermission.get(permission) as { id: number } | undefined;\n\t\t\t\tif (row) {\n\t\t\t\t\tlinkRole.run(role.id, row.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst insertAbsenceType = db.prepare(\n\t\t\t`INSERT OR IGNORE INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active, color)\n\t\t\t VALUES (NULL, ?, ?, 1, ?, ?, ?, ?)`,\n\t\t);\n\t\tfor (const type of ABSENCE_TYPES) {\n\t\t\tinsertAbsenceType.run(\n\t\t\t\ttype.code,\n\t\t\t\ttype.name,\n\t\t\t\ttype.factor,\n\t\t\t\ttype.reduceVacation ? 1 : 0,\n\t\t\t\ttype.active ? 1 : 0,\n\t\t\t\ttype.color,\n\t\t\t);\n\t\t}\n\n\t\tconst insertSetting = db.prepare(\n\t\t\t\"INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)\",\n\t\t);\n\t\tfor (const [key, value] of Object.entries(SETTING_DEFAULTS)) {\n\t\t\tinsertSetting.run(key, key === \"holiday_country\" ? country : value, now);\n\t\t}\n\t});\n\trun();\n\n\tlet holidayCount = 0;\n\tfor (const year of years) {\n\t\tholidayCount += ensureHolidaysForYear(db, country, year);\n\t}\n\n\tconst permissionCount = (db.prepare(\"SELECT COUNT(*) AS c FROM permissions\").get() as { c: number }).c;\n\treturn { permissions: permissionCount, holidays: holidayCount };\n}\n\n/**\n * Generates and stores the public holidays of one year for a country (idempotent).\n *\n * @param db - open database handle\n * @param country - supported country code\n * @param year - four digit year\n * @returns number of inserted holiday rows\n */\nexport function ensureHolidaysForYear(db: Db, country: HolidayCountry, year: number): number {\n\tconst insert = db.prepare(\"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\");\n\tlet inserted = 0;\n\tconst run = db.transaction((): void => {\n\t\tfor (const holiday of holidaysForYear(year, country)) {\n\t\t\tinserted += insert.run(country, year, holiday.date, holiday.name, holiday.key).changes;\n\t\t}\n\t});\n\trun();\n\treturn inserted;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Initial data (seeds): roles, permission catalogue, absence types, instance settings and holidays.\n *\n * All seed operations are idempotent (`INSERT OR IGNORE`), so `seed()` can run on every adapter start.\n */\n\nimport type { Db } from \"./database\";\nimport { holidaysForYear, type HolidayCountry } from \"../domain/holidays\";\n\n/** Permission catalogue (keys are stored in `permissions.key`). */\nexport const PERMISSIONS = {\n\ttime: [\"time.punch\", \"time.edit_own\", \"time.edit_other\", \"time.delete\", \"time.import\", \"time.resolve_conflict\"],\n\tabsence: [\"absence.request\", \"absence.approve\", \"absence.edit_other\", \"absence.manage_types\"],\n\tuser: [\"user.view\", \"user.create\", \"user.edit\", \"user.deactivate\", \"user.manage_roles\"],\n\treport: [\"report.view_own\", \"report.view_other\", \"report.export\", \"report.statistics\", \"report.print_locked\"],\n\tpayout: [\"payout.view\", \"payout.create\"],\n\tsystem: [\n\t\t\"settings.view\",\n\t\t\"settings.edit\",\n\t\t\"rfid.manage\",\n\t\t\"terminal.manage\",\n\t\t\"holiday.manage\",\n\t\t\"import.run\",\n\t\t\"audit.view\",\n\t\t\"backup.run\",\n\t],\n\tcommand: [\"command.punch\", \"command.close_month\", \"command.recalc\"],\n} as const;\n\nexport const ALL_PERMISSIONS: string[] = Object.values(PERMISSIONS).flat();\n\n/**\n * Role \u2192 permissions (specification section 4.8).\n *\n * The administrator account is created by the installation and belongs to nobody: it administers the employees\n * instead of working with them and therefore does not punch (migration 26 takes the right away from installations\n * that already exist). Whoever both administers and works carries the `employee` role as well \u2014 the right comes\n * back with it, because permissions are the union of the roles of an account.\n */\nexport const ROLE_PERMISSIONS: Record<string, string[]> = {\n\tadmin: ALL_PERMISSIONS.filter(permission => permission !== \"time.punch\"),\n\tmanager: [\n\t\t\"time.punch\",\n\t\t\"time.edit_own\",\n\t\t\"time.edit_other\",\n\t\t\"time.delete\",\n\t\t\"time.resolve_conflict\",\n\t\t\"absence.request\",\n\t\t\"absence.approve\",\n\t\t\"absence.edit_other\",\n\t\t\"absence.manage_types\",\n\t\t\"user.view\",\n\t\t\"report.view_own\",\n\t\t\"report.view_other\",\n\t\t\"report.export\",\n\t\t\"report.statistics\",\n\t\t\"report.print_locked\",\n\t\t\"payout.view\",\n\t\t\"settings.view\",\n\t\t\"holiday.manage\",\n\t\t\"audit.view\",\n\t\t\"command.punch\",\n\t\t\"command.close_month\",\n\t\t\"command.recalc\",\n\t],\n\temployee: [\"time.punch\", \"time.edit_own\", \"absence.request\", \"report.view_own\"],\n};\n\n/**\n * Default absence types of the time tracking: code, name, factor, reduces vacation, visible for everybody.\n *\n * \u201CVisible for everybody\u201D (`active`) says whether an employee may pick the type in the app. Sickness, accident and\n * military service start switched off: a company gets those notes on the same day and books them itself, while\n * vacation, further training and the like are requested by the employee.\n */\nexport const ABSENCE_TYPES: {\n\tcode: string;\n\tname: string;\n\tfactor: number;\n\treduceVacation: boolean;\n\tactive: boolean;\n\tcolor: string;\n}[] = [\n\t{ code: \"F\", name: \"Ferien\", factor: 100, reduceVacation: true, active: true, color: \"#2e7d32\" },\n\t{ code: \"K\", name: \"Krankheit\", factor: 100, reduceVacation: false, active: false, color: \"#c62828\" },\n\t{ code: \"U\", name: \"Unfall\", factor: 100, reduceVacation: false, active: false, color: \"#ef6c00\" },\n\t{ code: \"M\", name: \"Milit\u00E4r\", factor: 100, reduceVacation: false, active: false, color: \"#455a64\" },\n\t{ code: \"I\", name: \"Intern\", factor: 100, reduceVacation: false, active: true, color: \"#1565c0\" },\n\t{ code: \"W\", name: \"Weiterbildung\", factor: 50, reduceVacation: false, active: true, color: \"#6a1b9a\" },\n\t{ code: \"E\", name: \"Extern\", factor: 50, reduceVacation: false, active: true, color: \"#795548\" },\n];\n\n/** Default instance settings (specification section 2.9.9); existing values are never overwritten. */\nexport const SETTING_DEFAULTS: Record<string, string> = {\n\tedit_window_days: \"7\",\n\tquick_round_minutes: \"0\",\n\tabsence_calc_until_today: \"1\",\n\tabsence_deduct_worktime: \"0\",\n\tprint_limit_days: \"0\",\n\tholiday_country: \"DE\",\n\tdefault_language: \"de\",\n\tsession_ttl_minutes: \"720\",\n\tbackup_retention_days: \"30\",\n\tattendance_list_visible: \"0\",\n\t// how the pause of a day is determined: `auto` = the punched break, otherwise the graduated rules\n\tpause_mode: \"auto\",\n\t// path of a Unicode font for the PDF statement; empty uses the built-in Latin fonts\n\treport_font_path: \"\",\n\t// branding of the installation: logo and background as data URLs (empty = not set), accent colour as hex\n\tbrand_logo: \"\",\n\tbrand_background: \"\",\n\tbrand_color: \"\",\n};\n\n/**\n * Options controlling which initial data is written.\n */\nexport interface SeedOptions {\n\t/** Country used to generate public holidays (default `CH`) */\n\tholidayCountry?: HolidayCountry;\n\t/** Years for which holidays are generated (default: current and next year) */\n\tholidayYears?: number[];\n}\n\n/**\n * Writes all initial data. Idempotent: repeated calls change nothing and never overwrite\n * user-modified settings.\n *\n * @param db - open database handle\n * @param options - seed options\n * @returns summary of what is now present\n */\nexport function seed(db: Db, options: SeedOptions = {}): { permissions: number; holidays: number } {\n\tconst country: HolidayCountry = options.holidayCountry ?? \"DE\";\n\tconst currentYear = new Date().getUTCFullYear();\n\tconst years = options.holidayYears ?? [currentYear, currentYear + 1];\n\n\tconst run = db.transaction((): void => {\n\t\tconst now = Math.floor(Date.now() / 1000);\n\n\t\tconst insertRole = db.prepare(\"INSERT OR IGNORE INTO roles (key, name) VALUES (?, ?)\");\n\t\tinsertRole.run(\"admin\", \"Administrator\");\n\t\tinsertRole.run(\"manager\", \"Manager\");\n\t\tinsertRole.run(\"employee\", \"Employee\");\n\n\t\tconst insertPermission = db.prepare(\"INSERT OR IGNORE INTO permissions (key) VALUES (?)\");\n\t\tfor (const permission of ALL_PERMISSIONS) {\n\t\t\tinsertPermission.run(permission);\n\t\t}\n\n\t\tconst findRole = db.prepare(\"SELECT id FROM roles WHERE key = ?\");\n\t\tconst findPermission = db.prepare(\"SELECT id FROM permissions WHERE key = ?\");\n\t\tconst linkRole = db.prepare(\"INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)\");\n\t\tfor (const [roleKey, permissions] of Object.entries(ROLE_PERMISSIONS)) {\n\t\t\tconst role = findRole.get(roleKey) as { id: number } | undefined;\n\t\t\tif (!role) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const permission of permissions) {\n\t\t\t\tconst row = findPermission.get(permission) as { id: number } | undefined;\n\t\t\t\tif (row) {\n\t\t\t\t\tlinkRole.run(role.id, row.id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst insertAbsenceType = db.prepare(\n\t\t\t`INSERT OR IGNORE INTO absence_types (user_id, code, name, paid, factor, reduce_vacation, is_active, color)\n\t\t\t VALUES (NULL, ?, ?, 1, ?, ?, ?, ?)`,\n\t\t);\n\t\tfor (const type of ABSENCE_TYPES) {\n\t\t\tinsertAbsenceType.run(\n\t\t\t\ttype.code,\n\t\t\t\ttype.name,\n\t\t\t\ttype.factor,\n\t\t\t\ttype.reduceVacation ? 1 : 0,\n\t\t\t\ttype.active ? 1 : 0,\n\t\t\t\ttype.color,\n\t\t\t);\n\t\t}\n\n\t\tconst insertSetting = db.prepare(\n\t\t\t\"INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)\",\n\t\t);\n\t\tfor (const [key, value] of Object.entries(SETTING_DEFAULTS)) {\n\t\t\tinsertSetting.run(key, key === \"holiday_country\" ? country : value, now);\n\t\t}\n\t});\n\trun();\n\n\tlet holidayCount = 0;\n\tfor (const year of years) {\n\t\tholidayCount += ensureHolidaysForYear(db, country, year);\n\t}\n\n\tconst permissionCount = (db.prepare(\"SELECT COUNT(*) AS c FROM permissions\").get() as { c: number }).c;\n\treturn { permissions: permissionCount, holidays: holidayCount };\n}\n\n/**\n * Generates and stores the public holidays of one year for a country (idempotent).\n *\n * @param db - open database handle\n * @param country - supported country code\n * @param year - four digit year\n * @returns number of inserted holiday rows\n */\nexport function ensureHolidaysForYear(db: Db, country: HolidayCountry, year: number): number {\n\tconst insert = db.prepare(\"INSERT OR IGNORE INTO holidays (region, year, date, name, key) VALUES (?, ?, ?, ?, ?)\");\n\tlet inserted = 0;\n\tconst run = db.transaction((): void => {\n\t\tfor (const holiday of holidaysForYear(year, country)) {\n\t\t\tinserted += insert.run(country, year, holiday.date, holiday.name, holiday.key).changes;\n\t\t}\n\t});\n\trun();\n\treturn inserted;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -28,6 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var pdf_exports = {};
30
30
  __export(pdf_exports, {
31
+ LAYOUT: () => LAYOUT,
31
32
  buildMonthStatement: () => buildMonthStatement
32
33
  });
33
34
  module.exports = __toCommonJS(pdf_exports);
@@ -257,8 +258,9 @@ async function buildMonthStatement(input) {
257
258
  { bold: true, color: totals.balance < 0 ? "#c62828" : "#000000", topBorder: true }
258
259
  );
259
260
  if (input.absences.length > 0) {
261
+ const contentWidth = doc.page.width - 2 * LAYOUT.margin;
260
262
  doc.moveDown(1);
261
- doc.font(fonts.bold).fontSize(10).fillColor("#000000").text(labels.absences);
263
+ doc.font(fonts.bold).fontSize(10).fillColor("#000000").text(labels.absences, LAYOUT.margin, doc.y, { width: contentWidth });
262
264
  doc.moveDown(0.3);
263
265
  for (const absence of input.absences) {
264
266
  const portion = absence.dayPortion === 0.5 ? labels.halfDay : labels.fullDay;
@@ -266,7 +268,10 @@ async function buildMonthStatement(input) {
266
268
  `${absence.typeCode} - ${absence.typeName}: ${formatDate(absence.dateFrom, locale)} - ${formatDate(
267
269
  absence.dateTo,
268
270
  locale
269
- )}, ${portion}${absence.hours === null ? "" : `, ${absence.hours} h`}`
271
+ )}, ${portion}${absence.hours === null ? "" : `, ${absence.hours} h`}`,
272
+ LAYOUT.margin,
273
+ doc.y,
274
+ { width: contentWidth }
270
275
  );
271
276
  }
272
277
  }
@@ -303,6 +308,7 @@ async function buildMonthStatement(input) {
303
308
  }
304
309
  // Annotate the CommonJS export names for ESM import in node:
305
310
  0 && (module.exports = {
311
+ LAYOUT,
306
312
  buildMonthStatement
307
313
  });
308
314
  //# sourceMappingURL=pdf.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/reports/pdf.ts"],
4
- "sourcesContent": ["/**\n * Monthly work time statement as a PDF.\n *\n * `/api/reports/pdf` returns this file. It shows the same content as the Excel statement (header block, one row\n * per day, totals, absences, signature lines), drawn with `pdfkit`.\n *\n * **Fonts:** the built-in PDF fonts (`Helvetica`) only cover Latin-1, so Cyrillic (`ru`, `uk`) and Chinese\n * (`zh-cn`) cannot be rendered with them. Instead of producing empty boxes, the renderer refuses those\n * languages with a clear error until a Unicode font is configured (`report_font_path` setting, a `.ttf`/`.otf`\n * that covers the script). Latin statements work without any additional file, which keeps the adapter package\n * free of a multi-megabyte font.\n *\n * Text is written **uncompressed** (`compress: false`): a monthly statement is a few kilobytes, and an\n * uncompressed content stream keeps the file verifiable (and testable) without a PDF library.\n */\n\nimport * as fs from \"node:fs\";\nimport PDFDocument from \"pdfkit\";\nimport { ValidationError } from \"../errors\";\nimport type { ReportLabels } from \"./labels\";\nimport type { ReportInput } from \"./types\";\n\n/** Input of the PDF statement. */\nexport type PdfStatementInput = ReportInput & {\n\t/** Name of the generator, e.g. `time-tracker 0.0.1` */\n\tgenerator: string;\n\t/** Path of a Unicode font (`.ttf`/`.otf`), `null`/empty to use the built-in fonts */\n\tfontPath?: string | null;\n};\n\n/** Languages whose script the built-in PDF fonts cannot display. */\nconst NON_LATIN_LANGUAGES = new Set([\"ru\", \"uk\", \"zh-cn\"]);\n\n/** Characters WinAnsiEncoding knows in addition to Latin-1 \u2014 anything else cannot be drawn by the built-in fonts. */\nconst WIN_ANSI_EXTRAS = \"\u20AC\u201A\u0192\u201E\u2026\u2020\u2021\u02C6\u2030\u0160\u2039\u0152\u017D\u2018\u2019\u201C\u201D\u2022\u2013\u2014\u02DC\u2122\u0161\u203A\u0153\u017E\u0178\";\n\n/**\n * Reports whether the built-in fonts can draw a text.\n *\n * The built-in fonts use WinAnsiEncoding, which covers Latin-1 and a handful of typographic characters. A Polish\n * \"Nieobecno\u015B\u0107\" is outside of it: the renderer would draw nonsense instead of the word, so such a text counts as\n * undrawable.\n *\n * @param text - text to check\n * @returns true when every character is part of WinAnsiEncoding\n */\nfunction fitsBuiltInFont(text: string): boolean {\n\treturn [...text].every(character => character.charCodeAt(0) <= 0xff || WIN_ANSI_EXTRAS.includes(character));\n}\n\n/** Fonts the renderer works with. */\ninterface FontChoice {\n\t/** Font of the regular text */\n\tregular: string;\n\t/** Font of the headings */\n\tbold: string;\n\t/** True when a file of the administrator is used */\n\tembedded: boolean;\n}\n\n/** Margins and text sizes of the statement (A4 portrait, points). */\nconst LAYOUT = {\n\tmargin: 40,\n\tfontSize: 9,\n\tlineHeight: 14,\n} as const;\n\n/** Distance between two columns, so that texts never touch. */\nconst COLUMN_PADDING = 6;\n\n/**\n * Font sizes tried for the column headings, the largest first.\n *\n * A heading has to stay on one line: \"Arbeitszeit\" is about 57 points wide at 9 points, the Ukrainian\n * \"\u0412\u0456\u0434\u043F\u0440\u0430\u0446\u044C\u043E\u0432\u0430\u043D\u043E\" needs even more, and this renderer breaks a word when the column is too narrow for it (the\n * option `lineBreak: false` does not prevent that). The headings are therefore measured and the size steps down\n * until every one of them fits.\n */\nconst HEADER_SIZES = [9, 8, 7, 6.5] as const;\n\n/** The columns of the day table, in the order they are drawn. */\nconst COLUMNS: {\n\t/** Key in the values object and in the labels */\n\tkey: keyof ReportLabels;\n\t/** Alignment of the text inside the cell */\n\talign: \"left\" | \"right\";\n\t/** A typical value: the column is at least wide enough for it */\n\tsample: string;\n\t/** Share of the width left over after each column got what it needs */\n\tgrow: number;\n}[] = [\n\t{ key: \"date\", align: \"left\", sample: \"Mi., 30.09.2026\", grow: 0.35 },\n\t{ key: \"timeIn\", align: \"right\", sample: \"08:00\", grow: 0 },\n\t{ key: \"timeOut\", align: \"right\", sample: \"08:00\", grow: 0 },\n\t{ key: \"worked\", align: \"right\", sample: \"00:00\", grow: 0 },\n\t{ key: \"breaks\", align: \"right\", sample: \"00:00\", grow: 0 },\n\t{ key: \"paidBreaks\", align: \"right\", sample: \"00:00\", grow: 0 },\n\t{ key: \"target\", align: \"right\", sample: \"00:00\", grow: 0 },\n\t{ key: \"balance\", align: \"right\", sample: \"-00:00\", grow: 0 },\n\t{ key: \"absence\", align: \"left\", sample: \"F 50 %\", grow: 0 },\n\t{ key: \"note\", align: \"left\", sample: \"Feiertag, offen\", grow: 0.65 },\n];\n\n/** A column with the position and the width the renderer measured. */\ninterface MeasuredColumn {\n\t/** Key in the values object */\n\tkey: keyof ReportLabels;\n\t/** Left edge in points */\n\tx: number;\n\t/** Width in points */\n\twidth: number;\n\t/** Alignment of the text inside the cell */\n\talign: \"left\" | \"right\";\n}\n\n/**\n * Chooses the fonts of a statement.\n *\n * @param language - language key of the labels, e.g. `ru`\n * @param fontPath - configured font file\n * @param labels - labels of the employee's language\n * @returns the fonts to use\n */\nfunction chooseFonts(language: string, fontPath: string | null | undefined, labels: ReportLabels): FontChoice {\n\tconst configured = (fontPath ?? \"\").trim();\n\tif (configured !== \"\" && fs.existsSync(configured)) {\n\t\t// one file is used for regular and bold text: a statement must not depend on a second file being there\n\t\treturn { regular: configured, bold: configured, embedded: true };\n\t}\n\t// the language list is the fast path; the labels catch a language whose letters are outside WinAnsiEncoding\n\t// (Polish \"Nieobecno\u015B\u0107\", \"\u015Bwi\u0119to\" or \"ca\u0142y dzie\u0144\") \u2014 drawing them would produce nonsense, not a statement\n\tconst undrawable = Object.values(labels).filter(text => !fitsBuiltInFont(text));\n\tif (NON_LATIN_LANGUAGES.has(language) || undrawable.length > 0) {\n\t\tthrow new ValidationError(\n\t\t\t`language \"${language}\" needs a Unicode font: set report_font_path to a .ttf/.otf file that covers the script`,\n\t\t);\n\t}\n\treturn { regular: \"Helvetica\", bold: \"Helvetica-Bold\", embedded: false };\n}\n\n/**\n * Measures the columns of the day table.\n *\n * Every column is at least as wide as its heading, so a heading never has to be broken: a column sized for the\n * value \"9:30\" would otherwise split \"Arbeitszeit\" in the middle of the word. The width left over after that\n * goes to the date and to the note \u2014 the two columns that carry free text.\n *\n * @param doc - document the texts are measured on\n * @param fonts - fonts of the statement\n * @param labels - labels of the employee's language\n * @param samples - the texts of a column, so the column is wide enough for every one of them\n * @returns the columns and the font size of the heading row\n */\nfunction measureColumns(\n\tdoc: PDFKit.PDFDocument,\n\tfonts: FontChoice,\n\tlabels: ReportLabels,\n\tsamples: Partial<Record<keyof ReportLabels, string[]>>,\n): { columns: MeasuredColumn[]; headerSize: number } {\n\tconst available = doc.page.width - 2 * LAYOUT.margin;\n\tconst smallest = HEADER_SIZES.length - 1;\n\n\tfor (const [index, size] of HEADER_SIZES.entries()) {\n\t\tconst needed = COLUMNS.map(column => {\n\t\t\tdoc.font(fonts.bold).fontSize(size);\n\t\t\tconst heading = doc.widthOfString(labels[column.key]);\n\t\t\tdoc.font(fonts.regular).fontSize(LAYOUT.fontSize);\n\t\t\t// every value of the column is measured: a total is wider than a single day (\"187:00\" against\n\t\t\t// \"8:00\"), and measuring only a sample would break that number in the middle\n\t\t\tconst texts = samples[column.key] ?? [column.sample];\n\t\t\tconst value = Math.max(\n\t\t\t\t...(texts.length > 0 ? texts : [column.sample]).map(text => doc.widthOfString(text)),\n\t\t\t);\n\t\t\t// two paddings: one is the gap to the next column, the other keeps the text off the edge \u2014 a text that\n\t\t\t// fills its box to the last point is wrapped (or cut) after the last character by the renderer\n\t\t\treturn Math.max(heading, value) + 2 * COLUMN_PADDING;\n\t\t});\n\t\tconst total = needed.reduce((sum, width) => sum + width, 0);\n\t\tif (total > available && index < smallest) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst leftover = Math.max(0, available - total);\n\t\tlet x = LAYOUT.margin;\n\t\tconst columns = COLUMNS.map((column, position) => {\n\t\t\tconst width = needed[position] + leftover * column.grow;\n\t\t\tconst measured: MeasuredColumn = { key: column.key, x, width, align: column.align };\n\t\t\tx += width;\n\t\t\treturn measured;\n\t\t});\n\t\treturn { columns, headerSize: size };\n\t}\n\n\t// not reachable: the smallest size always fits\n\treturn { columns: [], headerSize: HEADER_SIZES[smallest] };\n}\n\n/**\n * Formats a local date for display.\n *\n * @param date - local date, `YYYY-MM-DD`\n * @param locale - locale of the employee\n * @returns formatted date, e.g. `Mo, 14.09.2026`\n */\nfunction formatDate(date: string, locale: string): string {\n\treturn new Intl.DateTimeFormat(locale, {\n\t\tweekday: \"short\",\n\t\tday: \"2-digit\",\n\t\tmonth: \"2-digit\",\n\t\tyear: \"numeric\",\n\t\ttimeZone: \"UTC\",\n\t}).format(new Date(`${date}T12:00:00Z`));\n}\n\n/**\n * Formats an instant as time of day in a time zone.\n *\n * @param tsUtc - UTC epoch seconds or `null`\n * @param timeZone - IANA time zone of the employee\n * @param locale - locale of the employee\n * @returns formatted time or an empty string\n */\nfunction formatTime(tsUtc: number | null, timeZone: string, locale: string): string {\n\tif (tsUtc === null) {\n\t\treturn \"\";\n\t}\n\treturn new Intl.DateTimeFormat(locale, { timeZone, hour: \"2-digit\", minute: \"2-digit\" }).format(\n\t\tnew Date(tsUtc * 1000),\n\t);\n}\n\n/**\n * Formats minutes as `H:MM`.\n *\n * @param minutes - duration in minutes\n * @returns formatted duration, e.g. `9:30`\n */\nfunction formatMinutes(minutes: number): string {\n\tconst sign = minutes < 0 ? \"-\" : \"\";\n\tconst absolute = Math.abs(minutes);\n\treturn `${sign}${Math.floor(absolute / 60)}:${String(absolute % 60).padStart(2, \"0\")}`;\n}\n\n/**\n * Formats an instant for the header block.\n *\n * @param tsUtc - UTC epoch seconds\n * @param locale - locale of the employee\n * @param timeZone - IANA time zone of the employee\n * @returns formatted date and time\n */\nfunction formatStamp(tsUtc: number, locale: string, timeZone: string): string {\n\treturn new Intl.DateTimeFormat(locale, { timeZone, dateStyle: \"short\", timeStyle: \"short\" }).format(\n\t\tnew Date(tsUtc * 1000),\n\t);\n}\n\n/**\n * Builds the monthly statement as a PDF.\n *\n * @param input - employee, month, days, absences, labels and font\n * @returns the document as bytes\n */\nexport async function buildMonthStatement(input: PdfStatementInput): Promise<Buffer> {\n\tconst { labels, locale, user } = input;\n\tconst fonts = chooseFonts(input.language, input.fontPath, labels);\n\tconst from = `${input.year}-${String(input.month).padStart(2, \"0\")}-01`;\n\tconst lastDay = new Date(Date.UTC(input.year, input.month, 0)).getUTCDate();\n\tconst to = `${input.year}-${String(input.month).padStart(2, \"0\")}-${String(lastDay).padStart(2, \"0\")}`;\n\tconst generated = formatStamp(input.generatedAt, locale, user.timezone);\n\n\tconst doc = new PDFDocument({\n\t\tsize: \"A4\",\n\t\tmargin: LAYOUT.margin,\n\t\t// an uncompressed stream keeps the statement checkable without a PDF reader\n\t\tcompress: false,\n\t\tinfo: {\n\t\t\tTitle: `${labels.title} ${input.year}-${String(input.month).padStart(2, \"0\")}`,\n\t\t\tAuthor: input.generator,\n\t\t\tCreator: input.generator,\n\t\t\tProducer: input.generator,\n\t\t\tSubject: `${user.displayName} (${user.login})`,\n\t\t},\n\t});\n\n\tconst chunks: Buffer[] = [];\n\tdoc.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n\tconst finished = new Promise<void>(resolve => doc.on(\"end\", resolve));\n\n\tconst pageBottom = doc.page.height - LAYOUT.margin - 24;\n\t/**\n\t * Returns the longer of two texts.\n\t *\n\t * @param first - first text\n\t * @param second - second text\n\t * @returns the longer text\n\t */\n\t// The values of every column are summed up here: the totals are drawn in the row below and their width is\n\t// what the columns are measured against, so a total like \"187:00\" gets the room it needs.\n\tconst totals = input.days.reduce(\n\t\t(sum, day) => ({\n\t\t\tworked: sum.worked + day.workedMin,\n\t\t\tbreaks: sum.breaks + day.breakMin,\n\t\t\tpaidBreaks: sum.paidBreaks + day.paidBreakMin,\n\t\t\ttarget: sum.target + day.targetMin,\n\t\t\tbalance: sum.balance + day.balanceMin,\n\t\t}),\n\t\t{ worked: 0, breaks: 0, paidBreaks: 0, target: 0, balance: 0 },\n\t);\n\t/** The widest time of a day: in English a time carries an \"AM\"/\"PM\" marker, so \"11:59 PM\" is what fits. */\n\tconst widestTime = formatTime(Date.UTC(2026, 0, 1, 23, 59) / 1000, \"UTC\", locale);\n\n\tconst { columns, headerSize } = measureColumns(doc, fonts, labels, {\n\t\t// the date column holds the dates of the report: \"czw., 30.09.2026\" is wider than \"Thu, 30.09.2026\"\n\t\tdate: input.days.map(day => formatDate(day.localDate, locale)),\n\t\ttimeIn: [...input.days.map(day => formatTime(day.firstInUtc, user.timezone, locale)), widestTime],\n\t\ttimeOut: [...input.days.map(day => formatTime(day.lastOutUtc, user.timezone, locale)), widestTime],\n\t\tworked: [...input.days.map(day => formatMinutes(day.workedMin)), formatMinutes(totals.worked)],\n\t\tbreaks: [...input.days.map(day => formatMinutes(day.breakMin)), formatMinutes(totals.breaks)],\n\t\tpaidBreaks: [...input.days.map(day => formatMinutes(day.paidBreakMin)), formatMinutes(totals.paidBreaks)],\n\t\ttarget: [...input.days.map(day => formatMinutes(day.targetMin)), formatMinutes(totals.target)],\n\t\tbalance: [...input.days.map(day => formatMinutes(day.balanceMin)), formatMinutes(totals.balance)],\n\t\t// the note column carries the marks of a day: a public holiday and a punch without a counterpart\n\t\tnote: [labels.holiday, labels.openEntry, `${labels.holiday}, ${labels.openEntry}`],\n\t});\n\n\t/**\n\t * Draws one line of the day table.\n\t *\n\t * @param values - column values by key\n\t * @param options - styling\n\t * @param options.bold - true for headings and totals\n\t * @param options.color - text colour\n\t * @param options.topBorder - true to draw a line above the row\n\t * @param options.fontSize - text size, defaults to the size of the rows\n\t */\n\tconst drawRow = (\n\t\tvalues: Partial<Record<keyof ReportLabels, string>>,\n\t\toptions: { bold?: boolean; color?: string; topBorder?: boolean; fontSize?: number } = {},\n\t): void => {\n\t\tconst y = doc.y;\n\t\tif (options.topBorder) {\n\t\t\tdoc.moveTo(LAYOUT.margin, y - 3)\n\t\t\t\t.lineTo(doc.page.width - LAYOUT.margin, y - 3)\n\t\t\t\t.lineWidth(0.5)\n\t\t\t\t.strokeColor(\"#9aa5b1\")\n\t\t\t\t.stroke();\n\t\t\tdoc.y = y;\n\t\t}\n\t\tdoc.font(options.bold ? fonts.bold : fonts.regular).fontSize(options.fontSize ?? LAYOUT.fontSize);\n\t\tdoc.fillColor(options.color ?? \"#000000\");\n\t\tfor (const column of columns) {\n\t\t\t// the width of the drawn box is one padding narrower than the column: without that gap a right\n\t\t\t// aligned value would end exactly where the next column starts (\"SaldoAbwesenheit\")\n\t\t\tdoc.text(values[column.key] ?? \"\", column.x, y, {\n\t\t\t\twidth: Math.max(1, column.width - COLUMN_PADDING),\n\t\t\t\talign: column.align,\n\t\t\t\tlineBreak: false,\n\t\t\t});\n\t\t}\n\t\tdoc.y = y + LAYOUT.lineHeight;\n\t};\n\n\t/** Draws the column headings and the line below them. */\n\tconst drawHeaderRow = (): void => {\n\t\tconst headings: Partial<Record<keyof ReportLabels, string>> = {};\n\t\tfor (const column of columns) {\n\t\t\theadings[column.key] = labels[column.key];\n\t\t}\n\t\tdrawRow(headings, { bold: true, fontSize: headerSize });\n\t\tdoc.moveTo(LAYOUT.margin, doc.y - 3)\n\t\t\t.lineTo(doc.page.width - LAYOUT.margin, doc.y - 3)\n\t\t\t.lineWidth(0.5)\n\t\t\t.strokeColor(\"#9aa5b1\")\n\t\t\t.stroke();\n\t};\n\n\t// --- header block ---------------------------------------------------------\n\tdoc.font(fonts.bold).fontSize(16).fillColor(\"#000000\").text(labels.title);\n\tdoc.moveDown(0.3);\n\tdoc.font(fonts.regular).fontSize(10);\n\tdoc.text(`${labels.employee}: ${user.displayName} (${user.login})`);\n\tdoc.text(`${labels.period}: ${formatDate(from, locale)} - ${formatDate(to, locale)}`);\n\tdoc.text(`${labels.created}: ${generated}`);\n\tdoc.moveDown(0.8);\n\n\t// --- days -----------------------------------------------------------------\n\tdrawHeaderRow();\n\n\tfor (const day of input.days) {\n\t\t// one more page when the month does not fit (defensive: 31 rows always fit on one A4 page)\n\t\tif (doc.y + LAYOUT.lineHeight > pageBottom) {\n\t\t\tdoc.addPage();\n\t\t\tdoc.font(fonts.regular).fontSize(8).fillColor(\"#757575\").text(labels.title, LAYOUT.margin, LAYOUT.margin);\n\t\t\tdoc.moveDown(0.5);\n\t\t\tdrawHeaderRow();\n\t\t}\n\n\t\tconst notes: string[] = [];\n\t\tif (day.isHoliday) {\n\t\t\tnotes.push(labels.holiday);\n\t\t}\n\t\tif (day.hasOpenEntry) {\n\t\t\tnotes.push(labels.openEntry);\n\t\t}\n\n\t\tdrawRow(\n\t\t\t{\n\t\t\t\tdate: formatDate(day.localDate, locale),\n\t\t\t\ttimeIn: formatTime(day.firstInUtc, user.timezone, locale),\n\t\t\t\ttimeOut: formatTime(day.lastOutUtc, user.timezone, locale),\n\t\t\t\tworked: formatMinutes(day.workedMin),\n\t\t\t\tbreaks: formatMinutes(day.breakMin),\n\t\t\t\tpaidBreaks: formatMinutes(day.paidBreakMin),\n\t\t\t\ttarget: formatMinutes(day.targetMin),\n\t\t\t\tbalance: formatMinutes(day.balanceMin),\n\t\t\t\tabsence: day.absenceCode ?? \"\",\n\t\t\t\tnote: notes.join(\", \"),\n\t\t\t},\n\t\t\t{\n\t\t\t\tcolor: day.balanceMin < 0 ? \"#c62828\" : day.isHoliday || day.workedMin === 0 ? \"#757575\" : \"#000000\",\n\t\t\t},\n\t\t);\n\t}\n\n\tdrawRow(\n\t\t{\n\t\t\tdate: labels.total,\n\t\t\tworked: formatMinutes(totals.worked),\n\t\t\tbreaks: formatMinutes(totals.breaks),\n\t\t\tpaidBreaks: formatMinutes(totals.paidBreaks),\n\t\t\ttarget: formatMinutes(totals.target),\n\t\t\tbalance: formatMinutes(totals.balance),\n\t\t\tabsence: `${labels.days}: ${input.days.length}`,\n\t\t},\n\t\t{ bold: true, color: totals.balance < 0 ? \"#c62828\" : \"#000000\", topBorder: true },\n\t);\n\n\t// --- absences -------------------------------------------------------------\n\tif (input.absences.length > 0) {\n\t\tdoc.moveDown(1);\n\t\tdoc.font(fonts.bold).fontSize(10).fillColor(\"#000000\").text(labels.absences);\n\t\tdoc.moveDown(0.3);\n\t\tfor (const absence of input.absences) {\n\t\t\tconst portion = absence.dayPortion === 0.5 ? labels.halfDay : labels.fullDay;\n\t\t\tdoc.font(fonts.regular)\n\t\t\t\t.fontSize(LAYOUT.fontSize)\n\t\t\t\t.fillColor(\"#000000\")\n\t\t\t\t.text(\n\t\t\t\t\t`${absence.typeCode} - ${absence.typeName}: ${formatDate(absence.dateFrom, locale)} - ${formatDate(\n\t\t\t\t\t\tabsence.dateTo,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t)}, ${portion}${absence.hours === null ? \"\" : `, ${absence.hours} h`}`,\n\t\t\t\t);\n\t\t}\n\t}\n\n\t// --- signatures -----------------------------------------------------------\n\t// the label is drawn and the line is ruled: a long label plus a string of underscores is wrapped by the\n\t// renderer (the Russian and Ukrainian labels are long), and then the two signature lines are no longer level\n\t//\n\t// The block sits near the bottom of the page: a statement ends with two signature lines, so an empty area\n\t// above them looks like the document was cut off. It stays below the content when that reaches far down.\n\tconst signatureGap = 16;\n\tconst signatureWidth = (doc.page.width - 2 * LAYOUT.margin - signatureGap) / 2;\n\tconst signaturePreferred = doc.page.height - 30 - LAYOUT.lineHeight - 24;\n\tconst signatureY = Math.max(doc.y + 24, Math.min(signaturePreferred, pageBottom - LAYOUT.lineHeight));\n\n\t/**\n\t * Draws one signature line: the label followed by the rule to sign on.\n\t *\n\t * @param label - text before the line\n\t * @param x - left edge of the block\n\t */\n\tconst drawSignature = (label: string, x: number): void => {\n\t\tdoc.font(fonts.regular).fontSize(LAYOUT.fontSize).fillColor(\"#000000\");\n\t\tconst written = `${label}:`;\n\t\t// without a width limit the label is never broken \u2014 the block is wide enough for all eleven languages\n\t\tdoc.text(written, x, signatureY, { lineBreak: false });\n\t\tconst start = Math.min(x + doc.widthOfString(written) + 6, x + signatureWidth - 24);\n\t\tdoc.moveTo(start, signatureY + LAYOUT.fontSize + 2)\n\t\t\t.lineTo(x + signatureWidth, signatureY + LAYOUT.fontSize + 2)\n\t\t\t.lineWidth(0.5)\n\t\t\t.strokeColor(\"#000000\")\n\t\t\t.stroke();\n\t\tdoc.y = signatureY + LAYOUT.lineHeight;\n\t};\n\n\tdrawSignature(labels.signatureEmployee, LAYOUT.margin);\n\tdrawSignature(labels.signatureManager, LAYOUT.margin + signatureWidth + signatureGap);\n\n\t// --- footer on every page -------------------------------------------------\n\t// the footer sits below the writing area on purpose, so the bottom margin is lifted while it is drawn: PDFKit\n\t// starts a new page when a text crosses the margin, and that page would carry nothing but the footer\n\tconst range = doc.bufferedPageRange();\n\tfor (let index = 0; index < range.count; index++) {\n\t\tdoc.switchToPage(range.start + index);\n\t\tconst bottom = doc.page.margins.bottom;\n\t\tdoc.page.margins.bottom = 0;\n\t\tdoc.font(fonts.regular)\n\t\t\t.fontSize(8)\n\t\t\t.fillColor(\"#757575\")\n\t\t\t.text(\n\t\t\t\t`${input.generator} - ${labels.title} ${input.year}-${String(input.month).padStart(2, \"0\")} - ${\n\t\t\t\t\tindex + 1\n\t\t\t\t}/${range.count}`,\n\t\t\t\tLAYOUT.margin,\n\t\t\t\tdoc.page.height - 30,\n\t\t\t\t{ width: doc.page.width - 2 * LAYOUT.margin, align: \"center\", lineBreak: false },\n\t\t\t);\n\t\tdoc.page.margins.bottom = bottom;\n\t}\n\n\tdoc.end();\n\tawait finished;\n\treturn Buffer.concat(chunks);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Monthly work time statement as a PDF.\n *\n * `/api/reports/pdf` returns this file. It shows the same content as the Excel statement (header block, one row\n * per day, totals, absences, signature lines), drawn with `pdfkit`.\n *\n * **Fonts:** the built-in PDF fonts (`Helvetica`) only cover Latin-1, so Cyrillic (`ru`, `uk`) and Chinese\n * (`zh-cn`) cannot be rendered with them. Instead of producing empty boxes, the renderer refuses those\n * languages with a clear error until a Unicode font is configured (`report_font_path` setting, a `.ttf`/`.otf`\n * that covers the script). Latin statements work without any additional file, which keeps the adapter package\n * free of a multi-megabyte font.\n *\n * Text is written **uncompressed** (`compress: false`): a monthly statement is a few kilobytes, and an\n * uncompressed content stream keeps the file verifiable (and testable) without a PDF library.\n */\n\nimport * as fs from \"node:fs\";\nimport PDFDocument from \"pdfkit\";\nimport { ValidationError } from \"../errors\";\nimport type { ReportLabels } from \"./labels\";\nimport type { ReportInput } from \"./types\";\n\n/** Input of the PDF statement. */\nexport type PdfStatementInput = ReportInput & {\n\t/** Name of the generator, e.g. `time-tracker 0.0.1` */\n\tgenerator: string;\n\t/** Path of a Unicode font (`.ttf`/`.otf`), `null`/empty to use the built-in fonts */\n\tfontPath?: string | null;\n};\n\n/** Languages whose script the built-in PDF fonts cannot display. */\nconst NON_LATIN_LANGUAGES = new Set([\"ru\", \"uk\", \"zh-cn\"]);\n\n/** Characters WinAnsiEncoding knows in addition to Latin-1 \u2014 anything else cannot be drawn by the built-in fonts. */\nconst WIN_ANSI_EXTRAS = \"\u20AC\u201A\u0192\u201E\u2026\u2020\u2021\u02C6\u2030\u0160\u2039\u0152\u017D\u2018\u2019\u201C\u201D\u2022\u2013\u2014\u02DC\u2122\u0161\u203A\u0153\u017E\u0178\";\n\n/**\n * Reports whether the built-in fonts can draw a text.\n *\n * The built-in fonts use WinAnsiEncoding, which covers Latin-1 and a handful of typographic characters. A Polish\n * \"Nieobecno\u015B\u0107\" is outside of it: the renderer would draw nonsense instead of the word, so such a text counts as\n * undrawable.\n *\n * @param text - text to check\n * @returns true when every character is part of WinAnsiEncoding\n */\nfunction fitsBuiltInFont(text: string): boolean {\n\treturn [...text].every(character => character.charCodeAt(0) <= 0xff || WIN_ANSI_EXTRAS.includes(character));\n}\n\n/** Fonts the renderer works with. */\ninterface FontChoice {\n\t/** Font of the regular text */\n\tregular: string;\n\t/** Font of the headings */\n\tbold: string;\n\t/** True when a file of the administrator is used */\n\tembedded: boolean;\n}\n\n/** Margins and text sizes of the statement (A4 portrait, points). */\nexport const LAYOUT = {\n\tmargin: 40,\n\tfontSize: 9,\n\tlineHeight: 14,\n} as const;\n\n/** Distance between two columns, so that texts never touch. */\nconst COLUMN_PADDING = 6;\n\n/**\n * Font sizes tried for the column headings, the largest first.\n *\n * A heading has to stay on one line: \"Arbeitszeit\" is about 57 points wide at 9 points, the Ukrainian\n * \"\u0412\u0456\u0434\u043F\u0440\u0430\u0446\u044C\u043E\u0432\u0430\u043D\u043E\" needs even more, and this renderer breaks a word when the column is too narrow for it (the\n * option `lineBreak: false` does not prevent that). The headings are therefore measured and the size steps down\n * until every one of them fits.\n */\nconst HEADER_SIZES = [9, 8, 7, 6.5] as const;\n\n/** The columns of the day table, in the order they are drawn. */\nconst COLUMNS: {\n\t/** Key in the values object and in the labels */\n\tkey: keyof ReportLabels;\n\t/** Alignment of the text inside the cell */\n\talign: \"left\" | \"right\";\n\t/** A typical value: the column is at least wide enough for it */\n\tsample: string;\n\t/** Share of the width left over after each column got what it needs */\n\tgrow: number;\n}[] = [\n\t{ key: \"date\", align: \"left\", sample: \"Mi., 30.09.2026\", grow: 0.35 },\n\t{ key: \"timeIn\", align: \"right\", sample: \"08:00\", grow: 0 },\n\t{ key: \"timeOut\", align: \"right\", sample: \"08:00\", grow: 0 },\n\t{ key: \"worked\", align: \"right\", sample: \"00:00\", grow: 0 },\n\t{ key: \"breaks\", align: \"right\", sample: \"00:00\", grow: 0 },\n\t{ key: \"paidBreaks\", align: \"right\", sample: \"00:00\", grow: 0 },\n\t{ key: \"target\", align: \"right\", sample: \"00:00\", grow: 0 },\n\t{ key: \"balance\", align: \"right\", sample: \"-00:00\", grow: 0 },\n\t{ key: \"absence\", align: \"left\", sample: \"F 50 %\", grow: 0 },\n\t{ key: \"note\", align: \"left\", sample: \"Feiertag, offen\", grow: 0.65 },\n];\n\n/** A column with the position and the width the renderer measured. */\ninterface MeasuredColumn {\n\t/** Key in the values object */\n\tkey: keyof ReportLabels;\n\t/** Left edge in points */\n\tx: number;\n\t/** Width in points */\n\twidth: number;\n\t/** Alignment of the text inside the cell */\n\talign: \"left\" | \"right\";\n}\n\n/**\n * Chooses the fonts of a statement.\n *\n * @param language - language key of the labels, e.g. `ru`\n * @param fontPath - configured font file\n * @param labels - labels of the employee's language\n * @returns the fonts to use\n */\nfunction chooseFonts(language: string, fontPath: string | null | undefined, labels: ReportLabels): FontChoice {\n\tconst configured = (fontPath ?? \"\").trim();\n\tif (configured !== \"\" && fs.existsSync(configured)) {\n\t\t// one file is used for regular and bold text: a statement must not depend on a second file being there\n\t\treturn { regular: configured, bold: configured, embedded: true };\n\t}\n\t// the language list is the fast path; the labels catch a language whose letters are outside WinAnsiEncoding\n\t// (Polish \"Nieobecno\u015B\u0107\", \"\u015Bwi\u0119to\" or \"ca\u0142y dzie\u0144\") \u2014 drawing them would produce nonsense, not a statement\n\tconst undrawable = Object.values(labels).filter(text => !fitsBuiltInFont(text));\n\tif (NON_LATIN_LANGUAGES.has(language) || undrawable.length > 0) {\n\t\tthrow new ValidationError(\n\t\t\t`language \"${language}\" needs a Unicode font: set report_font_path to a .ttf/.otf file that covers the script`,\n\t\t);\n\t}\n\treturn { regular: \"Helvetica\", bold: \"Helvetica-Bold\", embedded: false };\n}\n\n/**\n * Measures the columns of the day table.\n *\n * Every column is at least as wide as its heading, so a heading never has to be broken: a column sized for the\n * value \"9:30\" would otherwise split \"Arbeitszeit\" in the middle of the word. The width left over after that\n * goes to the date and to the note \u2014 the two columns that carry free text.\n *\n * @param doc - document the texts are measured on\n * @param fonts - fonts of the statement\n * @param labels - labels of the employee's language\n * @param samples - the texts of a column, so the column is wide enough for every one of them\n * @returns the columns and the font size of the heading row\n */\nfunction measureColumns(\n\tdoc: PDFKit.PDFDocument,\n\tfonts: FontChoice,\n\tlabels: ReportLabels,\n\tsamples: Partial<Record<keyof ReportLabels, string[]>>,\n): { columns: MeasuredColumn[]; headerSize: number } {\n\tconst available = doc.page.width - 2 * LAYOUT.margin;\n\tconst smallest = HEADER_SIZES.length - 1;\n\n\tfor (const [index, size] of HEADER_SIZES.entries()) {\n\t\tconst needed = COLUMNS.map(column => {\n\t\t\tdoc.font(fonts.bold).fontSize(size);\n\t\t\tconst heading = doc.widthOfString(labels[column.key]);\n\t\t\tdoc.font(fonts.regular).fontSize(LAYOUT.fontSize);\n\t\t\t// every value of the column is measured: a total is wider than a single day (\"187:00\" against\n\t\t\t// \"8:00\"), and measuring only a sample would break that number in the middle\n\t\t\tconst texts = samples[column.key] ?? [column.sample];\n\t\t\tconst value = Math.max(\n\t\t\t\t...(texts.length > 0 ? texts : [column.sample]).map(text => doc.widthOfString(text)),\n\t\t\t);\n\t\t\t// two paddings: one is the gap to the next column, the other keeps the text off the edge \u2014 a text that\n\t\t\t// fills its box to the last point is wrapped (or cut) after the last character by the renderer\n\t\t\treturn Math.max(heading, value) + 2 * COLUMN_PADDING;\n\t\t});\n\t\tconst total = needed.reduce((sum, width) => sum + width, 0);\n\t\tif (total > available && index < smallest) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst leftover = Math.max(0, available - total);\n\t\tlet x = LAYOUT.margin;\n\t\tconst columns = COLUMNS.map((column, position) => {\n\t\t\tconst width = needed[position] + leftover * column.grow;\n\t\t\tconst measured: MeasuredColumn = { key: column.key, x, width, align: column.align };\n\t\t\tx += width;\n\t\t\treturn measured;\n\t\t});\n\t\treturn { columns, headerSize: size };\n\t}\n\n\t// not reachable: the smallest size always fits\n\treturn { columns: [], headerSize: HEADER_SIZES[smallest] };\n}\n\n/**\n * Formats a local date for display.\n *\n * @param date - local date, `YYYY-MM-DD`\n * @param locale - locale of the employee\n * @returns formatted date, e.g. `Mo, 14.09.2026`\n */\nfunction formatDate(date: string, locale: string): string {\n\treturn new Intl.DateTimeFormat(locale, {\n\t\tweekday: \"short\",\n\t\tday: \"2-digit\",\n\t\tmonth: \"2-digit\",\n\t\tyear: \"numeric\",\n\t\ttimeZone: \"UTC\",\n\t}).format(new Date(`${date}T12:00:00Z`));\n}\n\n/**\n * Formats an instant as time of day in a time zone.\n *\n * @param tsUtc - UTC epoch seconds or `null`\n * @param timeZone - IANA time zone of the employee\n * @param locale - locale of the employee\n * @returns formatted time or an empty string\n */\nfunction formatTime(tsUtc: number | null, timeZone: string, locale: string): string {\n\tif (tsUtc === null) {\n\t\treturn \"\";\n\t}\n\treturn new Intl.DateTimeFormat(locale, { timeZone, hour: \"2-digit\", minute: \"2-digit\" }).format(\n\t\tnew Date(tsUtc * 1000),\n\t);\n}\n\n/**\n * Formats minutes as `H:MM`.\n *\n * @param minutes - duration in minutes\n * @returns formatted duration, e.g. `9:30`\n */\nfunction formatMinutes(minutes: number): string {\n\tconst sign = minutes < 0 ? \"-\" : \"\";\n\tconst absolute = Math.abs(minutes);\n\treturn `${sign}${Math.floor(absolute / 60)}:${String(absolute % 60).padStart(2, \"0\")}`;\n}\n\n/**\n * Formats an instant for the header block.\n *\n * @param tsUtc - UTC epoch seconds\n * @param locale - locale of the employee\n * @param timeZone - IANA time zone of the employee\n * @returns formatted date and time\n */\nfunction formatStamp(tsUtc: number, locale: string, timeZone: string): string {\n\treturn new Intl.DateTimeFormat(locale, { timeZone, dateStyle: \"short\", timeStyle: \"short\" }).format(\n\t\tnew Date(tsUtc * 1000),\n\t);\n}\n\n/**\n * Builds the monthly statement as a PDF.\n *\n * @param input - employee, month, days, absences, labels and font\n * @returns the document as bytes\n */\nexport async function buildMonthStatement(input: PdfStatementInput): Promise<Buffer> {\n\tconst { labels, locale, user } = input;\n\tconst fonts = chooseFonts(input.language, input.fontPath, labels);\n\tconst from = `${input.year}-${String(input.month).padStart(2, \"0\")}-01`;\n\tconst lastDay = new Date(Date.UTC(input.year, input.month, 0)).getUTCDate();\n\tconst to = `${input.year}-${String(input.month).padStart(2, \"0\")}-${String(lastDay).padStart(2, \"0\")}`;\n\tconst generated = formatStamp(input.generatedAt, locale, user.timezone);\n\n\tconst doc = new PDFDocument({\n\t\tsize: \"A4\",\n\t\tmargin: LAYOUT.margin,\n\t\t// an uncompressed stream keeps the statement checkable without a PDF reader\n\t\tcompress: false,\n\t\tinfo: {\n\t\t\tTitle: `${labels.title} ${input.year}-${String(input.month).padStart(2, \"0\")}`,\n\t\t\tAuthor: input.generator,\n\t\t\tCreator: input.generator,\n\t\t\tProducer: input.generator,\n\t\t\tSubject: `${user.displayName} (${user.login})`,\n\t\t},\n\t});\n\n\tconst chunks: Buffer[] = [];\n\tdoc.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n\tconst finished = new Promise<void>(resolve => doc.on(\"end\", resolve));\n\n\tconst pageBottom = doc.page.height - LAYOUT.margin - 24;\n\t/**\n\t * Returns the longer of two texts.\n\t *\n\t * @param first - first text\n\t * @param second - second text\n\t * @returns the longer text\n\t */\n\t// The values of every column are summed up here: the totals are drawn in the row below and their width is\n\t// what the columns are measured against, so a total like \"187:00\" gets the room it needs.\n\tconst totals = input.days.reduce(\n\t\t(sum, day) => ({\n\t\t\tworked: sum.worked + day.workedMin,\n\t\t\tbreaks: sum.breaks + day.breakMin,\n\t\t\tpaidBreaks: sum.paidBreaks + day.paidBreakMin,\n\t\t\ttarget: sum.target + day.targetMin,\n\t\t\tbalance: sum.balance + day.balanceMin,\n\t\t}),\n\t\t{ worked: 0, breaks: 0, paidBreaks: 0, target: 0, balance: 0 },\n\t);\n\t/** The widest time of a day: in English a time carries an \"AM\"/\"PM\" marker, so \"11:59 PM\" is what fits. */\n\tconst widestTime = formatTime(Date.UTC(2026, 0, 1, 23, 59) / 1000, \"UTC\", locale);\n\n\tconst { columns, headerSize } = measureColumns(doc, fonts, labels, {\n\t\t// the date column holds the dates of the report: \"czw., 30.09.2026\" is wider than \"Thu, 30.09.2026\"\n\t\tdate: input.days.map(day => formatDate(day.localDate, locale)),\n\t\ttimeIn: [...input.days.map(day => formatTime(day.firstInUtc, user.timezone, locale)), widestTime],\n\t\ttimeOut: [...input.days.map(day => formatTime(day.lastOutUtc, user.timezone, locale)), widestTime],\n\t\tworked: [...input.days.map(day => formatMinutes(day.workedMin)), formatMinutes(totals.worked)],\n\t\tbreaks: [...input.days.map(day => formatMinutes(day.breakMin)), formatMinutes(totals.breaks)],\n\t\tpaidBreaks: [...input.days.map(day => formatMinutes(day.paidBreakMin)), formatMinutes(totals.paidBreaks)],\n\t\ttarget: [...input.days.map(day => formatMinutes(day.targetMin)), formatMinutes(totals.target)],\n\t\tbalance: [...input.days.map(day => formatMinutes(day.balanceMin)), formatMinutes(totals.balance)],\n\t\t// the note column carries the marks of a day: a public holiday and a punch without a counterpart\n\t\tnote: [labels.holiday, labels.openEntry, `${labels.holiday}, ${labels.openEntry}`],\n\t});\n\n\t/**\n\t * Draws one line of the day table.\n\t *\n\t * @param values - column values by key\n\t * @param options - styling\n\t * @param options.bold - true for headings and totals\n\t * @param options.color - text colour\n\t * @param options.topBorder - true to draw a line above the row\n\t * @param options.fontSize - text size, defaults to the size of the rows\n\t */\n\tconst drawRow = (\n\t\tvalues: Partial<Record<keyof ReportLabels, string>>,\n\t\toptions: { bold?: boolean; color?: string; topBorder?: boolean; fontSize?: number } = {},\n\t): void => {\n\t\tconst y = doc.y;\n\t\tif (options.topBorder) {\n\t\t\tdoc.moveTo(LAYOUT.margin, y - 3)\n\t\t\t\t.lineTo(doc.page.width - LAYOUT.margin, y - 3)\n\t\t\t\t.lineWidth(0.5)\n\t\t\t\t.strokeColor(\"#9aa5b1\")\n\t\t\t\t.stroke();\n\t\t\tdoc.y = y;\n\t\t}\n\t\tdoc.font(options.bold ? fonts.bold : fonts.regular).fontSize(options.fontSize ?? LAYOUT.fontSize);\n\t\tdoc.fillColor(options.color ?? \"#000000\");\n\t\tfor (const column of columns) {\n\t\t\t// the width of the drawn box is one padding narrower than the column: without that gap a right\n\t\t\t// aligned value would end exactly where the next column starts (\"SaldoAbwesenheit\")\n\t\t\tdoc.text(values[column.key] ?? \"\", column.x, y, {\n\t\t\t\twidth: Math.max(1, column.width - COLUMN_PADDING),\n\t\t\t\talign: column.align,\n\t\t\t\tlineBreak: false,\n\t\t\t});\n\t\t}\n\t\tdoc.y = y + LAYOUT.lineHeight;\n\t};\n\n\t/** Draws the column headings and the line below them. */\n\tconst drawHeaderRow = (): void => {\n\t\tconst headings: Partial<Record<keyof ReportLabels, string>> = {};\n\t\tfor (const column of columns) {\n\t\t\theadings[column.key] = labels[column.key];\n\t\t}\n\t\tdrawRow(headings, { bold: true, fontSize: headerSize });\n\t\tdoc.moveTo(LAYOUT.margin, doc.y - 3)\n\t\t\t.lineTo(doc.page.width - LAYOUT.margin, doc.y - 3)\n\t\t\t.lineWidth(0.5)\n\t\t\t.strokeColor(\"#9aa5b1\")\n\t\t\t.stroke();\n\t};\n\n\t// --- header block ---------------------------------------------------------\n\tdoc.font(fonts.bold).fontSize(16).fillColor(\"#000000\").text(labels.title);\n\tdoc.moveDown(0.3);\n\tdoc.font(fonts.regular).fontSize(10);\n\tdoc.text(`${labels.employee}: ${user.displayName} (${user.login})`);\n\tdoc.text(`${labels.period}: ${formatDate(from, locale)} - ${formatDate(to, locale)}`);\n\tdoc.text(`${labels.created}: ${generated}`);\n\tdoc.moveDown(0.8);\n\n\t// --- days -----------------------------------------------------------------\n\tdrawHeaderRow();\n\n\tfor (const day of input.days) {\n\t\t// one more page when the month does not fit (defensive: 31 rows always fit on one A4 page)\n\t\tif (doc.y + LAYOUT.lineHeight > pageBottom) {\n\t\t\tdoc.addPage();\n\t\t\tdoc.font(fonts.regular).fontSize(8).fillColor(\"#757575\").text(labels.title, LAYOUT.margin, LAYOUT.margin);\n\t\t\tdoc.moveDown(0.5);\n\t\t\tdrawHeaderRow();\n\t\t}\n\n\t\tconst notes: string[] = [];\n\t\tif (day.isHoliday) {\n\t\t\tnotes.push(labels.holiday);\n\t\t}\n\t\tif (day.hasOpenEntry) {\n\t\t\tnotes.push(labels.openEntry);\n\t\t}\n\n\t\tdrawRow(\n\t\t\t{\n\t\t\t\tdate: formatDate(day.localDate, locale),\n\t\t\t\ttimeIn: formatTime(day.firstInUtc, user.timezone, locale),\n\t\t\t\ttimeOut: formatTime(day.lastOutUtc, user.timezone, locale),\n\t\t\t\tworked: formatMinutes(day.workedMin),\n\t\t\t\tbreaks: formatMinutes(day.breakMin),\n\t\t\t\tpaidBreaks: formatMinutes(day.paidBreakMin),\n\t\t\t\ttarget: formatMinutes(day.targetMin),\n\t\t\t\tbalance: formatMinutes(day.balanceMin),\n\t\t\t\tabsence: day.absenceCode ?? \"\",\n\t\t\t\tnote: notes.join(\", \"),\n\t\t\t},\n\t\t\t{\n\t\t\t\tcolor: day.balanceMin < 0 ? \"#c62828\" : day.isHoliday || day.workedMin === 0 ? \"#757575\" : \"#000000\",\n\t\t\t},\n\t\t);\n\t}\n\n\tdrawRow(\n\t\t{\n\t\t\tdate: labels.total,\n\t\t\tworked: formatMinutes(totals.worked),\n\t\t\tbreaks: formatMinutes(totals.breaks),\n\t\t\tpaidBreaks: formatMinutes(totals.paidBreaks),\n\t\t\ttarget: formatMinutes(totals.target),\n\t\t\tbalance: formatMinutes(totals.balance),\n\t\t\tabsence: `${labels.days}: ${input.days.length}`,\n\t\t},\n\t\t{ bold: true, color: totals.balance < 0 ? \"#c62828\" : \"#000000\", topBorder: true },\n\t);\n\n\t// --- absences -------------------------------------------------------------\n\tif (input.absences.length > 0) {\n\t\t// The block starts at the left margin over the whole width: after the table the drawing position sits inside\n\t\t// the page (the last cell was written there), and every line would be squeezed into the rest of that row.\n\t\tconst contentWidth = doc.page.width - 2 * LAYOUT.margin;\n\t\tdoc.moveDown(1);\n\t\tdoc.font(fonts.bold)\n\t\t\t.fontSize(10)\n\t\t\t.fillColor(\"#000000\")\n\t\t\t.text(labels.absences, LAYOUT.margin, doc.y, { width: contentWidth });\n\t\tdoc.moveDown(0.3);\n\t\tfor (const absence of input.absences) {\n\t\t\tconst portion = absence.dayPortion === 0.5 ? labels.halfDay : labels.fullDay;\n\t\t\tdoc.font(fonts.regular)\n\t\t\t\t.fontSize(LAYOUT.fontSize)\n\t\t\t\t.fillColor(\"#000000\")\n\t\t\t\t.text(\n\t\t\t\t\t`${absence.typeCode} - ${absence.typeName}: ${formatDate(absence.dateFrom, locale)} - ${formatDate(\n\t\t\t\t\t\tabsence.dateTo,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t)}, ${portion}${absence.hours === null ? \"\" : `, ${absence.hours} h`}`,\n\t\t\t\t\tLAYOUT.margin,\n\t\t\t\t\tdoc.y,\n\t\t\t\t\t{ width: contentWidth },\n\t\t\t\t);\n\t\t}\n\t}\n\n\t// --- signatures -----------------------------------------------------------\n\t// the label is drawn and the line is ruled: a long label plus a string of underscores is wrapped by the\n\t// renderer (the Russian and Ukrainian labels are long), and then the two signature lines are no longer level\n\t//\n\t// The block sits near the bottom of the page: a statement ends with two signature lines, so an empty area\n\t// above them looks like the document was cut off. It stays below the content when that reaches far down.\n\tconst signatureGap = 16;\n\tconst signatureWidth = (doc.page.width - 2 * LAYOUT.margin - signatureGap) / 2;\n\tconst signaturePreferred = doc.page.height - 30 - LAYOUT.lineHeight - 24;\n\tconst signatureY = Math.max(doc.y + 24, Math.min(signaturePreferred, pageBottom - LAYOUT.lineHeight));\n\n\t/**\n\t * Draws one signature line: the label followed by the rule to sign on.\n\t *\n\t * @param label - text before the line\n\t * @param x - left edge of the block\n\t */\n\tconst drawSignature = (label: string, x: number): void => {\n\t\tdoc.font(fonts.regular).fontSize(LAYOUT.fontSize).fillColor(\"#000000\");\n\t\tconst written = `${label}:`;\n\t\t// without a width limit the label is never broken \u2014 the block is wide enough for all eleven languages\n\t\tdoc.text(written, x, signatureY, { lineBreak: false });\n\t\tconst start = Math.min(x + doc.widthOfString(written) + 6, x + signatureWidth - 24);\n\t\tdoc.moveTo(start, signatureY + LAYOUT.fontSize + 2)\n\t\t\t.lineTo(x + signatureWidth, signatureY + LAYOUT.fontSize + 2)\n\t\t\t.lineWidth(0.5)\n\t\t\t.strokeColor(\"#000000\")\n\t\t\t.stroke();\n\t\tdoc.y = signatureY + LAYOUT.lineHeight;\n\t};\n\n\tdrawSignature(labels.signatureEmployee, LAYOUT.margin);\n\tdrawSignature(labels.signatureManager, LAYOUT.margin + signatureWidth + signatureGap);\n\n\t// --- footer on every page -------------------------------------------------\n\t// the footer sits below the writing area on purpose, so the bottom margin is lifted while it is drawn: PDFKit\n\t// starts a new page when a text crosses the margin, and that page would carry nothing but the footer\n\tconst range = doc.bufferedPageRange();\n\tfor (let index = 0; index < range.count; index++) {\n\t\tdoc.switchToPage(range.start + index);\n\t\tconst bottom = doc.page.margins.bottom;\n\t\tdoc.page.margins.bottom = 0;\n\t\tdoc.font(fonts.regular)\n\t\t\t.fontSize(8)\n\t\t\t.fillColor(\"#757575\")\n\t\t\t.text(\n\t\t\t\t`${input.generator} - ${labels.title} ${input.year}-${String(input.month).padStart(2, \"0\")} - ${\n\t\t\t\t\tindex + 1\n\t\t\t\t}/${range.count}`,\n\t\t\t\tLAYOUT.margin,\n\t\t\t\tdoc.page.height - 30,\n\t\t\t\t{ width: doc.page.width - 2 * LAYOUT.margin, align: \"center\", lineBreak: false },\n\t\t\t);\n\t\tdoc.page.margins.bottom = bottom;\n\t}\n\n\tdoc.end();\n\tawait finished;\n\treturn Buffer.concat(chunks);\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBA,SAAoB;AACpB,oBAAwB;AACxB,oBAAgC;AAahC,MAAM,sBAAsB,oBAAI,IAAI,CAAC,MAAM,MAAM,OAAO,CAAC;AAGzD,MAAM,kBAAkB;AAYxB,SAAS,gBAAgB,MAAuB;AAC/C,SAAO,CAAC,GAAG,IAAI,EAAE,MAAM,eAAa,UAAU,WAAW,CAAC,KAAK,OAAQ,gBAAgB,SAAS,SAAS,CAAC;AAC3G;AAaO,MAAM,SAAS;AAAA,EACrB,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,YAAY;AACb;AAGA,MAAM,iBAAiB;AAUvB,MAAM,eAAe,CAAC,GAAG,GAAG,GAAG,GAAG;AAGlC,MAAM,UASA;AAAA,EACL,EAAE,KAAK,QAAQ,OAAO,QAAQ,QAAQ,mBAAmB,MAAM,KAAK;AAAA,EACpE,EAAE,KAAK,UAAU,OAAO,SAAS,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC1D,EAAE,KAAK,WAAW,OAAO,SAAS,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC3D,EAAE,KAAK,UAAU,OAAO,SAAS,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC1D,EAAE,KAAK,UAAU,OAAO,SAAS,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC1D,EAAE,KAAK,cAAc,OAAO,SAAS,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC9D,EAAE,KAAK,UAAU,OAAO,SAAS,QAAQ,SAAS,MAAM,EAAE;AAAA,EAC1D,EAAE,KAAK,WAAW,OAAO,SAAS,QAAQ,UAAU,MAAM,EAAE;AAAA,EAC5D,EAAE,KAAK,WAAW,OAAO,QAAQ,QAAQ,UAAU,MAAM,EAAE;AAAA,EAC3D,EAAE,KAAK,QAAQ,OAAO,QAAQ,QAAQ,mBAAmB,MAAM,KAAK;AACrE;AAsBA,SAAS,YAAY,UAAkB,UAAqC,QAAkC;AAC7G,QAAM,cAAc,8BAAY,IAAI,KAAK;AACzC,MAAI,eAAe,MAAM,GAAG,WAAW,UAAU,GAAG;AAEnD,WAAO,EAAE,SAAS,YAAY,MAAM,YAAY,UAAU,KAAK;AAAA,EAChE;AAGA,QAAM,aAAa,OAAO,OAAO,MAAM,EAAE,OAAO,UAAQ,CAAC,gBAAgB,IAAI,CAAC;AAC9E,MAAI,oBAAoB,IAAI,QAAQ,KAAK,WAAW,SAAS,GAAG;AAC/D,UAAM,IAAI;AAAA,MACT,aAAa,QAAQ;AAAA,IACtB;AAAA,EACD;AACA,SAAO,EAAE,SAAS,aAAa,MAAM,kBAAkB,UAAU,MAAM;AACxE;AAeA,SAAS,eACR,KACA,OACA,QACA,SACoD;AACpD,QAAM,YAAY,IAAI,KAAK,QAAQ,IAAI,OAAO;AAC9C,QAAM,WAAW,aAAa,SAAS;AAEvC,aAAW,CAAC,OAAO,IAAI,KAAK,aAAa,QAAQ,GAAG;AACnD,UAAM,SAAS,QAAQ,IAAI,YAAU;AAnKvC;AAoKG,UAAI,KAAK,MAAM,IAAI,EAAE,SAAS,IAAI;AAClC,YAAM,UAAU,IAAI,cAAc,OAAO,OAAO,GAAG,CAAC;AACpD,UAAI,KAAK,MAAM,OAAO,EAAE,SAAS,OAAO,QAAQ;AAGhD,YAAM,SAAQ,aAAQ,OAAO,GAAG,MAAlB,YAAuB,CAAC,OAAO,MAAM;AACnD,YAAM,QAAQ,KAAK;AAAA,QAClB,IAAI,MAAM,SAAS,IAAI,QAAQ,CAAC,OAAO,MAAM,GAAG,IAAI,UAAQ,IAAI,cAAc,IAAI,CAAC;AAAA,MACpF;AAGA,aAAO,KAAK,IAAI,SAAS,KAAK,IAAI,IAAI;AAAA,IACvC,CAAC;AACD,UAAM,QAAQ,OAAO,OAAO,CAAC,KAAK,UAAU,MAAM,OAAO,CAAC;AAC1D,QAAI,QAAQ,aAAa,QAAQ,UAAU;AAC1C;AAAA,IACD;AAEA,UAAM,WAAW,KAAK,IAAI,GAAG,YAAY,KAAK;AAC9C,QAAI,IAAI,OAAO;AACf,UAAM,UAAU,QAAQ,IAAI,CAAC,QAAQ,aAAa;AACjD,YAAM,QAAQ,OAAO,QAAQ,IAAI,WAAW,OAAO;AACnD,YAAM,WAA2B,EAAE,KAAK,OAAO,KAAK,GAAG,OAAO,OAAO,OAAO,MAAM;AAClF,WAAK;AACL,aAAO;AAAA,IACR,CAAC;AACD,WAAO,EAAE,SAAS,YAAY,KAAK;AAAA,EACpC;AAGA,SAAO,EAAE,SAAS,CAAC,GAAG,YAAY,aAAa,QAAQ,EAAE;AAC1D;AASA,SAAS,WAAW,MAAc,QAAwB;AACzD,SAAO,IAAI,KAAK,eAAe,QAAQ;AAAA,IACtC,SAAS;AAAA,IACT,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,UAAU;AAAA,EACX,CAAC,EAAE,OAAO,oBAAI,KAAK,GAAG,IAAI,YAAY,CAAC;AACxC;AAUA,SAAS,WAAW,OAAsB,UAAkB,QAAwB;AACnF,MAAI,UAAU,MAAM;AACnB,WAAO;AAAA,EACR;AACA,SAAO,IAAI,KAAK,eAAe,QAAQ,EAAE,UAAU,MAAM,WAAW,QAAQ,UAAU,CAAC,EAAE;AAAA,IACxF,IAAI,KAAK,QAAQ,GAAI;AAAA,EACtB;AACD;AAQA,SAAS,cAAc,SAAyB;AAC/C,QAAM,OAAO,UAAU,IAAI,MAAM;AACjC,QAAM,WAAW,KAAK,IAAI,OAAO;AACjC,SAAO,GAAG,IAAI,GAAG,KAAK,MAAM,WAAW,EAAE,CAAC,IAAI,OAAO,WAAW,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC;AACrF;AAUA,SAAS,YAAY,OAAe,QAAgB,UAA0B;AAC7E,SAAO,IAAI,KAAK,eAAe,QAAQ,EAAE,UAAU,WAAW,SAAS,WAAW,QAAQ,CAAC,EAAE;AAAA,IAC5F,IAAI,KAAK,QAAQ,GAAI;AAAA,EACtB;AACD;AAQA,eAAsB,oBAAoB,OAA2C;AAvQrF;AAwQC,QAAM,EAAE,QAAQ,QAAQ,KAAK,IAAI;AACjC,QAAM,QAAQ,YAAY,MAAM,UAAU,MAAM,UAAU,MAAM;AAChE,QAAM,OAAO,GAAG,MAAM,IAAI,IAAI,OAAO,MAAM,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC;AAClE,QAAM,UAAU,IAAI,KAAK,KAAK,IAAI,MAAM,MAAM,MAAM,OAAO,CAAC,CAAC,EAAE,WAAW;AAC1E,QAAM,KAAK,GAAG,MAAM,IAAI,IAAI,OAAO,MAAM,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,OAAO,EAAE,SAAS,GAAG,GAAG,CAAC;AACpG,QAAM,YAAY,YAAY,MAAM,aAAa,QAAQ,KAAK,QAAQ;AAEtE,QAAM,MAAM,IAAI,cAAAA,QAAY;AAAA,IAC3B,MAAM;AAAA,IACN,QAAQ,OAAO;AAAA;AAAA,IAEf,UAAU;AAAA,IACV,MAAM;AAAA,MACL,OAAO,GAAG,OAAO,KAAK,IAAI,MAAM,IAAI,IAAI,OAAO,MAAM,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,MAC5E,QAAQ,MAAM;AAAA,MACd,SAAS,MAAM;AAAA,MACf,UAAU,MAAM;AAAA,MAChB,SAAS,GAAG,KAAK,WAAW,KAAK,KAAK,KAAK;AAAA,IAC5C;AAAA,EACD,CAAC;AAED,QAAM,SAAmB,CAAC;AAC1B,MAAI,GAAG,QAAQ,CAAC,UAAkB,OAAO,KAAK,KAAK,CAAC;AACpD,QAAM,WAAW,IAAI,QAAc,aAAW,IAAI,GAAG,OAAO,OAAO,CAAC;AAEpE,QAAM,aAAa,IAAI,KAAK,SAAS,OAAO,SAAS;AAUrD,QAAM,SAAS,MAAM,KAAK;AAAA,IACzB,CAAC,KAAK,SAAS;AAAA,MACd,QAAQ,IAAI,SAAS,IAAI;AAAA,MACzB,QAAQ,IAAI,SAAS,IAAI;AAAA,MACzB,YAAY,IAAI,aAAa,IAAI;AAAA,MACjC,QAAQ,IAAI,SAAS,IAAI;AAAA,MACzB,SAAS,IAAI,UAAU,IAAI;AAAA,IAC5B;AAAA,IACA,EAAE,QAAQ,GAAG,QAAQ,GAAG,YAAY,GAAG,QAAQ,GAAG,SAAS,EAAE;AAAA,EAC9D;AAEA,QAAM,aAAa,WAAW,KAAK,IAAI,MAAM,GAAG,GAAG,IAAI,EAAE,IAAI,KAAM,OAAO,MAAM;AAEhF,QAAM,EAAE,SAAS,WAAW,IAAI,eAAe,KAAK,OAAO,QAAQ;AAAA;AAAA,IAElE,MAAM,MAAM,KAAK,IAAI,SAAO,WAAW,IAAI,WAAW,MAAM,CAAC;AAAA,IAC7D,QAAQ,CAAC,GAAG,MAAM,KAAK,IAAI,SAAO,WAAW,IAAI,YAAY,KAAK,UAAU,MAAM,CAAC,GAAG,UAAU;AAAA,IAChG,SAAS,CAAC,GAAG,MAAM,KAAK,IAAI,SAAO,WAAW,IAAI,YAAY,KAAK,UAAU,MAAM,CAAC,GAAG,UAAU;AAAA,IACjG,QAAQ,CAAC,GAAG,MAAM,KAAK,IAAI,SAAO,cAAc,IAAI,SAAS,CAAC,GAAG,cAAc,OAAO,MAAM,CAAC;AAAA,IAC7F,QAAQ,CAAC,GAAG,MAAM,KAAK,IAAI,SAAO,cAAc,IAAI,QAAQ,CAAC,GAAG,cAAc,OAAO,MAAM,CAAC;AAAA,IAC5F,YAAY,CAAC,GAAG,MAAM,KAAK,IAAI,SAAO,cAAc,IAAI,YAAY,CAAC,GAAG,cAAc,OAAO,UAAU,CAAC;AAAA,IACxG,QAAQ,CAAC,GAAG,MAAM,KAAK,IAAI,SAAO,cAAc,IAAI,SAAS,CAAC,GAAG,cAAc,OAAO,MAAM,CAAC;AAAA,IAC7F,SAAS,CAAC,GAAG,MAAM,KAAK,IAAI,SAAO,cAAc,IAAI,UAAU,CAAC,GAAG,cAAc,OAAO,OAAO,CAAC;AAAA;AAAA,IAEhG,MAAM,CAAC,OAAO,SAAS,OAAO,WAAW,GAAG,OAAO,OAAO,KAAK,OAAO,SAAS,EAAE;AAAA,EAClF,CAAC;AAYD,QAAM,UAAU,CACf,QACA,UAAsF,CAAC,MAC7E;AAnVZ,QAAAC,KAAA;AAoVE,UAAM,IAAI,IAAI;AACd,QAAI,QAAQ,WAAW;AACtB,UAAI,OAAO,OAAO,QAAQ,IAAI,CAAC,EAC7B,OAAO,IAAI,KAAK,QAAQ,OAAO,QAAQ,IAAI,CAAC,EAC5C,UAAU,GAAG,EACb,YAAY,SAAS,EACrB,OAAO;AACT,UAAI,IAAI;AAAA,IACT;AACA,QAAI,KAAK,QAAQ,OAAO,MAAM,OAAO,MAAM,OAAO,EAAE,UAASA,MAAA,QAAQ,aAAR,OAAAA,MAAoB,OAAO,QAAQ;AAChG,QAAI,WAAU,aAAQ,UAAR,YAAiB,SAAS;AACxC,eAAW,UAAU,SAAS;AAG7B,UAAI,MAAK,YAAO,OAAO,GAAG,MAAjB,YAAsB,IAAI,OAAO,GAAG,GAAG;AAAA,QAC/C,OAAO,KAAK,IAAI,GAAG,OAAO,QAAQ,cAAc;AAAA,QAChD,OAAO,OAAO;AAAA,QACd,WAAW;AAAA,MACZ,CAAC;AAAA,IACF;AACA,QAAI,IAAI,IAAI,OAAO;AAAA,EACpB;AAGA,QAAM,gBAAgB,MAAY;AACjC,UAAM,WAAwD,CAAC;AAC/D,eAAW,UAAU,SAAS;AAC7B,eAAS,OAAO,GAAG,IAAI,OAAO,OAAO,GAAG;AAAA,IACzC;AACA,YAAQ,UAAU,EAAE,MAAM,MAAM,UAAU,WAAW,CAAC;AACtD,QAAI,OAAO,OAAO,QAAQ,IAAI,IAAI,CAAC,EACjC,OAAO,IAAI,KAAK,QAAQ,OAAO,QAAQ,IAAI,IAAI,CAAC,EAChD,UAAU,GAAG,EACb,YAAY,SAAS,EACrB,OAAO;AAAA,EACV;AAGA,MAAI,KAAK,MAAM,IAAI,EAAE,SAAS,EAAE,EAAE,UAAU,SAAS,EAAE,KAAK,OAAO,KAAK;AACxE,MAAI,SAAS,GAAG;AAChB,MAAI,KAAK,MAAM,OAAO,EAAE,SAAS,EAAE;AACnC,MAAI,KAAK,GAAG,OAAO,QAAQ,KAAK,KAAK,WAAW,KAAK,KAAK,KAAK,GAAG;AAClE,MAAI,KAAK,GAAG,OAAO,MAAM,KAAK,WAAW,MAAM,MAAM,CAAC,MAAM,WAAW,IAAI,MAAM,CAAC,EAAE;AACpF,MAAI,KAAK,GAAG,OAAO,OAAO,KAAK,SAAS,EAAE;AAC1C,MAAI,SAAS,GAAG;AAGhB,gBAAc;AAEd,aAAW,OAAO,MAAM,MAAM;AAE7B,QAAI,IAAI,IAAI,OAAO,aAAa,YAAY;AAC3C,UAAI,QAAQ;AACZ,UAAI,KAAK,MAAM,OAAO,EAAE,SAAS,CAAC,EAAE,UAAU,SAAS,EAAE,KAAK,OAAO,OAAO,OAAO,QAAQ,OAAO,MAAM;AACxG,UAAI,SAAS,GAAG;AAChB,oBAAc;AAAA,IACf;AAEA,UAAM,QAAkB,CAAC;AACzB,QAAI,IAAI,WAAW;AAClB,YAAM,KAAK,OAAO,OAAO;AAAA,IAC1B;AACA,QAAI,IAAI,cAAc;AACrB,YAAM,KAAK,OAAO,SAAS;AAAA,IAC5B;AAEA;AAAA,MACC;AAAA,QACC,MAAM,WAAW,IAAI,WAAW,MAAM;AAAA,QACtC,QAAQ,WAAW,IAAI,YAAY,KAAK,UAAU,MAAM;AAAA,QACxD,SAAS,WAAW,IAAI,YAAY,KAAK,UAAU,MAAM;AAAA,QACzD,QAAQ,cAAc,IAAI,SAAS;AAAA,QACnC,QAAQ,cAAc,IAAI,QAAQ;AAAA,QAClC,YAAY,cAAc,IAAI,YAAY;AAAA,QAC1C,QAAQ,cAAc,IAAI,SAAS;AAAA,QACnC,SAAS,cAAc,IAAI,UAAU;AAAA,QACrC,UAAS,SAAI,gBAAJ,YAAmB;AAAA,QAC5B,MAAM,MAAM,KAAK,IAAI;AAAA,MACtB;AAAA,MACA;AAAA,QACC,OAAO,IAAI,aAAa,IAAI,YAAY,IAAI,aAAa,IAAI,cAAc,IAAI,YAAY;AAAA,MAC5F;AAAA,IACD;AAAA,EACD;AAEA;AAAA,IACC;AAAA,MACC,MAAM,OAAO;AAAA,MACb,QAAQ,cAAc,OAAO,MAAM;AAAA,MACnC,QAAQ,cAAc,OAAO,MAAM;AAAA,MACnC,YAAY,cAAc,OAAO,UAAU;AAAA,MAC3C,QAAQ,cAAc,OAAO,MAAM;AAAA,MACnC,SAAS,cAAc,OAAO,OAAO;AAAA,MACrC,SAAS,GAAG,OAAO,IAAI,KAAK,MAAM,KAAK,MAAM;AAAA,IAC9C;AAAA,IACA,EAAE,MAAM,MAAM,OAAO,OAAO,UAAU,IAAI,YAAY,WAAW,WAAW,KAAK;AAAA,EAClF;AAGA,MAAI,MAAM,SAAS,SAAS,GAAG;AAG9B,UAAM,eAAe,IAAI,KAAK,QAAQ,IAAI,OAAO;AACjD,QAAI,SAAS,CAAC;AACd,QAAI,KAAK,MAAM,IAAI,EACjB,SAAS,EAAE,EACX,UAAU,SAAS,EACnB,KAAK,OAAO,UAAU,OAAO,QAAQ,IAAI,GAAG,EAAE,OAAO,aAAa,CAAC;AACrE,QAAI,SAAS,GAAG;AAChB,eAAW,WAAW,MAAM,UAAU;AACrC,YAAM,UAAU,QAAQ,eAAe,MAAM,OAAO,UAAU,OAAO;AACrE,UAAI,KAAK,MAAM,OAAO,EACpB,SAAS,OAAO,QAAQ,EACxB,UAAU,SAAS,EACnB;AAAA,QACA,GAAG,QAAQ,QAAQ,MAAM,QAAQ,QAAQ,KAAK,WAAW,QAAQ,UAAU,MAAM,CAAC,MAAM;AAAA,UACvF,QAAQ;AAAA,UACR;AAAA,QACD,CAAC,KAAK,OAAO,GAAG,QAAQ,UAAU,OAAO,KAAK,KAAK,QAAQ,KAAK,IAAI;AAAA,QACpE,OAAO;AAAA,QACP,IAAI;AAAA,QACJ,EAAE,OAAO,aAAa;AAAA,MACvB;AAAA,IACF;AAAA,EACD;AAQA,QAAM,eAAe;AACrB,QAAM,kBAAkB,IAAI,KAAK,QAAQ,IAAI,OAAO,SAAS,gBAAgB;AAC7E,QAAM,qBAAqB,IAAI,KAAK,SAAS,KAAK,OAAO,aAAa;AACtE,QAAM,aAAa,KAAK,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI,oBAAoB,aAAa,OAAO,UAAU,CAAC;AAQpG,QAAM,gBAAgB,CAAC,OAAe,MAAoB;AACzD,QAAI,KAAK,MAAM,OAAO,EAAE,SAAS,OAAO,QAAQ,EAAE,UAAU,SAAS;AACrE,UAAM,UAAU,GAAG,KAAK;AAExB,QAAI,KAAK,SAAS,GAAG,YAAY,EAAE,WAAW,MAAM,CAAC;AACrD,UAAM,QAAQ,KAAK,IAAI,IAAI,IAAI,cAAc,OAAO,IAAI,GAAG,IAAI,iBAAiB,EAAE;AAClF,QAAI,OAAO,OAAO,aAAa,OAAO,WAAW,CAAC,EAChD,OAAO,IAAI,gBAAgB,aAAa,OAAO,WAAW,CAAC,EAC3D,UAAU,GAAG,EACb,YAAY,SAAS,EACrB,OAAO;AACT,QAAI,IAAI,aAAa,OAAO;AAAA,EAC7B;AAEA,gBAAc,OAAO,mBAAmB,OAAO,MAAM;AACrD,gBAAc,OAAO,kBAAkB,OAAO,SAAS,iBAAiB,YAAY;AAKpF,QAAM,QAAQ,IAAI,kBAAkB;AACpC,WAAS,QAAQ,GAAG,QAAQ,MAAM,OAAO,SAAS;AACjD,QAAI,aAAa,MAAM,QAAQ,KAAK;AACpC,UAAM,SAAS,IAAI,KAAK,QAAQ;AAChC,QAAI,KAAK,QAAQ,SAAS;AAC1B,QAAI,KAAK,MAAM,OAAO,EACpB,SAAS,CAAC,EACV,UAAU,SAAS,EACnB;AAAA,MACA,GAAG,MAAM,SAAS,MAAM,OAAO,KAAK,IAAI,MAAM,IAAI,IAAI,OAAO,MAAM,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,MACzF,QAAQ,CACT,IAAI,MAAM,KAAK;AAAA,MACf,OAAO;AAAA,MACP,IAAI,KAAK,SAAS;AAAA,MAClB,EAAE,OAAO,IAAI,KAAK,QAAQ,IAAI,OAAO,QAAQ,OAAO,UAAU,WAAW,MAAM;AAAA,IAChF;AACD,QAAI,KAAK,QAAQ,SAAS;AAAA,EAC3B;AAEA,MAAI,IAAI;AACR,QAAM;AACN,SAAO,OAAO,OAAO,MAAM;AAC5B;",
6
6
  "names": ["PDFDocument", "_a"]
7
7
  }
@@ -50,20 +50,21 @@ function createSyncService(deps) {
50
50
  };
51
51
  return {
52
52
  sync(input) {
53
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
53
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
54
54
  const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
55
55
  const maxSkew = (_b = input.maxClockSkewSeconds) != null ? _b : 300;
56
56
  const minDistance = (_c = input.minDistanceSeconds) != null ? _c : 30;
57
57
  const maxFuture = (_d = input.maxFutureSeconds) != null ? _d : 12 * 3600;
58
+ const maxPast = (_e = input.maxPastSeconds) != null ? _e : 0;
58
59
  const profile = users.getWorkProfile(input.userId);
59
- const window = { startDate: (_e = profile == null ? void 0 : profile.startDate) != null ? _e : null, endDate: (_f = profile == null ? void 0 : profile.endDate) != null ? _f : null };
60
+ const window = { startDate: (_f = profile == null ? void 0 : profile.startDate) != null ? _f : null, endDate: (_g = profile == null ? void 0 : profile.endDate) != null ? _g : null };
60
61
  const accepted = [];
61
62
  const conflicts = [];
62
63
  const rejected = [];
63
64
  const touched = /* @__PURE__ */ new Set();
64
65
  const sorted = input.punches.slice().sort((a, b) => a.tsUtc - b.tsUtc || a.idempotencyKey.localeCompare(b.idempotencyKey));
65
66
  for (const punch of sorted) {
66
- const key = (_h = (_g = punch.idempotencyKey) == null ? void 0 : _g.trim()) != null ? _h : "";
67
+ const key = (_i = (_h = punch.idempotencyKey) == null ? void 0 : _h.trim()) != null ? _i : "";
67
68
  if (!key) {
68
69
  rejected.push({ idempotencyKey: "", reason: "missing_key", detail: "idempotencyKey is required" });
69
70
  continue;
@@ -102,26 +103,27 @@ function createSyncService(deps) {
102
103
  }
103
104
  const skew = punch.clientTsUtc == null ? 0 : Math.abs(punch.tsUtc - punch.clientTsUtc);
104
105
  const windowError = (0, import_punch.checkEmploymentWindow)(punch.tsUtc, window);
105
- const syncState = skew > maxSkew || windowError ? "conflict" : "synced";
106
+ const tooOld = maxPast > 0 && punch.tsUtc < now - maxPast;
107
+ const syncState = skew > maxSkew || windowError || tooOld ? "conflict" : "synced";
106
108
  const stored = entries.insert({
107
109
  userId: input.userId,
108
110
  tsUtc: punch.tsUtc,
109
- clientTsUtc: (_i = punch.clientTsUtc) != null ? _i : null,
111
+ clientTsUtc: (_j = punch.clientTsUtc) != null ? _j : null,
110
112
  timeZone: input.timeZone,
111
113
  source: "api",
112
- direction: (_j = punch.direction) != null ? _j : "auto",
114
+ direction: (_k = punch.direction) != null ? _k : "auto",
113
115
  idempotencyKey: key,
114
116
  syncState,
115
- note: (_k = punch.note) != null ? _k : null,
117
+ note: (_l = punch.note) != null ? _l : null,
116
118
  actorId: input.actorId,
117
- actorIp: (_l = input.actorIp) != null ? _l : null,
119
+ actorIp: (_m = input.actorIp) != null ? _m : null,
118
120
  now
119
121
  });
120
122
  touched.add(stored.entry.localDate);
121
123
  if (syncState === "conflict") {
122
124
  conflicts.push({
123
125
  idempotencyKey: key,
124
- reason: windowError ? "employment_window" : "clock_skew",
126
+ reason: tooOld ? "too_old" : windowError ? "employment_window" : "clock_skew",
125
127
  entryId: stored.entry.id,
126
128
  entry: stored.entry
127
129
  });