Files
alpenwerk-hr/lib/checklist-print-data.ts
Maximilian Stubhan 19e3170b00
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 6m6s
CI / Integrationstests (echtes Postgres) (push) Failing after 5m22s
Print a checklist without printing the application around it
The checklist gets a print view: one A4 page, header carrying personnel
number, name, date of extract, entry date and position, the items in two
flowing columns so twenty-five fit, and two signature lines at the foot.
It prints the *state*, dated — not a blank form.

The default filename in the save dialog comes from the document title,
which is set to the agreed convention:

  20260818_2884_Aigner-Manuel_Onboarding-Checklist

Surname first, like everywhere else in the application, so a folder of
these sorts by person and within a person by date. Umlauts are resolved
rather than stripped: the naive route (NFKD, then every non-ASCII to a
dash) turns "Müller" into "Mu-ller", because decomposition splits the
umlaut and the diaeresis becomes the dash. "Weiß" needs its own rule —
it has no decomposition and would otherwise vanish.

The reported defect: the printout carried the application's own top bar
— hamburger, bell, "Neueinstellung", sign-out. Those are controls; on
paper they are decoration, and on a checklist filed in a personnel
record, misleading. The rule now sits on AppShell rather than on this
one page, so the org-chart print view — which had the same problem —
gets it too, along with anything printed later. The shell's padding goes
with it: the type area is set by @page on the print page itself, and the
shell's would have been added on top.

The browser's own header line (date, title, URL) is separate — that is a
checkbox in the print dialog, not something CSS can reach.

440 tests pass, including 13 new ones pinning the filename convention.
Not yet seen in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:19:58 +02:00

80 lines
2.8 KiB
TypeScript

import type { AufgabenStand } from "./checklist";
import type { ChecklistArt } from "./checklist-print";
import type { Tx } from "./db";
import { jsonArrayFrom, jsonObjectFrom, zeitstempel } from "./db/json";
import { todayIso } from "./format";
import { besetzungenAbfrage, orgAsOf, pickPlacements } from "./placement";
// Was der Ausdruck einer Checkliste braucht — und nur das.
//
// Nicht loadEmployeeDetail: das holt offene Planstellen, Berichtslinien, den
// Organisationsbaum und die Historie, wovon der Ausdruck nichts zeigt. Eine
// eigene, schmale Abfrage in einer Rundreise (lib/db/json.ts).
export type ChecklistKopf = {
personnel_number: number;
first_name: string;
last_name: string;
entry_date: string;
/** Die Bezeichnung der Person; kann von der Planstelle abweichen. */
job_title: string;
/** Aus der laufenden Besetzung — bei einer Person ohne Planstelle leer. */
position_number: string | null;
position_title: string | null;
};
export type ChecklistDruckDaten = {
kopf: ChecklistKopf;
staende: AufgabenStand[];
};
export async function loadChecklistPrint(
tx: Tx,
employeeId: string,
art: ChecklistArt,
today: string = todayIso()
): Promise<ChecklistDruckDaten | null> {
const tabelle = art === "offboarding" ? "offboarding_tasks" : "onboarding_tasks";
const gelesen = await tx
.selectNoFrom((eb) => [
jsonObjectFrom(
eb
.selectFrom("employees")
.select(["personnel_number", "first_name", "last_name", "entry_date", "exit_date", "job_title"])
.where("id", "=", employeeId)
).as("person"),
jsonArrayFrom(besetzungenAbfrage(eb, [employeeId])).as("placementRows"),
jsonArrayFrom(
eb
.selectFrom(tabelle)
.select(["item_key", "erledigt", "wert", "kommentar", "updated_by_name"])
.select((x) => zeitstempel(x.ref("updated_at")).as("updated_at"))
.where("employee_id", "=", employeeId)
).as("staende"),
])
.executeTakeFirstOrThrow();
const person = gelesen.person;
if (!person) return null;
// Derselbe Stichtag wie in der Akte: bei einer ausgetretenen Person ist die
// Besetzung am Austrittstag beendet, und zu heute gefragt käme keine heraus
// — der Kopf stünde dann ohne Planstelle da, obwohl es eine gab.
const asOf = orgAsOf(person, today);
const placement = pickPlacements(gelesen.placementRows as never, asOf).get(employeeId) ?? null;
return {
kopf: {
personnel_number: person.personnel_number,
first_name: person.first_name,
last_name: person.last_name,
entry_date: person.entry_date,
job_title: person.job_title,
position_number: placement?.positionNumber ?? null,
position_title: placement?.jobTitle ?? null,
},
staende: gelesen.staende as AufgabenStand[],
};
}