Print a checklist without printing the application around it
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 6m6s
CI / Integrationstests (echtes Postgres) (push) Failing after 5m22s

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>
This commit is contained in:
2026-08-18 20:19:58 +02:00
parent 521e4ecf00
commit 19e3170b00
9 changed files with 528 additions and 9 deletions

View File

@@ -1,9 +1,10 @@
"use client";
import { MessageSquarePlus } from "lucide-react";
import { MessageSquarePlus, Printer } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Button } from "@/components/ui/Button";
import { Button, LINK_BUTTON_CLASS } from "@/components/ui/Button";
import { useToast } from "@/components/ui/Toast";
import {
alleReichungen,
@@ -34,6 +35,8 @@ type Props = {
/** Text, solange keine Liste existiert. */
leerText: string;
anlegenLabel: string;
/** Ziel für den Ausdruck — eine eigene Seite, siehe PrintChecklist. */
druckHref: string;
onSpeichern: (
itemKey: string,
teil: { erledigt?: boolean; wert?: string | null; kommentar?: string | null }
@@ -41,7 +44,16 @@ type Props = {
onAnlegen: () => Promise<ActionResult>;
};
export function ChecklistPanel({ gruppen, staende, vorhanden, leerText, anlegenLabel, onSpeichern, onAnlegen }: Props) {
export function ChecklistPanel({
gruppen,
staende,
vorhanden,
leerText,
anlegenLabel,
druckHref,
onSpeichern,
onAnlegen,
}: Props) {
const { showToast } = useToast();
const router = useRouter();
const [laeuft, setLaeuft] = useState<string | null>(null);
@@ -86,6 +98,15 @@ export function ChecklistPanel({ gruppen, staende, vorhanden, leerText, anlegenL
return (
<div className="flex flex-col gap-6">
<div className="flex justify-end">
{/* Neuer Tab: der Ausdruck ist eine eigene Seite, und wer ihn schliesst,
steht wieder in der Akte statt auf einer leeren Historie. */}
<Link href={druckHref} target="_blank" className={LINK_BUTTON_CLASS}>
<Printer className="h-4 w-4" />
Als PDF speichern
</Link>
</div>
<div className="rounded border border-border bg-surface px-3 py-2.5">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<span className="text-sm font-semibold text-ink">

View File

@@ -25,6 +25,7 @@ export function OffboardingTab({
vorhanden={vorhanden}
leerText="Für diese Person gibt es keine Offboarding-Checkliste. Für einen No Show wird keine angelegt — wer nie angetreten ist, hat nichts offzuboarden."
anlegenLabel="Checkliste anlegen"
druckHref={`/employees/${employeeId}/checkliste?art=offboarding`}
onSpeichern={(itemKey, teil) => setOffboardingTask({ employee_id: employeeId, item_key: itemKey, ...teil })}
onAnlegen={() => startOffboarding(employeeId)}
/>

View File

@@ -26,6 +26,7 @@ export function OnboardingTab({
vorhanden={vorhanden}
leerText="Für diese Person gibt es keine Onboarding-Checkliste. Sie entsteht mit einer Einstellung oder Wiedereinstellung — wer davor eingetreten ist, hat keine."
anlegenLabel="Checkliste anlegen"
druckHref={`/employees/${employeeId}/checkliste?art=onboarding`}
onSpeichern={(itemKey, teil) => setOnboardingTask({ employee_id: employeeId, item_key: itemKey, ...teil })}
onAnlegen={() => startOnboarding(employeeId)}
/>