iobroker.time-tracker 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/admin/i18n/de.json +34 -0
- package/admin/i18n/en.json +1 -0
- package/admin/i18n/es.json +34 -0
- package/admin/i18n/fr.json +34 -0
- package/admin/i18n/it.json +34 -0
- package/admin/i18n/nl.json +34 -0
- package/admin/i18n/pl.json +34 -0
- package/admin/i18n/pt.json +34 -0
- package/admin/i18n/ru.json +34 -0
- package/admin/i18n/uk.json +34 -0
- package/admin/i18n/zh-cn.json +34 -0
- package/admin/jsonConfig.json +266 -0
- package/admin/time-tracker.png +0 -0
- package/build/lib/adapter/automation.js +100 -0
- package/build/lib/adapter/automation.js.map +7 -0
- package/build/lib/adapter/commands.js +160 -0
- package/build/lib/adapter/commands.js.map +7 -0
- package/build/lib/adapter/messages.js +207 -0
- package/build/lib/adapter/messages.js.map +7 -0
- package/build/lib/adapter/presence.js +129 -0
- package/build/lib/adapter/presence.js.map +7 -0
- package/build/lib/adapter/states.js +330 -0
- package/build/lib/adapter/states.js.map +7 -0
- package/build/lib/adapter/triggers.js +92 -0
- package/build/lib/adapter/triggers.js.map +7 -0
- package/build/lib/db/database.js +96 -0
- package/build/lib/db/database.js.map +7 -0
- package/build/lib/db/migrations.js +603 -0
- package/build/lib/db/migrations.js.map +7 -0
- package/build/lib/db/repositories/absences.js +432 -0
- package/build/lib/db/repositories/absences.js.map +7 -0
- package/build/lib/db/repositories/audit.js +90 -0
- package/build/lib/db/repositories/audit.js.map +7 -0
- package/build/lib/db/repositories/automations.js +326 -0
- package/build/lib/db/repositories/automations.js.map +7 -0
- package/build/lib/db/repositories/entries.js +335 -0
- package/build/lib/db/repositories/entries.js.map +7 -0
- package/build/lib/db/repositories/holidays.js +156 -0
- package/build/lib/db/repositories/holidays.js.map +7 -0
- package/build/lib/db/repositories/payouts.js +221 -0
- package/build/lib/db/repositories/payouts.js.map +7 -0
- package/build/lib/db/repositories/rfid.js +264 -0
- package/build/lib/db/repositories/rfid.js.map +7 -0
- package/build/lib/db/repositories/rules.js +356 -0
- package/build/lib/db/repositories/rules.js.map +7 -0
- package/build/lib/db/repositories/settings.js +91 -0
- package/build/lib/db/repositories/settings.js.map +7 -0
- package/build/lib/db/repositories/terminals.js +243 -0
- package/build/lib/db/repositories/terminals.js.map +7 -0
- package/build/lib/db/repositories/triggers.js +240 -0
- package/build/lib/db/repositories/triggers.js.map +7 -0
- package/build/lib/db/repositories/users.js +491 -0
- package/build/lib/db/repositories/users.js.map +7 -0
- package/build/lib/db/seed.js +180 -0
- package/build/lib/db/seed.js.map +7 -0
- package/build/lib/domain/avatar.js +63 -0
- package/build/lib/domain/avatar.js.map +7 -0
- package/build/lib/domain/breaks.js +70 -0
- package/build/lib/domain/breaks.js.map +7 -0
- package/build/lib/domain/calculation.js +150 -0
- package/build/lib/domain/calculation.js.map +7 -0
- package/build/lib/domain/holidays.js +113 -0
- package/build/lib/domain/holidays.js.map +7 -0
- package/build/lib/domain/punch.js +99 -0
- package/build/lib/domain/punch.js.map +7 -0
- package/build/lib/domain/target.js +89 -0
- package/build/lib/domain/target.js.map +7 -0
- package/build/lib/errors.js +56 -0
- package/build/lib/errors.js.map +7 -0
- package/build/lib/reports/labels.js +358 -0
- package/build/lib/reports/labels.js.map +7 -0
- package/build/lib/reports/pdf.js +308 -0
- package/build/lib/reports/pdf.js.map +7 -0
- package/build/lib/reports/types.js +32 -0
- package/build/lib/reports/types.js.map +7 -0
- package/build/lib/reports/xls.js +211 -0
- package/build/lib/reports/xls.js.map +7 -0
- package/build/lib/services/aggregation.js +495 -0
- package/build/lib/services/aggregation.js.map +7 -0
- package/build/lib/services/auth.js +381 -0
- package/build/lib/services/auth.js.map +7 -0
- package/build/lib/services/backup.js +372 -0
- package/build/lib/services/backup.js.map +7 -0
- package/build/lib/services/closing.js +106 -0
- package/build/lib/services/closing.js.map +7 -0
- package/build/lib/services/sessionSecret.js +100 -0
- package/build/lib/services/sessionSecret.js.map +7 -0
- package/build/lib/services/sync.js +278 -0
- package/build/lib/services/sync.js.map +7 -0
- package/build/lib/util/time.js +125 -0
- package/build/lib/util/time.js.map +7 -0
- package/build/lib/web/api.js +2589 -0
- package/build/lib/web/api.js.map +7 -0
- package/build/lib/web/cookies.js +80 -0
- package/build/lib/web/cookies.js.map +7 -0
- package/build/lib/web/events.js +50 -0
- package/build/lib/web/events.js.map +7 -0
- package/build/lib/web/pin-guard.js +86 -0
- package/build/lib/web/pin-guard.js.map +7 -0
- package/build/lib/web/problem.js +129 -0
- package/build/lib/web/problem.js.map +7 -0
- package/build/lib/web/rate-limit.js +65 -0
- package/build/lib/web/rate-limit.js.map +7 -0
- package/build/lib/web/router.js +274 -0
- package/build/lib/web/router.js.map +7 -0
- package/build/lib/web/server.js +189 -0
- package/build/lib/web/server.js.map +7 -0
- package/build/lib/web/static.js +151 -0
- package/build/lib/web/static.js.map +7 -0
- package/build/lib/web/stream.js +135 -0
- package/build/lib/web/stream.js.map +7 -0
- package/build/main.js +986 -0
- package/build/main.js.map +7 -0
- package/io-package.json +221 -0
- package/package.json +131 -0
- package/src-pwa/src/i18n/de.json +359 -0
- package/src-pwa/src/i18n/en.json +359 -0
- package/src-pwa/src/i18n/es.json +359 -0
- package/src-pwa/src/i18n/fr.json +359 -0
- package/src-pwa/src/i18n/index.ts +96 -0
- package/src-pwa/src/i18n/it.json +359 -0
- package/src-pwa/src/i18n/nl.json +359 -0
- package/src-pwa/src/i18n/pl.json +359 -0
- package/src-pwa/src/i18n/pt.json +359 -0
- package/src-pwa/src/i18n/ru.json +359 -0
- package/src-pwa/src/i18n/uk.json +359 -0
- package/src-pwa/src/i18n/zh-cn.json +359 -0
- package/www/assets/index-QI3wrEvW.js +119 -0
- package/www/assets/index-QI3wrEvW.js.map +1 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js +3 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js.map +1 -0
- package/www/favicon.svg +16 -0
- package/www/icon-192.png +0 -0
- package/www/icon-512.png +0 -0
- package/www/index.html +38 -0
- package/www/manifest.webmanifest +1 -0
- package/www/person.png +0 -0
- package/www/sw.js +3 -0
- package/www/sw.js.map +1 -0
- package/www/workbox-9c191d2f.js +3 -0
- package/www/workbox-9c191d2f.js.map +1 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var pdf_exports = {};
|
|
30
|
+
__export(pdf_exports, {
|
|
31
|
+
buildMonthStatement: () => buildMonthStatement
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(pdf_exports);
|
|
34
|
+
var fs = __toESM(require("node:fs"));
|
|
35
|
+
var import_pdfkit = __toESM(require("pdfkit"));
|
|
36
|
+
var import_errors = require("../errors");
|
|
37
|
+
const NON_LATIN_LANGUAGES = /* @__PURE__ */ new Set(["ru", "uk", "zh-cn"]);
|
|
38
|
+
const 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";
|
|
39
|
+
function fitsBuiltInFont(text) {
|
|
40
|
+
return [...text].every((character) => character.charCodeAt(0) <= 255 || WIN_ANSI_EXTRAS.includes(character));
|
|
41
|
+
}
|
|
42
|
+
const LAYOUT = {
|
|
43
|
+
margin: 40,
|
|
44
|
+
fontSize: 9,
|
|
45
|
+
lineHeight: 14
|
|
46
|
+
};
|
|
47
|
+
const COLUMN_PADDING = 6;
|
|
48
|
+
const HEADER_SIZES = [9, 8, 7, 6.5];
|
|
49
|
+
const COLUMNS = [
|
|
50
|
+
{ key: "date", align: "left", sample: "Mi., 30.09.2026", grow: 0.35 },
|
|
51
|
+
{ key: "timeIn", align: "right", sample: "08:00", grow: 0 },
|
|
52
|
+
{ key: "timeOut", align: "right", sample: "08:00", grow: 0 },
|
|
53
|
+
{ key: "worked", align: "right", sample: "00:00", grow: 0 },
|
|
54
|
+
{ key: "breaks", align: "right", sample: "00:00", grow: 0 },
|
|
55
|
+
{ key: "paidBreaks", align: "right", sample: "00:00", grow: 0 },
|
|
56
|
+
{ key: "target", align: "right", sample: "00:00", grow: 0 },
|
|
57
|
+
{ key: "balance", align: "right", sample: "-00:00", grow: 0 },
|
|
58
|
+
{ key: "absence", align: "left", sample: "F 50 %", grow: 0 },
|
|
59
|
+
{ key: "note", align: "left", sample: "Feiertag, offen", grow: 0.65 }
|
|
60
|
+
];
|
|
61
|
+
function chooseFonts(language, fontPath, labels) {
|
|
62
|
+
const configured = (fontPath != null ? fontPath : "").trim();
|
|
63
|
+
if (configured !== "" && fs.existsSync(configured)) {
|
|
64
|
+
return { regular: configured, bold: configured, embedded: true };
|
|
65
|
+
}
|
|
66
|
+
const undrawable = Object.values(labels).filter((text) => !fitsBuiltInFont(text));
|
|
67
|
+
if (NON_LATIN_LANGUAGES.has(language) || undrawable.length > 0) {
|
|
68
|
+
throw new import_errors.ValidationError(
|
|
69
|
+
`language "${language}" needs a Unicode font: set report_font_path to a .ttf/.otf file that covers the script`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return { regular: "Helvetica", bold: "Helvetica-Bold", embedded: false };
|
|
73
|
+
}
|
|
74
|
+
function measureColumns(doc, fonts, labels, samples) {
|
|
75
|
+
const available = doc.page.width - 2 * LAYOUT.margin;
|
|
76
|
+
const smallest = HEADER_SIZES.length - 1;
|
|
77
|
+
for (const [index, size] of HEADER_SIZES.entries()) {
|
|
78
|
+
const needed = COLUMNS.map((column) => {
|
|
79
|
+
var _a;
|
|
80
|
+
doc.font(fonts.bold).fontSize(size);
|
|
81
|
+
const heading = doc.widthOfString(labels[column.key]);
|
|
82
|
+
doc.font(fonts.regular).fontSize(LAYOUT.fontSize);
|
|
83
|
+
const texts = (_a = samples[column.key]) != null ? _a : [column.sample];
|
|
84
|
+
const value = Math.max(
|
|
85
|
+
...(texts.length > 0 ? texts : [column.sample]).map((text) => doc.widthOfString(text))
|
|
86
|
+
);
|
|
87
|
+
return Math.max(heading, value) + 2 * COLUMN_PADDING;
|
|
88
|
+
});
|
|
89
|
+
const total = needed.reduce((sum, width) => sum + width, 0);
|
|
90
|
+
if (total > available && index < smallest) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const leftover = Math.max(0, available - total);
|
|
94
|
+
let x = LAYOUT.margin;
|
|
95
|
+
const columns = COLUMNS.map((column, position) => {
|
|
96
|
+
const width = needed[position] + leftover * column.grow;
|
|
97
|
+
const measured = { key: column.key, x, width, align: column.align };
|
|
98
|
+
x += width;
|
|
99
|
+
return measured;
|
|
100
|
+
});
|
|
101
|
+
return { columns, headerSize: size };
|
|
102
|
+
}
|
|
103
|
+
return { columns: [], headerSize: HEADER_SIZES[smallest] };
|
|
104
|
+
}
|
|
105
|
+
function formatDate(date, locale) {
|
|
106
|
+
return new Intl.DateTimeFormat(locale, {
|
|
107
|
+
weekday: "short",
|
|
108
|
+
day: "2-digit",
|
|
109
|
+
month: "2-digit",
|
|
110
|
+
year: "numeric",
|
|
111
|
+
timeZone: "UTC"
|
|
112
|
+
}).format(/* @__PURE__ */ new Date(`${date}T12:00:00Z`));
|
|
113
|
+
}
|
|
114
|
+
function formatTime(tsUtc, timeZone, locale) {
|
|
115
|
+
if (tsUtc === null) {
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
return new Intl.DateTimeFormat(locale, { timeZone, hour: "2-digit", minute: "2-digit" }).format(
|
|
119
|
+
new Date(tsUtc * 1e3)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
function formatMinutes(minutes) {
|
|
123
|
+
const sign = minutes < 0 ? "-" : "";
|
|
124
|
+
const absolute = Math.abs(minutes);
|
|
125
|
+
return `${sign}${Math.floor(absolute / 60)}:${String(absolute % 60).padStart(2, "0")}`;
|
|
126
|
+
}
|
|
127
|
+
function formatStamp(tsUtc, locale, timeZone) {
|
|
128
|
+
return new Intl.DateTimeFormat(locale, { timeZone, dateStyle: "short", timeStyle: "short" }).format(
|
|
129
|
+
new Date(tsUtc * 1e3)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
async function buildMonthStatement(input) {
|
|
133
|
+
var _a;
|
|
134
|
+
const { labels, locale, user } = input;
|
|
135
|
+
const fonts = chooseFonts(input.language, input.fontPath, labels);
|
|
136
|
+
const from = `${input.year}-${String(input.month).padStart(2, "0")}-01`;
|
|
137
|
+
const lastDay = new Date(Date.UTC(input.year, input.month, 0)).getUTCDate();
|
|
138
|
+
const to = `${input.year}-${String(input.month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`;
|
|
139
|
+
const generated = formatStamp(input.generatedAt, locale, user.timezone);
|
|
140
|
+
const doc = new import_pdfkit.default({
|
|
141
|
+
size: "A4",
|
|
142
|
+
margin: LAYOUT.margin,
|
|
143
|
+
// an uncompressed stream keeps the statement checkable without a PDF reader
|
|
144
|
+
compress: false,
|
|
145
|
+
info: {
|
|
146
|
+
Title: `${labels.title} ${input.year}-${String(input.month).padStart(2, "0")}`,
|
|
147
|
+
Author: input.generator,
|
|
148
|
+
Creator: input.generator,
|
|
149
|
+
Producer: input.generator,
|
|
150
|
+
Subject: `${user.displayName} (${user.login})`
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
const chunks = [];
|
|
154
|
+
doc.on("data", (chunk) => chunks.push(chunk));
|
|
155
|
+
const finished = new Promise((resolve) => doc.on("end", resolve));
|
|
156
|
+
const pageBottom = doc.page.height - LAYOUT.margin - 24;
|
|
157
|
+
const totals = input.days.reduce(
|
|
158
|
+
(sum, day) => ({
|
|
159
|
+
worked: sum.worked + day.workedMin,
|
|
160
|
+
breaks: sum.breaks + day.breakMin,
|
|
161
|
+
paidBreaks: sum.paidBreaks + day.paidBreakMin,
|
|
162
|
+
target: sum.target + day.targetMin,
|
|
163
|
+
balance: sum.balance + day.balanceMin
|
|
164
|
+
}),
|
|
165
|
+
{ worked: 0, breaks: 0, paidBreaks: 0, target: 0, balance: 0 }
|
|
166
|
+
);
|
|
167
|
+
const widestTime = formatTime(Date.UTC(2026, 0, 1, 23, 59) / 1e3, "UTC", locale);
|
|
168
|
+
const { columns, headerSize } = measureColumns(doc, fonts, labels, {
|
|
169
|
+
// the date column holds the dates of the report: "czw., 30.09.2026" is wider than "Thu, 30.09.2026"
|
|
170
|
+
date: input.days.map((day) => formatDate(day.localDate, locale)),
|
|
171
|
+
timeIn: [...input.days.map((day) => formatTime(day.firstInUtc, user.timezone, locale)), widestTime],
|
|
172
|
+
timeOut: [...input.days.map((day) => formatTime(day.lastOutUtc, user.timezone, locale)), widestTime],
|
|
173
|
+
worked: [...input.days.map((day) => formatMinutes(day.workedMin)), formatMinutes(totals.worked)],
|
|
174
|
+
breaks: [...input.days.map((day) => formatMinutes(day.breakMin)), formatMinutes(totals.breaks)],
|
|
175
|
+
paidBreaks: [...input.days.map((day) => formatMinutes(day.paidBreakMin)), formatMinutes(totals.paidBreaks)],
|
|
176
|
+
target: [...input.days.map((day) => formatMinutes(day.targetMin)), formatMinutes(totals.target)],
|
|
177
|
+
balance: [...input.days.map((day) => formatMinutes(day.balanceMin)), formatMinutes(totals.balance)],
|
|
178
|
+
// the note column carries the marks of a day: a public holiday and a punch without a counterpart
|
|
179
|
+
note: [labels.holiday, labels.openEntry, `${labels.holiday}, ${labels.openEntry}`]
|
|
180
|
+
});
|
|
181
|
+
const drawRow = (values, options = {}) => {
|
|
182
|
+
var _a2, _b, _c;
|
|
183
|
+
const y = doc.y;
|
|
184
|
+
if (options.topBorder) {
|
|
185
|
+
doc.moveTo(LAYOUT.margin, y - 3).lineTo(doc.page.width - LAYOUT.margin, y - 3).lineWidth(0.5).strokeColor("#9aa5b1").stroke();
|
|
186
|
+
doc.y = y;
|
|
187
|
+
}
|
|
188
|
+
doc.font(options.bold ? fonts.bold : fonts.regular).fontSize((_a2 = options.fontSize) != null ? _a2 : LAYOUT.fontSize);
|
|
189
|
+
doc.fillColor((_b = options.color) != null ? _b : "#000000");
|
|
190
|
+
for (const column of columns) {
|
|
191
|
+
doc.text((_c = values[column.key]) != null ? _c : "", column.x, y, {
|
|
192
|
+
width: Math.max(1, column.width - COLUMN_PADDING),
|
|
193
|
+
align: column.align,
|
|
194
|
+
lineBreak: false
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
doc.y = y + LAYOUT.lineHeight;
|
|
198
|
+
};
|
|
199
|
+
const drawHeaderRow = () => {
|
|
200
|
+
const headings = {};
|
|
201
|
+
for (const column of columns) {
|
|
202
|
+
headings[column.key] = labels[column.key];
|
|
203
|
+
}
|
|
204
|
+
drawRow(headings, { bold: true, fontSize: headerSize });
|
|
205
|
+
doc.moveTo(LAYOUT.margin, doc.y - 3).lineTo(doc.page.width - LAYOUT.margin, doc.y - 3).lineWidth(0.5).strokeColor("#9aa5b1").stroke();
|
|
206
|
+
};
|
|
207
|
+
doc.font(fonts.bold).fontSize(16).fillColor("#000000").text(labels.title);
|
|
208
|
+
doc.moveDown(0.3);
|
|
209
|
+
doc.font(fonts.regular).fontSize(10);
|
|
210
|
+
doc.text(`${labels.employee}: ${user.displayName} (${user.login})`);
|
|
211
|
+
doc.text(`${labels.period}: ${formatDate(from, locale)} - ${formatDate(to, locale)}`);
|
|
212
|
+
doc.text(`${labels.created}: ${generated}`);
|
|
213
|
+
doc.moveDown(0.8);
|
|
214
|
+
drawHeaderRow();
|
|
215
|
+
for (const day of input.days) {
|
|
216
|
+
if (doc.y + LAYOUT.lineHeight > pageBottom) {
|
|
217
|
+
doc.addPage();
|
|
218
|
+
doc.font(fonts.regular).fontSize(8).fillColor("#757575").text(labels.title, LAYOUT.margin, LAYOUT.margin);
|
|
219
|
+
doc.moveDown(0.5);
|
|
220
|
+
drawHeaderRow();
|
|
221
|
+
}
|
|
222
|
+
const notes = [];
|
|
223
|
+
if (day.isHoliday) {
|
|
224
|
+
notes.push(labels.holiday);
|
|
225
|
+
}
|
|
226
|
+
if (day.hasOpenEntry) {
|
|
227
|
+
notes.push(labels.openEntry);
|
|
228
|
+
}
|
|
229
|
+
drawRow(
|
|
230
|
+
{
|
|
231
|
+
date: formatDate(day.localDate, locale),
|
|
232
|
+
timeIn: formatTime(day.firstInUtc, user.timezone, locale),
|
|
233
|
+
timeOut: formatTime(day.lastOutUtc, user.timezone, locale),
|
|
234
|
+
worked: formatMinutes(day.workedMin),
|
|
235
|
+
breaks: formatMinutes(day.breakMin),
|
|
236
|
+
paidBreaks: formatMinutes(day.paidBreakMin),
|
|
237
|
+
target: formatMinutes(day.targetMin),
|
|
238
|
+
balance: formatMinutes(day.balanceMin),
|
|
239
|
+
absence: (_a = day.absenceCode) != null ? _a : "",
|
|
240
|
+
note: notes.join(", ")
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
color: day.balanceMin < 0 ? "#c62828" : day.isHoliday || day.workedMin === 0 ? "#757575" : "#000000"
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
drawRow(
|
|
248
|
+
{
|
|
249
|
+
date: labels.total,
|
|
250
|
+
worked: formatMinutes(totals.worked),
|
|
251
|
+
breaks: formatMinutes(totals.breaks),
|
|
252
|
+
paidBreaks: formatMinutes(totals.paidBreaks),
|
|
253
|
+
target: formatMinutes(totals.target),
|
|
254
|
+
balance: formatMinutes(totals.balance),
|
|
255
|
+
absence: `${labels.days}: ${input.days.length}`
|
|
256
|
+
},
|
|
257
|
+
{ bold: true, color: totals.balance < 0 ? "#c62828" : "#000000", topBorder: true }
|
|
258
|
+
);
|
|
259
|
+
if (input.absences.length > 0) {
|
|
260
|
+
doc.moveDown(1);
|
|
261
|
+
doc.font(fonts.bold).fontSize(10).fillColor("#000000").text(labels.absences);
|
|
262
|
+
doc.moveDown(0.3);
|
|
263
|
+
for (const absence of input.absences) {
|
|
264
|
+
const portion = absence.dayPortion === 0.5 ? labels.halfDay : labels.fullDay;
|
|
265
|
+
doc.font(fonts.regular).fontSize(LAYOUT.fontSize).fillColor("#000000").text(
|
|
266
|
+
`${absence.typeCode} - ${absence.typeName}: ${formatDate(absence.dateFrom, locale)} - ${formatDate(
|
|
267
|
+
absence.dateTo,
|
|
268
|
+
locale
|
|
269
|
+
)}, ${portion}${absence.hours === null ? "" : `, ${absence.hours} h`}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const signatureGap = 16;
|
|
274
|
+
const signatureWidth = (doc.page.width - 2 * LAYOUT.margin - signatureGap) / 2;
|
|
275
|
+
const signaturePreferred = doc.page.height - 30 - LAYOUT.lineHeight - 24;
|
|
276
|
+
const signatureY = Math.max(doc.y + 24, Math.min(signaturePreferred, pageBottom - LAYOUT.lineHeight));
|
|
277
|
+
const drawSignature = (label, x) => {
|
|
278
|
+
doc.font(fonts.regular).fontSize(LAYOUT.fontSize).fillColor("#000000");
|
|
279
|
+
const written = `${label}:`;
|
|
280
|
+
doc.text(written, x, signatureY, { lineBreak: false });
|
|
281
|
+
const start = Math.min(x + doc.widthOfString(written) + 6, x + signatureWidth - 24);
|
|
282
|
+
doc.moveTo(start, signatureY + LAYOUT.fontSize + 2).lineTo(x + signatureWidth, signatureY + LAYOUT.fontSize + 2).lineWidth(0.5).strokeColor("#000000").stroke();
|
|
283
|
+
doc.y = signatureY + LAYOUT.lineHeight;
|
|
284
|
+
};
|
|
285
|
+
drawSignature(labels.signatureEmployee, LAYOUT.margin);
|
|
286
|
+
drawSignature(labels.signatureManager, LAYOUT.margin + signatureWidth + signatureGap);
|
|
287
|
+
const range = doc.bufferedPageRange();
|
|
288
|
+
for (let index = 0; index < range.count; index++) {
|
|
289
|
+
doc.switchToPage(range.start + index);
|
|
290
|
+
const bottom = doc.page.margins.bottom;
|
|
291
|
+
doc.page.margins.bottom = 0;
|
|
292
|
+
doc.font(fonts.regular).fontSize(8).fillColor("#757575").text(
|
|
293
|
+
`${input.generator} - ${labels.title} ${input.year}-${String(input.month).padStart(2, "0")} - ${index + 1}/${range.count}`,
|
|
294
|
+
LAYOUT.margin,
|
|
295
|
+
doc.page.height - 30,
|
|
296
|
+
{ width: doc.page.width - 2 * LAYOUT.margin, align: "center", lineBreak: false }
|
|
297
|
+
);
|
|
298
|
+
doc.page.margins.bottom = bottom;
|
|
299
|
+
}
|
|
300
|
+
doc.end();
|
|
301
|
+
await finished;
|
|
302
|
+
return Buffer.concat(chunks);
|
|
303
|
+
}
|
|
304
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
305
|
+
0 && (module.exports = {
|
|
306
|
+
buildMonthStatement
|
|
307
|
+
});
|
|
308
|
+
//# sourceMappingURL=pdf.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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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",
|
|
6
|
+
"names": ["PDFDocument", "_a"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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 types_exports = {};
|
|
20
|
+
__export(types_exports, {
|
|
21
|
+
reportFileName: () => reportFileName
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(types_exports);
|
|
24
|
+
function reportFileName(login, year, month, extension) {
|
|
25
|
+
const safe = (login || "user").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "user";
|
|
26
|
+
return `time-tracker-${safe}-${year}-${String(month).padStart(2, "0")}.${extension}`;
|
|
27
|
+
}
|
|
28
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
29
|
+
0 && (module.exports = {
|
|
30
|
+
reportFileName
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/reports/types.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Shared shapes of the downloadable reports.\n *\n * The Excel and the PDF statement show the same data with the same labels \u2014 only the renderer differs, so the\n * input is defined once here.\n */\n\nimport type { DayAggregateRecord } from \"../services/aggregation\";\nimport type { ReportLabels } from \"./labels\";\n\n/** One absence of the reported month. */\nexport interface ReportAbsence {\n\t/** Short type code, e.g. `U` */\n\ttypeCode: string;\n\t/** Name of the type in the language of the instance */\n\ttypeName: string;\n\t/** First day, local date */\n\tdateFrom: string;\n\t/** Last day, local date */\n\tdateTo: string;\n\t/** `1` for a whole day, `0.5` for a half day */\n\tdayPortion: number;\n\t/** Hours of a partial day absence, when known */\n\thours: number | null;\n}\n\n/** Everything a monthly statement needs. */\nexport interface ReportInput {\n\t/** Employee the statement belongs to */\n\tuser: { displayName: string; login: string; timezone: string };\n\t/** Labels in the language of the employee */\n\tlabels: ReportLabels;\n\t/** Language key the labels came from, e.g. `ru` */\n\tlanguage: string;\n\t/** Locale used for dates and times (`users.locale`) */\n\tlocale: string;\n\t/** Four digit year */\n\tyear: number;\n\t/** Month, 1 to 12 */\n\tmonth: number;\n\t/** Days of the month, from the aggregates */\n\tdays: DayAggregateRecord[];\n\t/** Absences overlapping the month */\n\tabsences: ReportAbsence[];\n\t/** Instant of the export */\n\tgeneratedAt: number;\n}\n\n/**\n * Builds the file name of a statement.\n *\n * @param login - login of the employee\n * @param year - four digit year\n * @param month - month, 1 to 12\n * @param extension - file extension without dot\n * @returns file name with extension\n */\nexport function reportFileName(login: string, year: number, month: number, extension: string): string {\n\t// a login may contain characters a file name cannot carry\n\tconst safe = (login || \"user\").replace(/[^A-Za-z0-9._-]+/g, \"-\").replace(/^-+|-+$/g, \"\") || \"user\";\n\treturn `time-tracker-${safe}-${year}-${String(month).padStart(2, \"0\")}.${extension}`;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAyDO,SAAS,eAAe,OAAe,MAAc,OAAe,WAA2B;AAErG,QAAM,QAAQ,SAAS,QAAQ,QAAQ,qBAAqB,GAAG,EAAE,QAAQ,YAAY,EAAE,KAAK;AAC5F,SAAO,gBAAgB,IAAI,IAAI,IAAI,IAAI,OAAO,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,SAAS;AACnF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var xls_exports = {};
|
|
30
|
+
__export(xls_exports, {
|
|
31
|
+
buildMonthReport: () => buildMonthReport
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(xls_exports);
|
|
34
|
+
var import_exceljs = __toESM(require("exceljs"));
|
|
35
|
+
const DURATION_FORMAT = "[h]:mm";
|
|
36
|
+
const HEADER_ROW = 6;
|
|
37
|
+
const FIRST_DAY_ROW = HEADER_ROW + 1;
|
|
38
|
+
function formatDate(date, locale) {
|
|
39
|
+
return new Intl.DateTimeFormat(locale, {
|
|
40
|
+
weekday: "short",
|
|
41
|
+
day: "2-digit",
|
|
42
|
+
month: "2-digit",
|
|
43
|
+
year: "numeric",
|
|
44
|
+
timeZone: "UTC"
|
|
45
|
+
}).format(/* @__PURE__ */ new Date(`${date}T12:00:00Z`));
|
|
46
|
+
}
|
|
47
|
+
function formatTime(tsUtc, timeZone, locale) {
|
|
48
|
+
if (tsUtc === null) {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
return new Intl.DateTimeFormat(locale, { timeZone, hour: "2-digit", minute: "2-digit" }).format(
|
|
52
|
+
new Date(tsUtc * 1e3)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
function duration(minutes) {
|
|
56
|
+
return minutes / 1440;
|
|
57
|
+
}
|
|
58
|
+
function formatStamp(tsUtc, locale, timeZone) {
|
|
59
|
+
return new Intl.DateTimeFormat(locale, {
|
|
60
|
+
timeZone,
|
|
61
|
+
dateStyle: "short",
|
|
62
|
+
timeStyle: "short"
|
|
63
|
+
}).format(new Date(tsUtc * 1e3));
|
|
64
|
+
}
|
|
65
|
+
async function buildMonthReport(input) {
|
|
66
|
+
var _a;
|
|
67
|
+
const { labels, locale, user } = input;
|
|
68
|
+
const workbook = new import_exceljs.default.Workbook();
|
|
69
|
+
workbook.creator = input.generator;
|
|
70
|
+
workbook.created = new Date(input.generatedAt * 1e3);
|
|
71
|
+
workbook.modified = workbook.created;
|
|
72
|
+
const sheet = workbook.addWorksheet(`${input.year}-${String(input.month).padStart(2, "0")}`, {
|
|
73
|
+
views: [{ state: "frozen", ySplit: HEADER_ROW }]
|
|
74
|
+
});
|
|
75
|
+
sheet.columns = [
|
|
76
|
+
{ key: "date", width: 24 },
|
|
77
|
+
{ key: "timeIn", width: 9 },
|
|
78
|
+
{ key: "timeOut", width: 9 },
|
|
79
|
+
{ key: "worked", width: 11 },
|
|
80
|
+
{ key: "breaks", width: 9 },
|
|
81
|
+
{ key: "paidBreaks", width: 12 },
|
|
82
|
+
{ key: "target", width: 10 },
|
|
83
|
+
{ key: "balance", width: 10 },
|
|
84
|
+
{ key: "absence", width: 12 },
|
|
85
|
+
{ key: "note", width: 18 }
|
|
86
|
+
];
|
|
87
|
+
const from = `${input.year}-${String(input.month).padStart(2, "0")}-01`;
|
|
88
|
+
const lastDay = new Date(Date.UTC(input.year, input.month, 0)).getUTCDate();
|
|
89
|
+
const to = `${input.year}-${String(input.month).padStart(2, "0")}-${String(lastDay).padStart(2, "0")}`;
|
|
90
|
+
const title = sheet.getCell("A1");
|
|
91
|
+
title.value = labels.title;
|
|
92
|
+
title.font = { bold: true, size: 14 };
|
|
93
|
+
sheet.mergeCells("A1:J1");
|
|
94
|
+
sheet.getCell("A2").value = `${labels.employee}: ${user.displayName} (${user.login})`;
|
|
95
|
+
sheet.getCell("A3").value = `${labels.period}: ${formatDate(from, locale)} - ${formatDate(to, locale)}`;
|
|
96
|
+
sheet.getCell("A4").value = `${labels.created}: ${formatStamp(input.generatedAt, locale, user.timezone)}`;
|
|
97
|
+
for (const row of [2, 3, 4]) {
|
|
98
|
+
sheet.getRow(row).font = { size: 10 };
|
|
99
|
+
}
|
|
100
|
+
const header = sheet.getRow(HEADER_ROW);
|
|
101
|
+
header.values = [
|
|
102
|
+
labels.date,
|
|
103
|
+
labels.timeIn,
|
|
104
|
+
labels.timeOut,
|
|
105
|
+
labels.worked,
|
|
106
|
+
labels.breaks,
|
|
107
|
+
labels.paidBreaks,
|
|
108
|
+
labels.target,
|
|
109
|
+
labels.balance,
|
|
110
|
+
labels.absence,
|
|
111
|
+
labels.note
|
|
112
|
+
];
|
|
113
|
+
header.font = { bold: true };
|
|
114
|
+
for (let column = 1; column <= 10; column++) {
|
|
115
|
+
const cell = header.getCell(column);
|
|
116
|
+
cell.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFEEF1F5" } };
|
|
117
|
+
cell.border = { bottom: { style: "thin", color: { argb: "FF9AA5B1" } } };
|
|
118
|
+
}
|
|
119
|
+
let rowIndex = FIRST_DAY_ROW;
|
|
120
|
+
const totals = { worked: 0, breaks: 0, paidBreaks: 0, target: 0, balance: 0 };
|
|
121
|
+
for (const day of input.days) {
|
|
122
|
+
const row = sheet.getRow(rowIndex++);
|
|
123
|
+
const notes = [];
|
|
124
|
+
if (day.isHoliday) {
|
|
125
|
+
notes.push(labels.holiday);
|
|
126
|
+
}
|
|
127
|
+
if (day.hasOpenEntry) {
|
|
128
|
+
notes.push(labels.openEntry);
|
|
129
|
+
}
|
|
130
|
+
row.values = [
|
|
131
|
+
formatDate(day.localDate, locale),
|
|
132
|
+
formatTime(day.firstInUtc, user.timezone, locale),
|
|
133
|
+
formatTime(day.lastOutUtc, user.timezone, locale),
|
|
134
|
+
duration(day.workedMin),
|
|
135
|
+
duration(day.breakMin),
|
|
136
|
+
duration(day.paidBreakMin),
|
|
137
|
+
duration(day.targetMin),
|
|
138
|
+
duration(day.balanceMin),
|
|
139
|
+
(_a = day.absenceCode) != null ? _a : "",
|
|
140
|
+
notes.join(", ")
|
|
141
|
+
];
|
|
142
|
+
for (const column of [4, 5, 6, 7, 8]) {
|
|
143
|
+
row.getCell(column).numFmt = DURATION_FORMAT;
|
|
144
|
+
}
|
|
145
|
+
if (day.balanceMin < 0) {
|
|
146
|
+
row.getCell(8).font = { color: { argb: "FFC62828" } };
|
|
147
|
+
}
|
|
148
|
+
if (day.isHoliday || day.workedMin === 0) {
|
|
149
|
+
row.getCell(1).font = { color: { argb: "FF757575" } };
|
|
150
|
+
}
|
|
151
|
+
totals.worked += day.workedMin;
|
|
152
|
+
totals.breaks += day.breakMin;
|
|
153
|
+
totals.paidBreaks += day.paidBreakMin;
|
|
154
|
+
totals.target += day.targetMin;
|
|
155
|
+
totals.balance += day.balanceMin;
|
|
156
|
+
}
|
|
157
|
+
const totalRow = sheet.getRow(rowIndex);
|
|
158
|
+
totalRow.values = [
|
|
159
|
+
labels.total,
|
|
160
|
+
"",
|
|
161
|
+
"",
|
|
162
|
+
duration(totals.worked),
|
|
163
|
+
duration(totals.breaks),
|
|
164
|
+
duration(totals.paidBreaks),
|
|
165
|
+
duration(totals.target),
|
|
166
|
+
duration(totals.balance),
|
|
167
|
+
`${labels.days}: ${input.days.length}`,
|
|
168
|
+
""
|
|
169
|
+
];
|
|
170
|
+
totalRow.font = { bold: true };
|
|
171
|
+
for (const column of [4, 5, 6, 7, 8]) {
|
|
172
|
+
const cell = totalRow.getCell(column);
|
|
173
|
+
cell.numFmt = DURATION_FORMAT;
|
|
174
|
+
cell.border = { top: { style: "thin", color: { argb: "FF9AA5B1" } } };
|
|
175
|
+
}
|
|
176
|
+
if (totals.balance < 0) {
|
|
177
|
+
totalRow.getCell(8).font = { bold: true, color: { argb: "FFC62828" } };
|
|
178
|
+
}
|
|
179
|
+
rowIndex += 1;
|
|
180
|
+
if (input.absences.length > 0) {
|
|
181
|
+
rowIndex += 2;
|
|
182
|
+
const section = sheet.getRow(rowIndex);
|
|
183
|
+
section.values = [labels.absences];
|
|
184
|
+
section.font = { bold: true };
|
|
185
|
+
const sectionHeader = sheet.getRow(rowIndex + 1);
|
|
186
|
+
sectionHeader.values = [labels.absence, labels.from, labels.to, labels.portion, ""];
|
|
187
|
+
sectionHeader.font = { bold: true, size: 10 };
|
|
188
|
+
for (const absence of input.absences) {
|
|
189
|
+
const row = sheet.getRow(rowIndex + 2 + input.absences.indexOf(absence));
|
|
190
|
+
const portion = absence.dayPortion === 0.5 ? labels.halfDay : labels.fullDay;
|
|
191
|
+
row.values = [
|
|
192
|
+
`${absence.typeCode} - ${absence.typeName}`,
|
|
193
|
+
formatDate(absence.dateFrom, locale),
|
|
194
|
+
formatDate(absence.dateTo, locale),
|
|
195
|
+
portion,
|
|
196
|
+
absence.hours === null ? "" : `${absence.hours} h`
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
rowIndex += 2 + input.absences.length;
|
|
200
|
+
}
|
|
201
|
+
rowIndex += 2;
|
|
202
|
+
sheet.getCell(`A${rowIndex}`).value = `${labels.signatureEmployee}: ______________________`;
|
|
203
|
+
sheet.getCell(`F${rowIndex}`).value = `${labels.signatureManager}: ______________________`;
|
|
204
|
+
sheet.getRow(rowIndex).font = { size: 10 };
|
|
205
|
+
return Buffer.from(await workbook.xlsx.writeBuffer());
|
|
206
|
+
}
|
|
207
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
208
|
+
0 && (module.exports = {
|
|
209
|
+
buildMonthReport
|
|
210
|
+
});
|
|
211
|
+
//# sourceMappingURL=xls.js.map
|