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.
This commit is contained in:
2026-09-15 22:35:47 +02:00
parent 1cbed1a8f5
commit 8d0c9b4b65
15 changed files with 426 additions and 48 deletions

View File

@@ -23,6 +23,9 @@ const TONE: Record<string, { text: string; bar: string }> = {
danger: { text: "text-danger-text", bar: "bg-danger-text" },
warning: { text: "text-warning-text", bar: "bg-warning-text" },
brand: { text: "text-brand-700", bar: "bg-brand-500" },
// Geplante Eintritte: eine Ankündigung, kein Befund. „info" ist der Ton,
// den die Statuschips für „Geplant" ohnehin tragen.
info: { text: "text-info-text", bar: "bg-info-text" },
};
// Die Tabelle, die hier stand, ist nach lib/colors.ts gewandert: Punkt und
@@ -169,13 +172,39 @@ export default async function DashboardPage({
href: `/reports?mode=events&eventType=Austritt&from=${yearStart}&to=${yearEnd}`,
},
{ label: "Langzeitabwesend", value: karenzCount, tone: "warning", href: "/employees?status=Karenz" },
// ── Zwei Kacheln aus dem Workshop (Anforderung 10) ──────────────
//
// „Aktives Dienstverhältnis" ist nicht dasselbe wie „Aktive
// Mitarbeiter:innen" ganz links. Dort steht, wer heute arbeitet; hier,
// mit wem ein Vertrag läuft — Langzeitabwesende eingeschlossen. Genau
// diese Zahl ist die Bezugsgrösse fast jeder Personalkennzahl, und sie
// war bisher nirgends zu sehen: sichtbar waren 806 und 10, addieren
// musste man selbst.
{
label: "Aktives Dienstverhältnis",
value: activeCount + karenzCount,
tone: "default",
href: "/employees?status=Aktiv&status=Karenz",
},
// Geplante Eintritte ohne Zeitgrenze — anders als die Karte „Anstehend"
// darunter, die nur den eingestellten Vorschauzeitraum zeigt. Ein
// Eintritt in vier Monaten ist vereinbart und zählt, auch wenn er in der
// Vorschau noch nicht auftaucht.
{
label: "Geplante Eintritte",
value: staffRows.filter((row) => statusOf(row) === "Geplant").length,
tone: "info",
href: "/employees?status=Geplant",
},
{ label: "Offene Positionen", value: openPositions.length, tone: "brand", href: "/positions" },
];
return (
<div className="flex flex-col gap-6">
{drafts.length > 0 && <DraftsCard drafts={drafts} />}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
{/* Acht Kacheln: vier je Zeile ab der grossen Breite, damit keine
Zeile mit zwei Resten endet. */}
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-4 xl:grid-cols-8">
{kpis.map((kpi) => (
<Link
key={kpi.label}

View File

@@ -6,6 +6,7 @@ import {
parseEventDateParam,
parseEventGroupDimension,
parseEventSplitDimension,
parseEventSubtype,
parseEventType,
parseGroupDimension,
parseIsoDateParam,
@@ -33,6 +34,7 @@ type SearchParams = Record<string, string | string[] | undefined> & {
status?: string;
asOf?: string;
eventType?: string;
eventSubtype?: string;
from?: string;
to?: string;
};
@@ -48,6 +50,7 @@ export default async function ReportsPage({ searchParams }: { searchParams: Prom
const eventGroup = parseEventGroupDimension(params.group);
const eventSplit = parseEventSplitDimension(params.split);
const eventType = parseEventType(params.eventType);
const eventSubtype = parseEventSubtype(params.eventSubtype, eventType);
const from = parseEventDateParam(params.from);
const to = parseEventDateParam(params.to);
const measure = parseMeasure(params.measure);
@@ -71,6 +74,7 @@ export default async function ReportsPage({ searchParams }: { searchParams: Prom
mode === "events"
? loadEventHistory(tx, {
eventType: eventType ?? undefined,
eventSubtype: eventSubtype ?? undefined,
division: params.division,
location: params.location,
from,
@@ -108,6 +112,7 @@ export default async function ReportsPage({ searchParams }: { searchParams: Prom
eventGroup={eventGroup}
eventSplit={eventSplit ?? ""}
eventType={eventType ?? ""}
eventSubtype={eventSubtype ?? ""}
eventFilters={{ division: params.division ?? "", location: params.location ?? "", from: from ?? "", to: to ?? "" }}
rows={rows}
total={sumValues(rows)}