Files
alpenwerk-hr/components/employees/PrintChecklist.tsx
Andrei Laas 8d0c9b4b65 Workshop-Anforderungen, erster Teil: was ohne Migration geht
Anforderung 1 — Freiwilliger vs unfreiwilliger Austritt. Die Liste der
Beendigungsarten zieht aus TerminatePanel.tsx nach lib/beendigung.ts um: der
Berichtemanager braucht sie ebenso, und zwei Listen liefen auseinander. Zwei
neue Arten (Beendigung in der Probezeit, je Seite). Auf wessen Betreiben
beendet wurde, wird aus der Art **abgeleitet** und nicht daneben gespeichert
— als zweites freies Feld liesse sich "Entlassung, freiwillig" erfassen. Das
Dropdown im Formular schraenkt die Auswahl darunter ein.

Drei Gruppen statt zwei: Befristungsablauf geschieht auf niemandes
Betreiben, ein Nichtantritt ist kein Austritt. Beide einer Seite
zuzuschlagen wuerde jede Fluktuationsquote verfaelschen.

Anforderung 2 — Namensfilter in "Anstehend", ab neun Eintraegen.

Anforderung 3 — die zwei Unterschriftenfelder im gedruckten Blatt sind weg;
"Firmenfahrzeug" steht in beiden Checklisten. has_dienstwagen sagt, ob eines
zusteht, nicht ob es uebergeben wurde.

Anforderung 4 — "+794 weitere" ist ein Knopf geworden; die Namen waren
vorher nur ueber den Export erreichbar. Stammdatenaenderung und
Gehaltsanpassung stehen nicht mehr zur Auswahl: die eine entsteht bei jeder
geaenderten Telefonnummer, die andere ist ein totes Ereignis, seit das
Gehalt in Loga liegt. Neu ist der Untertyp — Beendigungsart beim Austritt,
Art der Abwesenheit bei der Langzeitabwesenheit, im Bericht und im Export.

Anforderung 10 — zwei Kacheln. "Aktives Dienstverhaeltnis" ist nicht
dasselbe wie "Aktive Mitarbeiter:innen": dort steht, wer heute arbeitet,
hier, mit wem ein Vertrag laeuft. Sichtbar waren 806 und 10, addieren musste
man selbst.
2026-09-15 22:35:47 +02:00

196 lines
8.6 KiB
TypeScript

"use client";
import { Printer } from "lucide-react";
import { useEffect } from "react";
import { Button } from "@/components/ui/Button";
import {
alleReichungen,
fortschrittVon,
istErledigt,
type AufgabenGruppe,
type AufgabenStand,
} from "@/lib/checklist";
import { artTitel, checklistDateiname, type ChecklistArt } from "@/lib/checklist-print";
import type { ChecklistKopf } from "@/lib/checklist-print-data";
import { fmtDate, fmtName } from "@/lib/format";
// Der Ausdruck einer Checkliste — eine Seite, zum Abheften oder Mitnehmen.
//
// Warum überhaupt Papier, wenn die Liste in der Anwendung steht: sie wird
// besprochen. Beim Eintrittsgespräch liegt sie auf dem Tisch, beim Austritt
// geht sie an die Lohnverrechnung, und in einer Personalakte auf Papier fehlt
// sie sonst. Gedruckt wird deshalb der **Stand**, mit Datum des Auszugs — kein
// Formular zum Ausfüllen.
//
// Erzeugt wird kein PDF im Server: gedruckt wird über den Browser, wie beim
// Organigramm. Der Grund ist derselbe — eine PDF-Bibliothek müsste Schriften,
// Umbrüche und Seitenränder selbst nachbauen, und das Ergebnis wäre ein
// zweites Aussehen neben dem der Anwendung. Der Dateiname im Speichern-Dialog
// kommt aus dem Dokumenttitel; den setzt diese Seite auf die Konvention.
const GLYPH_AN = "☒";
const GLYPH_AUS = "☐";
export function PrintChecklist({
art,
kopf,
gruppen,
staende,
today,
}: {
art: ChecklistArt;
kopf: ChecklistKopf;
gruppen: readonly AufgabenGruppe[];
staende: AufgabenStand[];
/** Vom Server: das Auszugsdatum gehört nicht an die Uhr des Browsers. */
today: string;
}) {
const dateiname = checklistDateiname(art, kopf, today);
// Der Titel ist der Vorschlag im Druckdialog. Ohne ihn hiesse die Datei nach
// der Adresse der Seite, also etwa „employees-…-checkliste".
useEffect(() => {
const vorher = document.title;
document.title = dateiname;
return () => {
document.title = vorher;
};
}, [dateiname]);
const punkte = alleReichungen(gruppen);
const karte = new Map(staende.map((s) => [s.item_key, s]));
const stand = fortschrittVon(punkte, staende);
return (
<div className="flex flex-col gap-5">
<style>{`
@page { size: A4 portrait; margin: 14mm 14mm 12mm; }
@media print {
html, body { background: #fff; }
.nur-bildschirm { display: none !important; }
.blatt {
box-shadow: none !important; border: 0 !important; border-radius: 0 !important;
margin: 0 !important; padding: 0 !important; width: auto !important; min-height: 0 !important;
}
/* Eine Gruppe soll nicht mitten in der Überschrift umbrechen, und
eine Zeile nicht zwischen Haken und Kommentar. */
.gruppe { break-inside: avoid; }
.punkt { break-inside: avoid; }
}
/* Zweispaltig, damit 25 Punkte auf ein Blatt gehen. Als Spaltensatz und
nicht als Gitter: die Punkte fliessen dann von der linken in die
rechte Spalte, statt dass eine Spalte halb leer bleibt. */
.spalten { column-count: 2; column-gap: 10mm; }
`}</style>
<div className="nur-bildschirm flex flex-wrap items-center justify-between gap-3">
<p className="text-sm text-ink-muted">
So kommt es aus dem Drucker. Vorgeschlagener Dateiname:{" "}
<span className="font-semibold text-ink">{dateiname}.pdf</span>
</p>
<Button onClick={() => window.print()}>
<Printer className="h-4 w-4" />
Als PDF speichern
</Button>
</div>
<p className="nur-bildschirm max-w-prose text-xs leading-relaxed text-ink-muted">
Im Druckdialog &bdquo;Als PDF speichern&ldquo; wählen. Hintergrundgrafiken müssen dort eingeschaltet sein, sonst
fehlen die Flächen hinter den Gruppentiteln &mdash; in Chrome unter &bdquo;Weitere Einstellungen&ldquo;.
</p>
<div className="blatt mx-auto w-[210mm] rounded border border-border bg-white p-[14mm] shadow-[var(--shadow-card)]">
{/* ── Kopf ───────────────────────────────────────────── */}
<div className="flex items-start justify-between gap-6 border-b-2 border-ink pb-2">
<div>
<h1 className="text-[15pt] font-extrabold leading-tight text-ink">{artTitel(art)}</h1>
<p className="text-[9pt] text-ink-muted">Alpenwerk Industrie GmbH</p>
</div>
<p className="text-right text-[8pt] leading-snug text-ink-muted">
Auszug vom
<span className="block text-[10pt] font-semibold tabular-nums text-ink">{fmtDate(today)}</span>
</p>
</div>
<dl className="mt-3 grid grid-cols-[auto_1fr_auto_1fr] gap-x-4 gap-y-1 text-[9pt]">
<dt className="font-semibold text-ink-muted">Pers.-Nr.</dt>
<dd className="tabular-nums text-ink">{kopf.personnel_number}</dd>
<dt className="font-semibold text-ink-muted">Eintritt</dt>
<dd className="tabular-nums text-ink">{fmtDate(kopf.entry_date)}</dd>
<dt className="font-semibold text-ink-muted">Name</dt>
<dd className="font-semibold text-ink">{fmtName(kopf.first_name, kopf.last_name)}</dd>
<dt className="font-semibold text-ink-muted">Position</dt>
<dd className="text-ink">
{kopf.position_title ?? kopf.job_title}
{kopf.position_number && <span className="text-ink-muted"> · Planstelle {kopf.position_number}</span>}
</dd>
</dl>
{/* ── Fortschritt ────────────────────────────────────── */}
<div className="mt-3 flex items-baseline justify-between border-t border-border pt-2 text-[9pt]">
<span className="font-semibold text-ink">
{stand.erledigt} von {stand.gesamt} erledigt
</span>
{stand.offen.length > 0 && (
<span className="text-ink-muted">
{stand.offen.length} offen
</span>
)}
</div>
{/* ── Die Punkte ─────────────────────────────────────── */}
<div className="spalten mt-3">
{gruppen.map((gruppe) => (
<div key={gruppe.key} className="gruppe mb-3">
<h2 className="mb-1 bg-surface px-1.5 py-0.5 text-[8pt] font-bold uppercase tracking-wide text-ink-body">
{gruppe.label}
</h2>
<ul>
{gruppe.punkte.map((punkt) => {
const s = karte.get(punkt.key);
const fertig = istErledigt(punkt, s);
return (
<li key={punkt.key} className="punkt flex gap-1.5 py-[2pt] text-[9pt] leading-snug">
<span aria-hidden className="shrink-0 text-[10pt] leading-none text-ink">
{fertig ? GLYPH_AN : GLYPH_AUS}
</span>
<span className="min-w-0">
<span className={fertig ? "text-ink" : "text-ink-body"}>{punkt.label}</span>
{/* Bei Ja/Nein und Text steht der Wert daneben — ein
Haken allein sagte dort nicht, was erfasst wurde. */}
{punkt.art !== "haken" && (
<span className="font-semibold text-ink">
{": "}
{s?.wert ?? "—"}
</span>
)}
{s?.kommentar && (
<span className="block text-[8pt] italic text-ink-body">{s.kommentar}</span>
)}
{s?.updated_by_name && fertig && (
<span className="block text-[7pt] text-ink-muted">
{s.updated_by_name}
{s.updated_at ? ` · ${fmtDate(s.updated_at)}` : ""}
</span>
)}
</span>
</li>
);
})}
</ul>
</div>
))}
</div>
{/* Hier standen zwei Unterschriftenfelder — „Datum, Unterschrift
Mitarbeiter:in" und „… Personalabteilung". Der Kunde hat sie
gestrichen (Anforderung 3 aus dem Workshop): das Blatt ist eine
Arbeitsliste für die Personalabteilung und kein Dokument, das
jemand gegenzeichnet. Wer eine Aufgabe abgehakt hat, steht ohnehin
an der Aufgabe selbst. */}
</div>
</div>
);
}