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:
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
import { exportFilename, exportResponseHeaders, toCsv, toXlsx, type ExportColumn } from "@/lib/export";
|
||||
import { EVENT_TYPE_LABELS, parseEventDateParam, parseEventType, type OrgLookups, type ReportEvent } from "@/lib/reports";
|
||||
import {
|
||||
EVENT_TYPE_LABELS,
|
||||
parseEventDateParam,
|
||||
parseEventSubtype,
|
||||
parseEventType,
|
||||
type OrgLookups,
|
||||
type ReportEvent,
|
||||
} from "@/lib/reports";
|
||||
import { loadEventHistory, loadOrgLookups } from "@/lib/reports-data";
|
||||
import { requireHrUser } from "@/lib/auth/require-hr";
|
||||
import { withUser } from "@/lib/db";
|
||||
@@ -22,6 +29,10 @@ export async function GET(request: NextRequest) {
|
||||
loadOrgLookups(tx),
|
||||
loadEventHistory(tx, {
|
||||
eventType: eventType ?? undefined,
|
||||
// Ohne diese Zeile exportierte die Schaltfläche neben der Auswertung
|
||||
// etwas anderes, als auf dem Bildschirm steht — die Einschränkung auf
|
||||
// die Beendigungsart fiele beim Export still weg.
|
||||
eventSubtype: parseEventSubtype(params.get("eventSubtype"), eventType) ?? undefined,
|
||||
division: params.get("division") ?? undefined,
|
||||
location: params.get("location") ?? undefined,
|
||||
from: parseEventDateParam(params.get("from")),
|
||||
@@ -43,6 +54,9 @@ function eventExportColumns(lookups: OrgLookups): ExportColumn<ReportEvent>[] {
|
||||
return [
|
||||
{ header: "Datum", get: (e) => e.event_date, kind: "date" },
|
||||
{ header: "Ereignistyp", get: (e) => EVENT_TYPE_LABELS[e.event_type] ?? e.event_type },
|
||||
// Beendigungsart bzw. Art der Langzeitabwesenheit. Leer bei allen
|
||||
// Ereignissen, die keinen Untertyp kennen — eine Versetzung hat keinen.
|
||||
{ header: "Untertyp", get: (e) => e.subtype ?? "" },
|
||||
{ header: "Vorname", get: (e) => e.first_name },
|
||||
{ header: "Nachname", get: (e) => e.last_name },
|
||||
{ header: "Position", get: (e) => e.job_title },
|
||||
|
||||
Reference in New Issue
Block a user