From 11e704d0d897d9be7c070fbe8d459a2d71d610f4 Mon Sep 17 00:00:00 2001
From: Patrick Schiller
Date: Thu, 22 Oct 2026 23:48:24 +0200
Subject: [PATCH] feat: add HR working-time PDF exports and release 2.2.0
Signed-off-by: Patrick Schiller
---
.env.prod.example | 2 +-
FEATURES.md | 22 +-
NOTICE | 5 +
README.md | 28 +-
RELEASE_NOTES.md | 80 +++
RELEASING.md | 29 +-
ROADMAP.md | 11 +-
UPGRADING.md | 34 +-
.../api/international-policies.e2e.spec.ts | 10 +-
.../src/api/working-time-reports.e2e.spec.ts | 247 ++++++++++
apps/api/openapi.json | 93 +++-
apps/api/package.json | 1 +
apps/api/src/app/health/health.controller.ts | 2 +-
.../api/src/app/reports/reports.controller.ts | 24 +
apps/api/src/app/reports/reports.dto.ts | 38 +-
apps/api/src/app/reports/reports.service.ts | 178 +++++--
apps/api/src/app/reports/working-time-pdf.ts | 464 ++++++++++++++++++
apps/api/src/assets/fonts/NotoSans-Bold.ttf | Bin 0 -> 838072 bytes
.../api/src/assets/fonts/NotoSans-Regular.ttf | Bin 0 -> 825628 bytes
apps/api/src/assets/fonts/OFL.txt | 93 ++++
apps/api/src/generate-openapi.ts | 2 +-
apps/api/src/main.ts | 2 +-
apps/web/src/api/client.ts | 4 +
apps/web/src/api/generated.ts | 47 +-
apps/web/src/app/i18n.tsx | 20 +
.../src/routes/AdminWorkingTimesPage.spec.tsx | 342 ++++++++++++-
apps/web/src/routes/AdminWorkingTimesPage.tsx | 139 +++++-
apps/web/src/routes/KioskPinPanel.spec.tsx | 7 +-
docker-compose.pin-test.yml | 6 +-
docker-compose.solo-test.yml | 4 +-
docker-compose.test.yml | 4 +-
docs/LOCAL_PIN_DOCKER_TEST.de.md | 15 +-
docs/OPERATING_MODES.md | 2 +-
docs/SOLO_MODE.md | 6 +-
docs/TEAM_MODE.md | 26 +-
docs/WORKING_TIME_EXPORTS.de.md | 32 ++
package.json | 4 +-
pnpm-lock.yaml | 136 +++++
38 files changed, 2030 insertions(+), 129 deletions(-)
create mode 100644 apps/api/src/app/reports/working-time-pdf.ts
create mode 100644 apps/api/src/assets/fonts/NotoSans-Bold.ttf
create mode 100644 apps/api/src/assets/fonts/NotoSans-Regular.ttf
create mode 100644 apps/api/src/assets/fonts/OFL.txt
create mode 100644 docs/WORKING_TIME_EXPORTS.de.md
diff --git a/.env.prod.example b/.env.prod.example
index 6a6eeab..47ad747 100644
--- a/.env.prod.example
+++ b/.env.prod.example
@@ -3,7 +3,7 @@
# Pin deployments to an immutable OpenClockwork release. Do not use `latest`
# for production upgrades.
-OPENCLOCKWORK_VERSION=2.1.0
+OPENCLOCKWORK_VERSION=2.2.0
# Keep these names unchanged across upgrades.
OPENCLOCKWORK_DB_VOLUME=openclockwork-db-data-prod
OPENCLOCKWORK_ATTACHMENTS_VOLUME=openclockwork-attachments-prod
diff --git a/FEATURES.md b/FEATURES.md
index d1d061e..4bd720d 100644
--- a/FEATURES.md
+++ b/FEATURES.md
@@ -1,12 +1,14 @@
# OpenClockwork Features
-OpenClockwork 2.1.0 provides **Solo** personal work tracking and **Team** time and
+OpenClockwork 2.2.0 provides **Solo** personal work tracking and **Team** time and
attendance in one responsive, self-hosted application. Both keep deployment,
data, and configuration under the operator's control, with German and English UI.
-Version 2.1.0 adds direct PIN identification and explicit time booking at Team
-tablets, HR PIN administration, and an authentication audit. It retains the
-personnel-data access restrictions introduced in 2.0.1. RFID is planned separately.
+Version 2.2.0 adds HR working-time PDF exports for one, selected, or all employees,
+including inactive employees with historical bookings. It retains the PIN tablet
+terminals and audit introduced in 2.1.0 and personnel-data access restrictions
+introduced in 2.0.1. RFID is planned separately. See the
+[German legal context and export limitations](docs/WORKING_TIME_EXPORTS.de.md).
This page describes implemented capabilities, not a guarantee that every rule,
device, or external browser handoff has been certified. See
@@ -87,7 +89,7 @@ contains effective work or time off, the accounting mode starts on the next
working-timezone calendar date. See [the mode guide](docs/OPERATING_MODES.md).
Solo customer management and personal billable reports are not Team screens in
-2.1.0. Preserving records across a switch does not make every mode-specific
+2.2.0. Preserving records across a switch does not make every mode-specific
workflow available in both modes.
## Mobile PWA
@@ -227,7 +229,7 @@ Employee submits
| Absence administration | Record and review sickness, training, and flextime entries |
| Approval operations | Manager/HR inboxes, bulk actions, correction loops, and workflow history |
| Terminal administration | Configure, activate, pair, monitor, revoke, re-pair, deactivate, or permanently delete tablet kiosks |
-| Working-time reports | HR-only start, end, break, gross, net, approval, and CSV reporting, with optional clocking locations |
+| Working-time reports | HR-only CSV/PDF reporting for one, selected, or all employees; optional locations in table/CSV only |
Production starts with an empty database. The interactive
`prisma/create-admin.ts` bootstrap offers Solo or Team and creates exactly one
@@ -252,9 +254,17 @@ bookings, and customer-facing activity reports in one administrative workflow.
- Detailed evaluations filtered by period, employee, project, and order
- Customer-facing activity report and CSV export
- Project-independent HR working-time report for all closed, non-rejected entries
+- Direct PDF download for individual employees or a bulk selection, with separate
+ employee sections, personnel numbers, explicit timezone, full timestamps,
+ booking-day/period totals, approval state, and warnings for open timers
+- Historical inactive employees remain selectable for working-time exports
- Opt-in clock-in and clock-out locations in the report table and CSV export,
combining durable terminal labels with available GPS coordinates and accuracy
+PDFs exclude locations, project details, and private notes. Break deductions use
+the stored policy and do not establish when actual rest breaks were taken.
+See [German export requirements and limitations](docs/WORKING_TIME_EXPORTS.de.md).
+
Exports contain employee names and working-time data and must be handled as
personal data under the organisation's access and retention policies. Exact
clocking locations are excluded by default and require an explicit HR action;
diff --git a/NOTICE b/NOTICE
index 1082330..0c47539 100644
--- a/NOTICE
+++ b/NOTICE
@@ -46,3 +46,8 @@ superseded by this NOTICE. A consolidated list of runtime and build-time
dependencies is generated from the package manifests of each application
in the workspace; consult the lockfiles and the upstream projects for the
authoritative license terms.
+
+Working-time PDF reports embed Noto Sans Regular and Bold, distributed under
+the SIL Open Font License, Version 1.1. Copyright 2022 The Noto Project Authors
+(https://github.com/notofonts/latin-greek-cyrillic). The font files and full
+license are included in apps/api/src/assets/fonts/.
diff --git a/README.md b/README.md
index 1835e56..ae7d4d8 100644
--- a/README.md
+++ b/README.md
@@ -14,16 +14,18 @@
-OpenClockwork 2.1.0 provides two operating modes in one open-source application.
+OpenClockwork 2.2.0 provides two operating modes in one open-source application.
**Solo** helps an independent professional track personal work, organise customer
projects, and produce timesheets without inventing an HR department. **Team**
supports employee attendance, work schedules, leave, approvals, and tablet
terminals. Both use the same self-hosted PostgreSQL, API, and responsive PWA.
-**New in 2.1.0:** Team employees can identify themselves directly at a paired
-tablet with their personal number and a separate terminal PIN, then explicitly
-confirm clock-in or clock-out. HR manages PINs, terminal authentication methods,
-and the authentication audit. The access-control fixes from 2.0.1 are retained.
+**New in 2.2.0:** HR can download working-time PDFs alongside CSV for one,
+selected, or all employees, including inactive employees with historical
+bookings. Reports show full timestamps, break deductions, net work, status,
+and totals, with a separate section per person. Read the
+[German legal context and export limitations](docs/WORKING_TIME_EXPORTS.de.md).
+PIN tablet terminals from 2.1.0 and the access-control fixes from 2.0.1 are retained.
The interface is available in German and English on phones, tablets, and
desktops. Language is independent of working-time rules: targets, breaks,
@@ -33,7 +35,7 @@ country. No proprietary time-clock hardware or hosted subscription is required.
**Existing installations remain in Team mode after an upgrade.** A mode change
is a separate, authenticated administrator action with a preview and safety
checks. Invoice creation, monetary billing, and CAUR coding-agent usage import
-are not included in 2.1.0; see the [roadmap](ROADMAP.md).
+are not included in 2.2.0; see the [roadmap](ROADMAP.md).
## Choose your mode
@@ -89,7 +91,11 @@ Solo customer-management and billable-timesheet UI.
bulk actions, and workflow history.
- **Projects and reporting.** Assign employees, structure projects by service
order, compare PLAN and IST hours, edit booking targets, split entries, and
- export customer or working-time reports with optional clock-in/out locations.
+ export customer or working-time reports as CSV. HR can download working-time
+ PDFs for one, selected, or all employees, with a separate section per person.
+ Clock-in/out locations are optional in HR tables and CSV and excluded from PDFs.
+ See the [German working-time export guidance](docs/WORKING_TIME_EXPORTS.de.md)
+ for the legal context and the limits of automatic break deductions.
- **Self-hosted and API-first.** PostgreSQL, NestJS, React, OpenAPI, Socket.IO,
Docker, and an Azure reference deployment—without SaaS lock-in.
- **Localised interface.** German and English translations, browser-aware dates,
@@ -190,8 +196,8 @@ and read [UPGRADING.md](UPGRADING.md) before changing an existing installation.
The PWA is online-first. A running timer remains server state while a device is
offline; new writes require server confirmation and are not queued as offline
-bookings. CSV export starts a browser download, and print/PDF uses the browser's
-print dialog. Verify both handoffs in your chosen browser or installed PWA;
+bookings. CSV and HR working-time PDF exports start browser downloads; Solo
+print/PDF uses the browser's print dialog. Verify these handoffs in your chosen browser or installed PWA;
embedded browsers may handle downloads and printing differently.
## Tech stack
@@ -335,7 +341,7 @@ repository and enter its directory:
```bash
git clone https://github.com/patrickschiller/openclockwork.git
cd openclockwork
-git switch --detach v2.1.0
+git switch --detach v2.2.0
cp .env.prod.example .env.prod
```
@@ -366,7 +372,7 @@ openssl rand -hex 32
Open `.env.prod` in an editor and replace every `change-me` value:
-- Set `OPENCLOCKWORK_VERSION=2.1.0` for this release. Use the same explicit
+- Set `OPENCLOCKWORK_VERSION=2.2.0` for this release. Use the same explicit
version for the API and web images; never deploy `latest`.
- Put the first command's output into `POSTGRES_PASSWORD` and replace
`change-me-database-password` inside `DATABASE_URL` with that exact same
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index b6a5a75..bf153e2 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,3 +1,83 @@
+# OpenClockwork v2.2.0
+
+## Working-time PDFs for HR
+
+- HR can download a working-time PDF alongside the existing CSV export for one
+ employee, several selected employees, or all employees in the chosen period.
+ Historical records of inactive employees remain selectable. Each employee
+ starts on a new page in a combined document.
+- Reports include the employee's name and personnel number, reporting period,
+ export timestamp, installation timezone, full start/end dates and UTC offsets,
+ gross time, rule-based break deductions, net time, booking status and totals.
+ Elapsed time retains second precision in the report. Stored break-rule
+ snapshots determine deductions; changing a schedule does not rewrite history.
+- Pending completed bookings are labelled provisional. Open bookings are
+ flagged and excluded from totals; rejected and cancelled entries are omitted.
+ Overnight work belongs to its booking's start day, so daily subtotals are
+ booking-day totals rather than calendar-day compliance checks. Full dates and
+ UTC offsets make overnight work and daylight-saving transitions traceable.
+- PDF downloads enforce HR authorization and organization scope. Location data,
+ free-text notes and project details are excluded. Combined PDFs contain
+ personal data for several employees: restrict access and share them only with
+ the intended recipients.
+- Requested in [Discussion #37](https://github.com/patrickschiller/openclockwork/discussions/37).
+ A [two-page example PDF](https://github.com/user-attachments/files/33845433/openclockwork-arbeitszeitnachweis-beispiel.pdf)
+ uses synthetic data only. Read the
+ [German working-time export guide](https://github.com/patrickschiller/openclockwork/blob/v2.2.0/docs/WORKING_TIME_EXPORTS.de.md)
+ for selection, calculations and limitations.
+
+## Legal context in Germany
+
+- Employers must generally record the beginning, end and duration of daily
+ working time. German law does not prescribe a universal PDF layout or a
+ general PDF-export requirement for these records. See the
+ [BMAS working-time recording FAQ](https://www.bmas.de/DE/Arbeit/Arbeitsrecht/Arbeitnehmerrechte/Regelungen-zur-Arbeitszeit/Fragen-und-Antworten/faq-arbeitszeiterfassung.html)
+ and the [BAG decision of 13 September 2022, 1 ABR 22/21](https://www.bundesarbeitsgericht.de/entscheidung/1-abr-22-21/).
+- [Section 16(2) ArbZG](https://www.gesetze-im-internet.de/arbzg/__16.html)
+ requires the records specified there, including work beyond the working-day
+ limit under section 3 sentence 1, to be retained for at least two years.
+ [Section 17(1) MiLoG](https://www.gesetze-im-internet.de/milog/__17.html)
+ separately requires covered employers, notably for specified marginal
+ employment and sectors, to record beginning, end and duration by the end of
+ the seventh following calendar day and retain the records for at least two
+ years. Its scope and exemptions matter: this seven-day deadline is not a
+ general deadline for every employment relationship.
+- Automatic break deductions do not prove that breaks were actually taken.
+ The PDF states this limitation; it does not record actual break intervals.
+ Employers remain responsible for accurate, complete records, actual breaks,
+ lawful processing, access and retention. The export is a documentation aid,
+ not a certification of legal compliance or a complete payroll statement.
+ For minimum-wage documentation,
+ [BMAS confirms that signatures are not required](https://www.bmas.de/DE/Arbeit/Arbeitsrecht/Mindestlohn/Dokumentationspflicht/dokumentationspflicht.html).
+ The cited rules prescribe neither PDF/A nor export certification;
+ sector-specific rules and agreements may apply.
+ Legal sources were reviewed on 22 October 2026.
+
+## Upgrade and compatibility
+
+- Upgrading from 2.1.0 adds no database migrations and requires no seed, reset,
+ new secret or change to the working timezone. Preserve existing data,
+ attachment volumes, configuration and historical migrations. When skipping
+ older releases, follow their intervening requirements, including the 2.1.0
+ PIN migration. See the
+ [upgrade guide](https://github.com/patrickschiller/openclockwork/blob/v2.2.0/UPGRADING.md).
+- Deploy matching API and web images. If deployed separately, update the API
+ first, wait for `/api/health` to report `2.2.0`, then deploy the web image.
+ Reopen existing browser/PWA tabs to load the new export controls. The API
+ contract and generated web client include the PDF endpoint; existing CSV
+ exports, working-time accounts, approvals and break policies remain supported.
+- The PIN/QR functionality introduced in 2.1.0 and personnel-data access controls
+ introduced in 2.0.1 are retained. PIN deployment still requires its independent
+ secrets, restricted runtime role and operator-managed audit retention.
+
+## Docker images
+
+- `ghcr.io/patrickschiller/openclockwork-api:2.2.0`
+- `ghcr.io/patrickschiller/openclockwork-web:2.2.0`
+
+The release workflow also updates each image's `latest` tag. Pin `2.2.0` or a
+verified digest for production and use the same release for both images.
+
# OpenClockwork v2.1.0
## PIN tablet terminals
diff --git a/RELEASING.md b/RELEASING.md
index 755f353..68214ea 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -25,15 +25,18 @@ public-contract impact. Use matching metadata in five places:
5. Run `RELEASE_TAG=vMAJOR.MINOR.PATCH pnpm release:verify-tag` and the full CI
suite.
-For 2.1.0, include the additive PIN migration and matching QR/PIN API contract,
-upgrade instructions, role-setup tools and audit verification tools. Retain the
-2.0.1 personnel-data access fixes. Verify QR defaults and existing records across
-the upgrade, PIN and credential-access tests, concurrent booking/revocation,
-audit integrity and the separate runtime-role Docker path. Record the accepted
-local user check without claiming an unperformed target-device or external
-archive test. Production PIN activation still requires the operator's protected
-archive and retention configuration; publishing the source and images does not
-provision or certify that deployment.
+For 2.2.0, include the individual and bulk HR working-time PDF export, matching
+OpenAPI contract and client, legal context and upgrade instructions. Verify HR
+authorization and organization scope, selected/all employees including inactive
+employees with historical records, pending/open bookings, historical break-rule
+snapshots, second precision, overnight work and daylight-saving transitions.
+Render representative PDFs and inspect pagination, employee boundaries, totals
+and German/English text. Use synthetic data for public examples and attachments.
+There are no new migrations relative to 2.1.0; retain the PIN migration and all
+earlier security fixes. State the scoped German retention and recording rules
+without presenting the PDF as a legal certification. Production PIN activation
+still requires the operator's protected archive and retention configuration;
+publishing the source and images does not provision or certify that deployment.
## Publish
@@ -43,8 +46,8 @@ annotated tag on the exact merge commit and push it:
```bash
git switch main
git pull --ff-only
-git tag -a v2.1.0 -m "OpenClockwork v2.1.0"
-git push origin v2.1.0
+git tag -a v2.2.0 -m "OpenClockwork v2.2.0"
+git push origin v2.2.0
```
The release workflow verifies the tag and notes, runs Nx and API end-to-end
@@ -60,8 +63,8 @@ pulled anonymously by self-hosted installations.
## Verify the published release
```bash
-docker pull ghcr.io/patrickschiller/openclockwork-api:2.1.0
-docker pull ghcr.io/patrickschiller/openclockwork-web:2.1.0
+docker pull ghcr.io/patrickschiller/openclockwork-api:2.2.0
+docker pull ghcr.io/patrickschiller/openclockwork-web:2.2.0
```
Confirm that the GitHub Release is marked latest, contains the curated upgrade
diff --git a/ROADMAP.md b/ROADMAP.md
index 19c02ff..e32cec4 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,9 +1,10 @@
# OpenClockwork Roadmap
OpenClockwork is a public, Apache-2.0 self-hosted application for personal work
-and Team time and attendance. Version **2.1.0** adds PIN tablet terminals to
-Team, building on the Solo/Team workflows from 2.0 and the access-control fixes
-from 2.0.1. Customer invoicing and coding-agent usage accounting remain
+and Team time and attendance. Version **2.2.0** adds HR working-time PDF exports
+for one or several employees, building on PIN tablet terminals from 2.1.0,
+Solo/Team workflows from 2.0 and the access-control fixes from 2.0.1.
+Customer invoicing and coding-agent usage accounting remain
future work; a billable time flag does not mean those features already exist.
This roadmap expresses priorities and intended scope, not release dates or
@@ -15,7 +16,7 @@ boundaries, and [README.md](README.md) for release installation.
| Stage | Scope | Status / dependency |
| ----- | --------------------------------------------------- | --------------------------------------- |
-| 1 | Country-neutral configuration and defaults | Included in 1.4.0; retained in 2.1.0 |
+| 1 | Country-neutral configuration and defaults | Included in 1.4.0; retained in 2.2.0 |
| 2 | Complete personal Solo workflow alongside Team | Included in 2.0.0 |
| 3 | QR/PIN tablet terminals and HR authentication audit | Included in 2.1.0; Team only |
| 4 | Optional RFID terminal extension | Planned separately after PIN acceptance |
@@ -178,7 +179,7 @@ customer invoice from tracked work and follow it through payment or correction.
[CAUR — Coding Agent Usage Record](https://github.com/patrickschiller/caur)
defines a vendor-neutral usage record for a completed or interrupted coding-agent
run. It is a separate specification project, not an invoice format or pricing
-catalogue, and is not imported by OpenClockwork 2.1.0. A future integration should
+catalogue, and is not imported by OpenClockwork 2.2.0. A future integration should
consume explicitly supported schema versions and provide attribution, review,
and billing around them. Its design should follow the
[CAUR specification](https://github.com/patrickschiller/caur/blob/main/SPEC.md)
diff --git a/UPGRADING.md b/UPGRADING.md
index 99fc404..073779d 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -25,6 +25,32 @@ Compose from managing their deletion, but this does not protect against manual
volume removal, pruning unused volumes, Docker data loss or host failure. Do
not replace a populated installation's volume with a new name during an upgrade.
+## Required for 2.2.0
+
+Version 2.2.0 adds HR working-time PDF exports for one, selected, or all
+employees alongside CSV. Upgrading from 2.1.0 adds **no database migrations** and
+requires no seed, reset, new secret or timezone change. Preserve the existing
+database, attachment volumes, configuration and all applied migration files.
+Take a consistent backup and deploy matching 2.2.0 API/web images. If upgrading
+from an older version, also follow every intervening requirement below,
+including the 2.1.0 PIN migration where not yet applied.
+
+Deploy the API first when components are updated separately; wait for
+`/api/health` to report `2.2.0`, then deploy the web image. Reopen browser/PWA
+tabs to load the new PDF controls. Custom clients can use the updated
+[OpenAPI contract](apps/api/openapi.json); the PDF endpoint requires HR access.
+Verify exports for an individual and a selection of employees, employee page
+boundaries, totals and the configured working timezone before sharing reports.
+
+Review the [German export guide](docs/WORKING_TIME_EXPORTS.de.md) before using
+the PDF as a working-time record. Automatic break deductions do not prove
+actual breaks, pending bookings remain provisional, and overnight bookings are
+assigned to their start day. A downloaded PDF alone does not establish legal
+compliance or fulfill an installation's retention obligations. Combined PDFs
+contain personal data from several employees and need appropriate access and
+sharing controls. Existing CSV exports, working-time policies, approvals,
+accounts, QR/PIN support and personnel-data access restrictions are retained.
+
## Required for 2.1.0
Version 2.1.0 adds personal-number/PIN identification to paired Team terminals.
@@ -83,7 +109,7 @@ existing installations is recommended. From 2.0.0,
this update adds no database migrations and requires no seed or reset.
Keep the existing data, volumes, configuration and working timezone, take a
consistent backup, and deploy matching API/web versions. These access changes
-also apply when upgrading directly to 2.1.0; follow its additional requirements
+also apply when upgrading directly to 2.2.0; follow the additional requirements
above and the deployment procedure below.
Custom API clients must handle `401` and `403` responses and the scoped
@@ -387,7 +413,7 @@ runs in a non-production runtime; the API retains its production runtime.
## 2. Select and pull the release
Set `OPENCLOCKWORK_VERSION` in `.env.prod` to the exact version from the GitHub
-Release, **`2.1.0` for this upgrade**, only after preserving the old configuration
+Release, **`2.2.0` for this upgrade**, only after preserving the old configuration
in the backup set. Pin matching API/web versions or verified digests. Do not use
`latest`, a mutable `:local` image, or a source-build override for an unattended
production upgrade. Keep the same PostgreSQL major version; a PostgreSQL major
@@ -414,7 +440,7 @@ migrations and runtime grants; the API waits for its successful completion.
Prisma records applied migrations in `_prisma_migrations` and skips them on
subsequent starts. Production startup never seeds or resets the database.
If API/web are deployed independently, deploy the API first, wait for its
-health endpoint to report `2.1.0`, then deploy the matching web image. The web
+health endpoint to report `2.2.0`, then deploy the matching web image. The web
startup version check is not a promise of zero-downtime mixed-version operation;
keep additional writers paused until verification completes.
@@ -451,7 +477,7 @@ oc run --rm --no-deps -T migrate ./node_modules/.bin/prisma migrate status
The PIN runtime role deliberately has no access to `_prisma_migrations`; do not
broaden its permissions to run this maintenance check from the API container.
-Confirm `2.1.0` through the public web/proxy health route as well as the
+Confirm `2.2.0` through the public web/proxy health route as well as the
container check. Sign in and verify the actual deployed mode, a known employee,
an existing time entry, historic break totals, leave balances and configured
attachment storage. A 1.4.0 upgrade must still be Team unless an administrator
diff --git a/apps/api-e2e/src/api/international-policies.e2e.spec.ts b/apps/api-e2e/src/api/international-policies.e2e.spec.ts
index 078ad3c..b8aa6e1 100644
--- a/apps/api-e2e/src/api/international-policies.e2e.spec.ts
+++ b/apps/api-e2e/src/api/international-policies.e2e.spec.ts
@@ -221,7 +221,15 @@ describe('International calendars and configurable break policies', () => {
)
.set('Authorization', `Bearer ${hrToken}`)
.expect(200);
- expect(report.body.totals).toEqual(closed.body.summary);
+ // The report preserves timestamp precision; account/ERP summaries keep
+ // their historical whole-minute policy. Both use the captured deduction.
+ const reportGrossMinutes =
+ ((persisted.clockOut as Date).getTime() - clockIn.getTime()) / 60_000;
+ expect(report.body.totals).toEqual({
+ grossMinutes: reportGrossMinutes,
+ breakMinutes: 20,
+ netMinutes: reportGrossMinutes - 20,
+ });
const exported = await ctx.http
.get('/api/erp/timeentries')
.set('X-API-Key', process.env.ERP_API_KEY ?? 'e2e-erp-key')
diff --git a/apps/api-e2e/src/api/working-time-reports.e2e.spec.ts b/apps/api-e2e/src/api/working-time-reports.e2e.spec.ts
index 107943e..27efd7f 100644
--- a/apps/api-e2e/src/api/working-time-reports.e2e.spec.ts
+++ b/apps/api-e2e/src/api/working-time-reports.e2e.spec.ts
@@ -1,3 +1,4 @@
+import { randomUUID } from 'node:crypto';
import {
createTestApp,
login,
@@ -193,4 +194,250 @@ describe('Project-independent working-time reports', () => {
.set('Authorization', `Bearer ${employeeToken}`)
.expect(403);
});
+
+ it('filters the JSON report to an explicit subset of employees', async () => {
+ const { hr, manager, report, foreign } = await fixture();
+ const token = await login(ctx.http, hr.email);
+ await ctx.prisma.timeEntry.createMany({
+ data: [manager, report, foreign].map((employee) => ({
+ employeeId: employee.id,
+ clockIn: new Date('2026-08-10T07:00:00.000Z'),
+ clockOut: new Date('2026-08-10T08:00:00.000Z'),
+ status: 'Approved',
+ })),
+ });
+
+ const selected = await ctx.http
+ .get(
+ `/api/reports/working-times?from=2026-08-01&to=2026-08-31&employeeIds=${report.id}&employeeIds=${foreign.id}`,
+ )
+ .set('Authorization', `Bearer ${token}`)
+ .expect(200);
+ expect(selected.body.rows).toHaveLength(2);
+ expect(
+ new Set(
+ selected.body.rows.map((row: { employeeId: string }) => row.employeeId),
+ ),
+ ).toEqual(new Set([report.id, foreign.id]));
+ expect(selected.body.totals).toEqual({
+ grossMinutes: 120,
+ breakMinutes: 0,
+ netMinutes: 120,
+ });
+
+ const one = await ctx.http
+ .get(
+ `/api/reports/working-times?from=2026-08-01&to=2026-08-31&employeeIds=${report.id}`,
+ )
+ .set('Authorization', `Bearer ${token}`)
+ .expect(200);
+ expect(one.body.rows).toHaveLength(1);
+ expect(one.body.rows[0].employeeId).toBe(report.id);
+ });
+
+ it('retains seconds in ungrouped report durations and preserves captured breaks', async () => {
+ const { hr, report } = await fixture();
+ const token = await login(ctx.http, hr.email);
+ await ctx.prisma.timeEntry.create({
+ data: {
+ employeeId: report.id,
+ clockIn: new Date('2026-08-10T07:00:01.000Z'),
+ clockOut: new Date('2026-08-10T15:00:59.000Z'),
+ breakRules: [{ afterMinutes: 360, breakMinutes: 30 }],
+ status: 'Approved',
+ },
+ });
+
+ const result = await ctx.http
+ .get(
+ `/api/reports/working-times?from=2026-08-10&to=2026-08-10&employeeId=${report.id}`,
+ )
+ .set('Authorization', `Bearer ${token}`)
+ .expect(200);
+ expect(result.body.rows).toHaveLength(1);
+ expect(result.body.rows[0]).toMatchObject({
+ clockIn: '2026-08-10T07:00:01.000Z',
+ clockOut: '2026-08-10T15:00:59.000Z',
+ breakMinutes: 30,
+ });
+ expect(result.body.rows[0].grossMinutes).toBeCloseTo(480 + 58 / 60, 8);
+ expect(result.body.rows[0].netMinutes).toBeCloseTo(450 + 58 / 60, 8);
+ expect(result.body.totals.grossMinutes).toBeCloseTo(480 + 58 / 60, 8);
+ expect(result.body.totals.breakMinutes).toBe(30);
+ expect(result.body.totals.netMinutes).toBeCloseTo(450 + 58 / 60, 8);
+ });
+
+ it('preserves capture-group break allocation when a sibling falls outside the selected dates', async () => {
+ const { hr, report } = await fixture();
+ const token = await login(ctx.http, hr.email);
+ const common = {
+ employeeId: report.id,
+ captureGroupId: randomUUID(),
+ breakRules: [{ afterMinutes: 360, breakMinutes: 30 }],
+ status: 'Approved' as const,
+ };
+ await ctx.prisma.timeEntry.createMany({
+ data: [
+ {
+ ...common,
+ clockIn: new Date('2026-08-10T18:00:00.000Z'),
+ clockOut: new Date('2026-08-10T22:00:00.000Z'),
+ },
+ {
+ ...common,
+ clockIn: new Date('2026-08-11T06:00:00.000Z'),
+ clockOut: new Date('2026-08-11T10:00:00.000Z'),
+ },
+ {
+ ...common,
+ clockIn: new Date('2026-08-12T06:00:00.000Z'),
+ clockOut: new Date('2026-08-12T10:00:00.000Z'),
+ status: 'Rejected',
+ },
+ {
+ ...common,
+ clockIn: new Date('2026-08-13T06:00:00.000Z'),
+ clockOut: new Date('2026-08-13T10:00:00.000Z'),
+ voidedAt: new Date(),
+ },
+ ],
+ });
+
+ for (const to of ['2026-08-10', '2026-08-11']) {
+ const result = await ctx.http
+ .get(
+ `/api/reports/working-times?from=2026-08-10&to=${to}&employeeId=${report.id}`,
+ )
+ .set('Authorization', `Bearer ${token}`)
+ .expect(200);
+ const count = to === '2026-08-10' ? 1 : 2;
+ expect(result.body.rows).toHaveLength(count);
+ for (const row of result.body.rows) {
+ expect(row).toMatchObject({
+ grossMinutes: 240,
+ breakMinutes: 15,
+ netMinutes: 225,
+ });
+ }
+ expect(result.body.totals).toEqual({
+ grossMinutes: 240 * count,
+ breakMinutes: 15 * count,
+ netMinutes: 225 * count,
+ });
+ }
+ });
+
+ it('downloads PDF attachments for single, selected and all employees in both languages', async () => {
+ const { hr, manager, report, foreign } = await fixture();
+ const token = await login(ctx.http, hr.email);
+ await ctx.prisma.employee.update({
+ where: { id: foreign.id },
+ data: { isActive: false },
+ });
+ await ctx.prisma.timeEntry.createMany({
+ data: [
+ {
+ employeeId: report.id,
+ clockIn: new Date('2026-08-10T07:00:00.000Z'),
+ clockOut: new Date('2026-08-10T15:00:00.000Z'),
+ breakRules: [{ afterMinutes: 360, breakMinutes: 30 }],
+ status: 'Approved',
+ note: 'Private note not intended for a working-time PDF',
+ terminalLocationLabel: 'Private location',
+ },
+ {
+ employeeId: foreign.id,
+ clockIn: new Date('2026-08-10T07:00:00.000Z'),
+ clockOut: new Date('2026-08-10T08:00:00.000Z'),
+ status: 'Pending',
+ },
+ {
+ employeeId: report.id,
+ clockIn: new Date('2026-08-11T07:00:00.000Z'),
+ clockOut: null,
+ status: 'Open',
+ },
+ ],
+ });
+
+ for (const filter of [
+ `&employeeId=${report.id}`,
+ `&employeeIds=${report.id}&employeeIds=${foreign.id}&locale=en`,
+ `&employeeIds=${foreign.id}&locale=de`,
+ `&employeeId=${manager.id}`,
+ '',
+ ]) {
+ const result = await ctx.http
+ .get(
+ `/api/reports/working-times.pdf?from=2026-08-01&to=2026-08-31${filter}`,
+ )
+ .set('Authorization', `Bearer ${token}`)
+ .buffer(true)
+ .parse((res, callback) => {
+ const chunks: Buffer[] = [];
+ res.on('data', (chunk: Buffer) => chunks.push(chunk));
+ res.on('end', () => callback(null, Buffer.concat(chunks)));
+ res.on('error', callback);
+ })
+ .expect(200);
+ expect(result.headers['content-type']).toContain('application/pdf');
+ expect(result.headers['content-disposition']).toBe(
+ 'attachment; filename="openclockwork-working-times-2026-08-01-2026-08-31.pdf"',
+ );
+ expect(result.headers['cache-control']).toContain('no-store');
+ expect(result.headers['x-content-type-options']).toBe('nosniff');
+ expect(Buffer.isBuffer(result.body)).toBe(true);
+ expect((result.body as Buffer).subarray(0, 5).toString()).toBe('%PDF-');
+ expect((result.body as Buffer).subarray(-20).toString()).toContain(
+ '%%EOF',
+ );
+ }
+ });
+
+ it('restricts PDF downloads to authenticated HR administrators', async () => {
+ const { manager, report } = await fixture();
+ const url = '/api/reports/working-times.pdf?from=2026-08-01&to=2026-08-31';
+ await ctx.http.get(url).expect(401);
+ for (const employee of [manager, report]) {
+ const token = await login(ctx.http, employee.email);
+ await ctx.http
+ .get(url)
+ .set('Authorization', `Bearer ${token}`)
+ .expect(403);
+ }
+ });
+
+ it('validates PDF date ranges, employee selections and locale', async () => {
+ const { hr, report, foreign } = await fixture();
+ const token = await login(ctx.http, hr.email);
+ const validDates = 'from=2026-08-01&to=2026-08-31';
+ const invalidQueries = [
+ 'from=2026-02-31&to=2026-03-01',
+ 'from=2026-08-31&to=2026-08-01',
+ 'from=2025-01-01&to=2026-12-31',
+ 'to=2026-08-31',
+ `${validDates}&employeeId=invalid`,
+ `${validDates}&employeeIds=${report.id}&employeeIds=invalid`,
+ `${validDates}&employeeId=${report.id}&employeeIds=${foreign.id}`,
+ `${validDates}&employeeIds=${report.id}&employeeIds=${report.id}`,
+ `${validDates}&employeeIds=${randomUUID()}`,
+ `${validDates}&locale=fr`,
+ ];
+ for (const query of invalidQueries) {
+ await ctx.http
+ .get(`/api/reports/working-times.pdf?${query}`)
+ .set('Authorization', `Bearer ${token}`)
+ .expect(400);
+ }
+
+ for (const filter of [
+ `employeeId=${report.id}&employeeIds=${foreign.id}`,
+ `employeeIds=${report.id}&employeeIds=invalid`,
+ ]) {
+ await ctx.http
+ .get(`/api/reports/working-times?${validDates}&${filter}`)
+ .set('Authorization', `Bearer ${token}`)
+ .expect(400);
+ }
+ });
});
diff --git a/apps/api/openapi.json b/apps/api/openapi.json
index d4e214e..1a60945 100644
--- a/apps/api/openapi.json
+++ b/apps/api/openapi.json
@@ -1856,6 +1856,18 @@
"type": "string"
}
},
+ {
+ "name": "employeeIds",
+ "required": false,
+ "in": "query",
+ "description": "Repeat employeeIds for a subset (maximum 200). Omit both employee filters for all employees; cannot be combined with employeeId.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
{
"name": "includeLocations",
"required": false,
@@ -1886,6 +1898,83 @@
"tags": ["reports"]
}
},
+ "/api/reports/working-times.pdf": {
+ "get": {
+ "operationId": "ReportsController_workingTimesPdf",
+ "parameters": [
+ {
+ "name": "from",
+ "required": true,
+ "in": "query",
+ "schema": {
+ "format": "date",
+ "example": "2026-08-01",
+ "type": "string"
+ }
+ },
+ {
+ "name": "to",
+ "required": true,
+ "in": "query",
+ "schema": {
+ "format": "date",
+ "example": "2026-08-31",
+ "type": "string"
+ }
+ },
+ {
+ "name": "employeeId",
+ "required": false,
+ "in": "query",
+ "schema": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ {
+ "name": "employeeIds",
+ "required": false,
+ "in": "query",
+ "description": "Repeat employeeIds for a subset (maximum 200). Omit both employee filters for all employees; cannot be combined with employeeId.",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "name": "locale",
+ "required": false,
+ "in": "query",
+ "schema": {
+ "default": "de",
+ "type": "string",
+ "enum": ["de", "en"]
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Working-time PDF for one, selected or all employees, with separate employee sections, booking-day totals, timezone and policy-based break deductions. Excludes locations and private notes.",
+ "content": {
+ "application/pdf": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "tags": ["reports"]
+ }
+ },
"/api/timeentries": {
"get": {
"operationId": "TimeEntriesController_list",
@@ -4169,7 +4258,7 @@
"info": {
"title": "OpenClockwork API",
"description": "Self-hostable working-time tracker — REST + WebSocket surface.",
- "version": "2.1.0",
+ "version": "2.2.0",
"contact": {}
},
"tags": [],
@@ -4990,7 +5079,7 @@
},
"version": {
"type": "string",
- "example": "2.1.0"
+ "example": "2.2.0"
},
"utcTimestamp": {
"type": "string",
diff --git a/apps/api/package.json b/apps/api/package.json
index 5e7bd18..a39d5b1 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -96,6 +96,7 @@
"class-validator": "*",
"passport": "*",
"passport-jwt": "*",
+ "pdfkit": "^0.17.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0",
"shared": "workspace:*",
diff --git a/apps/api/src/app/health/health.controller.ts b/apps/api/src/app/health/health.controller.ts
index d0da296..4cc5607 100644
--- a/apps/api/src/app/health/health.controller.ts
+++ b/apps/api/src/app/health/health.controller.ts
@@ -9,7 +9,7 @@ export class HealthResponseDto {
@ApiProperty({ example: 'openclockwork-api' })
service!: string;
- @ApiProperty({ example: '2.1.0' })
+ @ApiProperty({ example: '2.2.0' })
version!: string;
@ApiProperty({ format: 'date-time' })
diff --git a/apps/api/src/app/reports/reports.controller.ts b/apps/api/src/app/reports/reports.controller.ts
index 7677f37..4cf0e10 100644
--- a/apps/api/src/app/reports/reports.controller.ts
+++ b/apps/api/src/app/reports/reports.controller.ts
@@ -8,6 +8,7 @@ import {
WorkingTimeReportDto,
WorkingTimeReportEmployeeDto,
WorkingTimeReportQueryDto,
+ WorkingTimePdfQueryDto,
} from './reports.dto';
import { ReportsService } from './reports.service';
import { CurrentUser } from '../auth/current-user.decorator';
@@ -65,4 +66,27 @@ export class ReportsController {
): Promise {
return this.reports.workingTimes(query);
}
+
+ @Get('working-times.pdf')
+ @ApiOkResponse({
+ description:
+ 'Working-time PDF for one, selected or all employees, with separate employee sections, booking-day totals, timezone and policy-based break deductions. Excludes locations and private notes.',
+ content: {
+ 'application/pdf': { schema: { type: 'string', format: 'binary' } },
+ },
+ })
+ async workingTimesPdf(
+ @Query() query: WorkingTimePdfQueryDto,
+ @Res() response: Response,
+ ): Promise {
+ const pdf = await this.reports.workingTimesPdf(query);
+ response.setHeader('Content-Type', 'application/pdf');
+ response.setHeader(
+ 'Content-Disposition',
+ `attachment; filename="openclockwork-working-times-${query.from}-${query.to}.pdf"`,
+ );
+ response.setHeader('Cache-Control', 'private, no-store');
+ response.setHeader('X-Content-Type-Options', 'nosniff');
+ response.send(pdf);
+ }
}
diff --git a/apps/api/src/app/reports/reports.dto.ts b/apps/api/src/app/reports/reports.dto.ts
index 2b3f712..d953fcf 100644
--- a/apps/api/src/app/reports/reports.dto.ts
+++ b/apps/api/src/app/reports/reports.dto.ts
@@ -1,5 +1,15 @@
-import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
-import { IsIn, IsOptional, IsUUID, Matches } from 'class-validator';
+import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger';
+import { Transform } from 'class-transformer';
+import {
+ ArrayMaxSize,
+ ArrayMinSize,
+ ArrayUnique,
+ IsArray,
+ IsIn,
+ IsOptional,
+ IsUUID,
+ Matches,
+} from 'class-validator';
const DATE_ONLY_PATTERN = '^\\d{4}-\\d{2}-\\d{2}$';
@@ -17,12 +27,36 @@ export class WorkingTimeReportQueryDto {
@IsUUID()
employeeId?: string;
+ @ApiPropertyOptional({
+ type: [String],
+ description:
+ 'Repeat employeeIds for a subset (maximum 200). Omit both employee filters for all employees; cannot be combined with employeeId.',
+ })
+ @IsOptional()
+ @Transform(({ value }) => (Array.isArray(value) ? value : [value]))
+ @IsArray()
+ @ArrayMinSize(1)
+ @ArrayMaxSize(200)
+ @ArrayUnique()
+ @IsUUID('all', { each: true })
+ employeeIds?: string[];
+
@ApiPropertyOptional({ type: Boolean, default: false })
@IsOptional()
@IsIn(['true', 'false'])
includeLocations?: string;
}
+export class WorkingTimePdfQueryDto extends OmitType(
+ WorkingTimeReportQueryDto,
+ ['includeLocations'] as const,
+) {
+ @ApiPropertyOptional({ enum: ['de', 'en'], default: 'de' })
+ @IsOptional()
+ @IsIn(['de', 'en'])
+ locale?: 'de' | 'en';
+}
+
export class WorkingTimeReportEmployeeDto {
@ApiProperty({ format: 'uuid' })
id!: string;
diff --git a/apps/api/src/app/reports/reports.service.ts b/apps/api/src/app/reports/reports.service.ts
index 0ab9656..f60d2d8 100644
--- a/apps/api/src/app/reports/reports.service.ts
+++ b/apps/api/src/app/reports/reports.service.ts
@@ -1,5 +1,4 @@
import { BadRequestException, Injectable } from '@nestjs/common';
-import { summarize, parseBreakRules } from 'shared';
import { PrismaService } from '../prisma/prisma.service';
import { Prisma } from '@prisma/client';
import { InstallationService } from '../installation/installation.service';
@@ -8,10 +7,12 @@ import {
type WorkingTimeReportEmployeeDto,
type WorkingTimeReportDto,
type WorkingTimeReportQueryDto,
+ type WorkingTimePdfQueryDto,
type SoloReportQueryDto,
type SoloReportDto,
type SoloReportRowDto,
} from './reports.dto';
+import { renderWorkingTimePdf } from './working-time-pdf';
const DAY_MS = 86_400_000;
const MAX_REPORT_DAYS = 366;
@@ -272,9 +273,6 @@ export class ReportsService {
async workingTimeEmployees(): Promise {
return this.prisma.employee.findMany({
- where: {
- isActive: true,
- },
select: { id: true, firstName: true, lastName: true },
orderBy: [{ lastName: 'asc' }, { firstName: 'asc' }],
});
@@ -283,6 +281,78 @@ export class ReportsService {
async workingTimes(
query: WorkingTimeReportQueryDto,
): Promise {
+ return this.prisma.$transaction((tx) => this.buildWorkingTimes(query, tx), {
+ isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead,
+ });
+ }
+
+ async workingTimesPdf(query: WorkingTimePdfQueryDto): Promise {
+ const input = await this.prisma.$transaction(
+ async (tx) => {
+ const report = await this.buildWorkingTimes(
+ { ...query, includeLocations: 'false' },
+ tx,
+ );
+ const from = parseLocalDay(query.from).date;
+ const to = parseLocalDay(query.to).date;
+ to.setDate(to.getDate() + 1);
+ const selectedIds = query.employeeId
+ ? [query.employeeId]
+ : query.employeeIds;
+ const periodEntries: Prisma.TimeEntryWhereInput = {
+ clockIn: { gte: from, lt: to },
+ voidedAt: null,
+ status: { not: 'Rejected' },
+ };
+ const employees = await tx.employee.findMany({
+ where: selectedIds
+ ? { id: { in: selectedIds } }
+ : { timeEntries: { some: periodEntries } },
+ select: {
+ id: true,
+ personalNo: true,
+ firstName: true,
+ lastName: true,
+ _count: {
+ select: {
+ timeEntries: {
+ where: { ...periodEntries, clockOut: null },
+ },
+ },
+ },
+ },
+ orderBy: [{ lastName: 'asc' }, { firstName: 'asc' }, { id: 'asc' }],
+ });
+ if (selectedIds && employees.length !== selectedIds.length) {
+ throw new BadRequestException('One or more employees do not exist');
+ }
+ return {
+ from: report.from,
+ to: report.to,
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ generatedAt: new Date(),
+ locale: query.locale ?? 'de',
+ employees: employees.map(({ _count, ...employee }) => ({
+ ...employee,
+ openEntryCount: _count.timeEntries,
+ rows: report.rows.filter((row) => row.employeeId === employee.id),
+ })),
+ };
+ },
+ { isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead },
+ );
+ return renderWorkingTimePdf(input);
+ }
+
+ private async buildWorkingTimes(
+ query: WorkingTimeReportQueryDto,
+ tx: Prisma.TransactionClient,
+ ): Promise {
+ if (query.employeeId && query.employeeIds) {
+ throw new BadRequestException(
+ 'employeeId and employeeIds cannot be combined',
+ );
+ }
const includeLocations = query.includeLocations === 'true';
const from = parseLocalDay(query.from);
const to = parseLocalDay(query.to);
@@ -303,9 +373,11 @@ export class ReportsService {
to.date.getDate() + 1,
);
- const entries = await this.prisma.timeEntry.findMany({
+ const entries = await tx.timeEntry.findMany({
where: {
- employeeId: query.employeeId,
+ employeeId:
+ query.employeeId ??
+ (query.employeeIds ? { in: query.employeeIds } : undefined),
clockIn: { gte: from.date, lt: toExclusive },
clockOut: { not: null },
status: { not: 'Rejected' },
@@ -315,38 +387,68 @@ export class ReportsService {
include: { employee: { select: { firstName: true, lastName: true } } },
});
- const rows = entries.map((entry) => {
- const summary = summarize(
- entry.clockIn,
- entry.clockOut as Date,
- parseBreakRules(entry.breakRules),
- );
- return {
- id: entry.id,
- employeeId: entry.employeeId,
- employeeName: `${entry.employee.firstName} ${entry.employee.lastName}`,
- date: localDate(entry.clockIn),
- clockIn: entry.clockIn.toISOString(),
- clockOut: (entry.clockOut as Date).toISOString(),
- status: entry.status,
- ...(includeLocations
- ? {
- clockInLocation: reportLocation(
- entry.terminalLocationLabel,
- entry.latitude,
- entry.longitude,
- entry.accuracyMeters,
- ),
- clockOutLocation: reportLocation(
- entry.clockOutTerminalLocationLabel,
- entry.clockOutLatitude,
- entry.clockOutLongitude,
- entry.clockOutAccuracyMeters,
- ),
- }
- : {}),
- ...summary,
- };
+ const groupIds = [
+ ...new Set(
+ entries.flatMap((entry) =>
+ entry.captureGroupId ? [entry.captureGroupId] : [],
+ ),
+ ),
+ ];
+ const siblings = groupIds.length
+ ? await tx.timeEntry.findMany({
+ where: {
+ captureGroupId: { in: groupIds },
+ employeeId: {
+ in: [...new Set(entries.map((entry) => entry.employeeId))],
+ },
+ voidedAt: null,
+ status: { not: 'Rejected' },
+ clockOut: { not: null },
+ },
+ })
+ : [];
+ const summaries = calculateCaptureSummaries([
+ ...new Map(
+ [...entries, ...siblings].map((entry) => [entry.id, entry]),
+ ).values(),
+ ]);
+ const rows = entries.flatMap((entry) => {
+ const summary = summaries.get(entry.id);
+ if (!summary) return [];
+ // Reports retain the precision of their timestamps while keeping the
+ // captured break rules and capture-group allocation used by accounts.
+ const grossMinutes =
+ ((entry.clockOut as Date).getTime() - entry.clockIn.getTime()) / 60_000;
+ return [
+ {
+ id: entry.id,
+ employeeId: entry.employeeId,
+ employeeName: `${entry.employee.firstName} ${entry.employee.lastName}`,
+ date: localDate(entry.clockIn),
+ clockIn: entry.clockIn.toISOString(),
+ clockOut: (entry.clockOut as Date).toISOString(),
+ status: entry.status,
+ ...(includeLocations
+ ? {
+ clockInLocation: reportLocation(
+ entry.terminalLocationLabel,
+ entry.latitude,
+ entry.longitude,
+ entry.accuracyMeters,
+ ),
+ clockOutLocation: reportLocation(
+ entry.clockOutTerminalLocationLabel,
+ entry.clockOutLatitude,
+ entry.clockOutLongitude,
+ entry.clockOutAccuracyMeters,
+ ),
+ }
+ : {}),
+ ...summary,
+ grossMinutes,
+ netMinutes: grossMinutes - summary.breakMinutes,
+ },
+ ];
});
return {
diff --git a/apps/api/src/app/reports/working-time-pdf.ts b/apps/api/src/app/reports/working-time-pdf.ts
new file mode 100644
index 0000000..9319041
--- /dev/null
+++ b/apps/api/src/app/reports/working-time-pdf.ts
@@ -0,0 +1,464 @@
+import { existsSync } from 'node:fs';
+import { join } from 'node:path';
+import PDFDocument from 'pdfkit';
+import type { WorkingTimeReportRowDto } from './reports.dto';
+
+export interface WorkingTimePdfEmployee {
+ id: string;
+ personalNo: string;
+ firstName: string;
+ lastName: string;
+ openEntryCount: number;
+ rows: WorkingTimeReportRowDto[];
+}
+
+export interface WorkingTimePdfInput {
+ from: string;
+ to: string;
+ timeZone: string;
+ generatedAt: Date;
+ locale: 'de' | 'en';
+ employees: WorkingTimePdfEmployee[];
+}
+
+const labels = {
+ de: {
+ title: 'Arbeitszeitnachweis',
+ personalNo: 'Personalnummer',
+ employeeId: 'Mitarbeiter-ID',
+ period: 'Zeitraum',
+ timeZone: 'Zeitzone',
+ generated: 'Erstellt',
+ bookingDay: 'Buchungstag\nnach Beginn',
+ start: 'Beginn\nDatum, Uhrzeit, UTC-Offset',
+ end: 'Ende\nDatum, Uhrzeit, UTC-Offset',
+ gross: 'Brutto\nh:mm:ss',
+ break: 'Pausenabzug\nh:mm:ss',
+ net: 'Netto\nh:mm:ss',
+ status: 'Status',
+ dayTotal: 'Tagessumme',
+ periodTotal: 'Zeitraumsumme',
+ noEntries: 'Keine abgeschlossenen Buchungen im gewählten Zeitraum.',
+ noEmployees: 'Keine Mitarbeiter:innen für diesen Export vorhanden.',
+ openWarning:
+ 'Nicht enthalten: {count} laufende Buchung(en) ohne Ende. Sie fehlen auch in den Summen.',
+ provisionalWarning:
+ 'Vorläufig: {count} abgeschlossene Buchung(en) sind noch nicht genehmigt und in den Summen enthalten.',
+ intervalNote:
+ 'Zuordnung nach dem lokalen Tag des Beginns. Intervalle über Mitternacht bleiben vollständig erhalten; sie werden nicht an den Zeitraumgrenzen gekürzt. Abgelehnte und stornierte Buchungen sind ausgeschlossen.',
+ breakNote:
+ 'Pausenabzüge werden automatisch nach den konfigurierten Regeln berechnet; sie belegen keine tatsächlichen Pausenzeiten. Dauerangaben in h:mm:ss, auf Sekunden gerundet. Pausenabzug = angezeigte Brutto- minus Nettozeit; Summen aus den gerundeten Werten.',
+ pending: 'Ausstehend (vorläufig)',
+ open: 'Offen (vorläufig)',
+ approved: 'Genehmigt',
+ page: 'Seite',
+ of: 'von',
+ },
+ en: {
+ title: 'Working time record',
+ personalNo: 'Personnel number',
+ employeeId: 'Employee ID',
+ period: 'Period',
+ timeZone: 'Time zone',
+ generated: 'Generated',
+ bookingDay: 'Booking day\nby start',
+ start: 'Start\nDate, time, UTC offset',
+ end: 'End\nDate, time, UTC offset',
+ gross: 'Gross\nh:mm:ss',
+ break: 'Break deduction\nh:mm:ss',
+ net: 'Net\nh:mm:ss',
+ status: 'Status',
+ dayTotal: 'Day total',
+ periodTotal: 'Period total',
+ noEntries: 'No closed entries in the selected period.',
+ noEmployees: 'No employees available for this export.',
+ openWarning:
+ 'Excluded: {count} running entry/entries without an end. They are also excluded from totals.',
+ provisionalWarning:
+ 'Provisional: {count} closed entry/entries are not yet approved and are included in totals.',
+ intervalNote:
+ 'Assigned by the local start day. Overnight intervals remain complete and are not clipped at the period boundaries. Rejected and voided entries are excluded.',
+ breakNote:
+ 'Break deductions are calculated automatically from the configured rules; they do not document actual break times. Durations use h:mm:ss, rounded to seconds. Break deduction = displayed gross minus net; totals sum these rounded values.',
+ pending: 'Pending (provisional)',
+ open: 'Open (provisional)',
+ approved: 'Approved',
+ page: 'Page',
+ of: 'of',
+ },
+} as const;
+
+const MARGIN = 32;
+const CELL_PADDING = 6;
+const ROW_HEIGHT = 32;
+const TOTAL_HEIGHT = 23;
+const TEXT_COLOR = '#172033';
+const MUTED_COLOR = '#526076';
+const BORDER_COLOR = '#d5dce5';
+
+function fontPath(fileName: string): string | undefined {
+ return [
+ join(__dirname, 'assets/fonts', fileName),
+ join(__dirname, '../../assets/fonts', fileName),
+ ].find((candidate) => existsSync(candidate));
+}
+
+function seconds(minutes: number): number {
+ return Math.max(0, Math.round(minutes * 60));
+}
+
+function rowAmounts(row: WorkingTimeReportRowDto): [number, number, number] {
+ const gross = seconds(row.grossMinutes);
+ const net = seconds(row.netMinutes);
+ return [gross, gross - net, net];
+}
+
+function duration(totalSeconds: number): string {
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${(totalSeconds % 60)
+ .toString()
+ .padStart(2, '0')}`;
+}
+
+function calendarDate(
+ value: string,
+ locale: WorkingTimePdfInput['locale'],
+): string {
+ const [year, month, day] = value.split('-');
+ return locale === 'de' ? `${day}.${month}.${year}` : value;
+}
+
+function identityText(value: string): string {
+ return value.replace(/\s+/gu, ' ').trim();
+}
+
+function localTimestamp(value: Date, timeZone: string): [string, string] {
+ const parts = new Intl.DateTimeFormat('en-GB', {
+ timeZone,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hourCycle: 'h23',
+ timeZoneName: 'longOffset',
+ }).formatToParts(value);
+ const part = (type: Intl.DateTimeFormatPartTypes) =>
+ parts.find((item) => item.type === type)?.value ?? '';
+ return [
+ `${part('year')}-${part('month')}-${part('day')}`,
+ `${part('hour')}:${part('minute')}:${part('second')} ${part('timeZoneName')}`,
+ ];
+}
+
+/** A snapshot of recorded intervals, without location or project data. */
+export async function renderWorkingTimePdf(
+ input: WorkingTimePdfInput,
+): Promise {
+ const regularFont = fontPath('NotoSans-Regular.ttf');
+ if (!regularFont)
+ throw new Error('The bundled working-time PDF font is missing');
+ const boldFont = fontPath('NotoSans-Bold.ttf') ?? regularFont;
+ const text = labels[input.locale];
+ const doc = new PDFDocument({
+ size: 'A4',
+ layout: 'landscape',
+ margin: MARGIN,
+ autoFirstPage: false,
+ bufferPages: true,
+ font: regularFont,
+ info: {
+ Title: text.title,
+ Author: 'OpenClockwork',
+ Subject: `${input.from} - ${input.to} (${input.timeZone})`,
+ CreationDate: input.generatedAt,
+ ModDate: input.generatedAt,
+ },
+ });
+ const result = new Promise((resolve, reject) => {
+ const chunks: Buffer[] = [];
+ doc.on('data', (chunk: Buffer) => chunks.push(chunk));
+ doc.on('end', () => resolve(Buffer.concat(chunks)));
+ doc.on('error', reject);
+ });
+ doc.registerFont('Regular', regularFont);
+ doc.registerFont('Bold', boldFont);
+
+ const pageWidth = 841.89;
+ const contentWidth = pageWidth - MARGIN * 2;
+ const columns = [
+ { label: text.bookingDay, width: 80, align: 'left' },
+ { label: text.start, width: 157, align: 'left' },
+ { label: text.end, width: 157, align: 'left' },
+ { label: text.gross, width: 73, align: 'right' },
+ { label: text.break, width: 87, align: 'right' },
+ { label: text.net, width: 73, align: 'right' },
+ { label: text.status, width: contentWidth - 627, align: 'left' },
+ ] as const;
+ const period = `${calendarDate(input.from, input.locale)} - ${calendarDate(input.to, input.locale)}`;
+ const generated = localTimestamp(input.generatedAt, input.timeZone);
+ const generatedLabel = `${calendarDate(generated[0], input.locale)} ${generated[1]}`;
+ let y = MARGIN;
+
+ const paragraph = (
+ value: string,
+ options: {
+ size?: number;
+ bold?: boolean;
+ color?: string;
+ gap?: number;
+ } = {},
+ ) => {
+ doc
+ .font(options.bold ? 'Bold' : 'Regular')
+ .fontSize(options.size ?? 8)
+ .fillColor(options.color ?? TEXT_COLOR);
+ const height = doc.heightOfString(value, {
+ width: contentWidth,
+ lineGap: 1,
+ });
+ doc.text(value, MARGIN, y, { width: contentWidth, lineGap: 1 });
+ y += height + (options.gap ?? 4);
+ };
+
+ const drawColumns = () => {
+ const headerHeight = 34;
+ doc.rect(MARGIN, y, contentWidth, headerHeight).fill('#e9eef5');
+ let x = MARGIN;
+ for (const column of columns) {
+ doc
+ .font('Bold')
+ .fontSize(7.5)
+ .fillColor(TEXT_COLOR)
+ .text(column.label, x + CELL_PADDING, y + 5, {
+ width: column.width - CELL_PADDING * 2,
+ align: column.align,
+ lineGap: 1,
+ });
+ x += column.width;
+ }
+ y += headerHeight;
+ };
+
+ const startPage = (employee: WorkingTimePdfEmployee, firstPage: boolean) => {
+ doc.addPage();
+ y = MARGIN;
+ paragraph(`OpenClockwork | ${text.title}`, {
+ size: 16,
+ bold: true,
+ gap: 7,
+ });
+ paragraph(identityText(`${employee.firstName} ${employee.lastName}`), {
+ size: 12,
+ bold: true,
+ });
+ paragraph(
+ `${text.personalNo}: ${identityText(employee.personalNo)} | ${text.employeeId}: ${identityText(employee.id)}`,
+ { size: 8 },
+ );
+ paragraph(
+ `${text.period}: ${period} | ${text.timeZone}: ${input.timeZone}`,
+ { size: 8 },
+ );
+ paragraph(`${text.generated}: ${generatedLabel}`, {
+ size: 7.5,
+ color: MUTED_COLOR,
+ gap: 7,
+ });
+ if (employee.openEntryCount > 0) {
+ paragraph(
+ text.openWarning.replace('{count}', String(employee.openEntryCount)),
+ {
+ bold: true,
+ size: 8,
+ color: '#9a3412',
+ gap: 5,
+ },
+ );
+ }
+ const provisional = employee.rows.filter(
+ (row) => row.status !== 'Approved',
+ ).length;
+ if (provisional > 0) {
+ paragraph(
+ text.provisionalWarning.replace('{count}', String(provisional)),
+ {
+ bold: true,
+ size: 8,
+ color: '#854d0e',
+ gap: 5,
+ },
+ );
+ }
+ if (firstPage) {
+ paragraph(text.intervalNote, { size: 7.5, color: MUTED_COLOR });
+ paragraph(text.breakNote, { size: 7.5, color: MUTED_COLOR, gap: 9 });
+ }
+ drawColumns();
+ };
+
+ const ensureSpace = (height: number, employee: WorkingTimePdfEmployee) => {
+ if (y + height > doc.page.height - MARGIN - 26) startPage(employee, false);
+ };
+
+ const totals = (
+ value: string,
+ rows: WorkingTimeReportRowDto[],
+ periodTotal: boolean,
+ ) => {
+ const amounts = rows.reduce(
+ (sum, row) => {
+ const values = rowAmounts(row);
+ return sum.map((amount, index) => amount + values[index]);
+ },
+ [0, 0, 0],
+ );
+ doc
+ .rect(MARGIN, y, contentWidth, TOTAL_HEIGHT)
+ .fill(periodTotal ? '#dbe6f3' : '#f2f5f9');
+ doc
+ .font('Bold')
+ .fontSize(8)
+ .fillColor(TEXT_COLOR)
+ .text(value, MARGIN + CELL_PADDING, y + 6, {
+ width: 394 - CELL_PADDING * 2,
+ });
+ let x = MARGIN + 394;
+ for (let index = 0; index < amounts.length; index += 1) {
+ const width = columns[index + 3].width;
+ doc.text(duration(amounts[index]), x + CELL_PADDING, y + 6, {
+ width: width - CELL_PADDING * 2,
+ align: 'right',
+ });
+ x += width;
+ }
+ y += TOTAL_HEIGHT;
+ };
+
+ for (const employee of input.employees) {
+ startPage(employee, true);
+ const rows = [...employee.rows].sort(
+ (left, right) =>
+ left.date.localeCompare(right.date) ||
+ left.clockIn.localeCompare(right.clockIn) ||
+ left.id.localeCompare(right.id),
+ );
+ if (rows.length === 0) {
+ y += 10;
+ paragraph(text.noEntries, { size: 9, gap: 10 });
+ }
+ let dayRows: WorkingTimeReportRowDto[] = [];
+ rows.forEach((row, index) => {
+ const lastOfDay = rows[index + 1]?.date !== row.date;
+ const finalTotalsHeight =
+ index === rows.length - 1 ? TOTAL_HEIGHT + 6 : 0;
+ ensureSpace(
+ ROW_HEIGHT + (lastOfDay ? TOTAL_HEIGHT : 0) + finalTotalsHeight,
+ employee,
+ );
+ const start = localTimestamp(new Date(row.clockIn), input.timeZone);
+ const end = localTimestamp(new Date(row.clockOut), input.timeZone);
+ const status =
+ row.status === 'Approved'
+ ? text.approved
+ : row.status === 'Pending'
+ ? text.pending
+ : text.open;
+ const values = [
+ calendarDate(row.date, input.locale),
+ `${calendarDate(start[0], input.locale)}\n${start[1]}`,
+ `${calendarDate(end[0], input.locale)}\n${end[1]}`,
+ ...rowAmounts(row).map(duration),
+ status,
+ ];
+ let x = MARGIN;
+ values.forEach((value, columnIndex) => {
+ const column = columns[columnIndex];
+ doc
+ .font('Regular')
+ .fontSize(8)
+ .fillColor(TEXT_COLOR)
+ .text(value, x + CELL_PADDING, y + 5, {
+ width: column.width - CELL_PADDING * 2,
+ align: column.align,
+ lineGap: 1,
+ });
+ x += column.width;
+ });
+ y += ROW_HEIGHT;
+ doc
+ .moveTo(MARGIN, y)
+ .lineTo(MARGIN + contentWidth, y)
+ .strokeColor(BORDER_COLOR)
+ .lineWidth(0.4)
+ .stroke();
+ dayRows.push(row);
+ if (lastOfDay) {
+ totals(
+ `${text.dayTotal} ${calendarDate(row.date, input.locale)}`,
+ dayRows,
+ false,
+ );
+ dayRows = [];
+ }
+ });
+ ensureSpace(TOTAL_HEIGHT + 6, employee);
+ y += 6;
+ totals(text.periodTotal, rows, true);
+ }
+
+ if (input.employees.length === 0) {
+ doc.addPage();
+ y = MARGIN;
+ paragraph(`OpenClockwork | ${text.title}`, {
+ size: 16,
+ bold: true,
+ gap: 10,
+ });
+ paragraph(
+ `${text.period}: ${period} | ${text.timeZone}: ${input.timeZone}`,
+ );
+ paragraph(`${text.generated}: ${generatedLabel}`);
+ paragraph(text.noEmployees, { size: 10 });
+ }
+
+ const pageRange = doc.bufferedPageRange();
+ for (
+ let index = pageRange.start;
+ index < pageRange.start + pageRange.count;
+ index += 1
+ ) {
+ doc.switchToPage(index);
+ // The reserved footer lies outside the body's bottom margin.
+ doc.page.margins.bottom = 0;
+ const footerY = doc.page.height - MARGIN + 7;
+ doc
+ .moveTo(MARGIN, footerY - 7)
+ .lineTo(MARGIN + contentWidth, footerY - 7)
+ .strokeColor(BORDER_COLOR)
+ .lineWidth(0.5)
+ .stroke();
+ doc
+ .font('Regular')
+ .fontSize(7)
+ .fillColor(MUTED_COLOR)
+ .text('OpenClockwork', MARGIN, footerY, {
+ width: contentWidth / 2,
+ lineBreak: false,
+ });
+ doc.text(
+ `${text.page} ${index + 1} ${text.of} ${pageRange.count}`,
+ MARGIN + contentWidth / 2,
+ footerY,
+ {
+ width: contentWidth / 2,
+ align: 'right',
+ lineBreak: false,
+ },
+ );
+ }
+ doc.end();
+ return result;
+}
diff --git a/apps/api/src/assets/fonts/NotoSans-Bold.ttf b/apps/api/src/assets/fonts/NotoSans-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..713a76885c341ba5b49e0f708d916e2ff176ee43
GIT binary patch
literal 838072
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