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.
196 lines
8.6 KiB
TypeScript
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 „Als PDF speichern“ wählen. Hintergrundgrafiken müssen dort eingeschaltet sein, sonst
|
|
fehlen die Flächen hinter den Gruppentiteln — in Chrome unter „Weitere Einstellungen“.
|
|
</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>
|
|
);
|
|
}
|