1) Die Kacheln stehen jetzt in drei Gruppen — Personalstand, Personalbewegung, Recruiting & Vakanzen — in der Reihenfolge aus dem Entwurf des Kunden. Acht Zahlen nebeneinander sind acht Zahlen; sie beantworten aber drei verschiedene Fragen, und ohne Ueberschrift muss man jede Beschriftung einzeln lesen, um das herauszufinden. "Aktives Dienstverhaeltnis" steht vorn: es ist die Bezugsgroesse fast jeder Personalkennzahl. 2) Der Namensfilter in "Anstehend" ist jetzt immer da. Die Schwelle "erst ab neun Eintraegen" war in der Bedienung falsch — das Feld erschien bei 180 Tagen und verschwand bei 30, und ein Bedienelement, das je nach Zeitraum da ist oder nicht, wirkt wie ein Fehler. 3b) Der Filter heisst jetzt "Aktives Dienstverhaeltnis (Aktiv + Langzeitabwesenheit)" — derselbe Name wie die Kachel, die dorthin verlinkt. 6) Die Personalnummer wird gegen die Datenbank geprueft, waehrend sie eingetippt wird, und nennt bei einem Treffer die Person, die sie schon hat. hire_employee weist sie weiterhin ab — das bleibt die verbindliche Pruefung, denn zwischen Frage und Anlegen kann jemand anderes dieselbe Nummer vergeben. Nur kam diese Abweisung bisher nach sechs Schritten Eingabe, und das Feld steht im ersten Schritt. Gemerkt wird dabei die gepruefte *Nummer* samt Ergebnis, nicht ein Ja/Nein: so ist die Sperre eine Ableitung aus dem, was im Feld steht, und es gibt keinen Zustand, dessen Zuruecksetzen man vergessen koennte. Zu 4) geprueft, nichts geaendert: die FTE-Kachel rechnet bereits Summe der Wochenstunden der heute Aktiven durch 38,5. Der Berichtemanager rechnet dieselbe Formel, nur als Summe der Einzelquotienten geschrieben.
123 lines
5.1 KiB
TypeScript
123 lines
5.1 KiB
TypeScript
"use client";
|
||
|
||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||
import { useEffect, useState } from "react";
|
||
import { FILTER_SELECT_CLASS } from "@/components/ui/Field";
|
||
import { SearchInput } from "@/components/ui/SearchInput";
|
||
|
||
type EmployeeFiltersProps = {
|
||
/** Der ganze Baum, in Tiefensuche-Reihenfolge. */
|
||
units: { id: string; name: string; unit_type: string }[];
|
||
depthOf: Map<string, number>;
|
||
locations: { id: string; name: string }[];
|
||
};
|
||
|
||
// Aktiv and Langzeitabwesenheit are separate statuses — somebody on a
|
||
// long-term absence is employed but not active. The combined entry is
|
||
// offered explicitly, named after the two it selects rather than calling
|
||
// the pair "aktiv". The stored value is still 'Karenz' (see lib/absence.ts).
|
||
const STATUS_OPTIONS = [
|
||
{ value: "Aktiv", label: "Aktiv" },
|
||
{ value: "Karenz", label: "Langzeitabwesenheit" },
|
||
// Der Name der Kachel zuerst, die Bestandteile in Klammern dahinter: die
|
||
// Kachel „Aktives Dienstverhältnis" verlinkt genau auf diese Auswahl, und
|
||
// wer ihr folgt, soll oben dieselbe Bezeichnung wiederfinden statt eine
|
||
// zweite für dieselbe Sache.
|
||
{ value: "Aktiv,Karenz", label: "Aktives Dienstverhältnis (Aktiv + Langzeitabwesenheit)" },
|
||
{ value: "Geplant", label: "Geplant" },
|
||
{ value: "Ausgetreten", label: "Ausgetreten" },
|
||
] as const;
|
||
|
||
export function EmployeeFilters({ units, depthOf, locations }: EmployeeFiltersProps) {
|
||
const router = useRouter();
|
||
const pathname = usePathname();
|
||
const searchParams = useSearchParams();
|
||
const [q, setQ] = useState(searchParams.get("q") ?? "");
|
||
|
||
useEffect(() => {
|
||
const handle = setTimeout(() => {
|
||
const current = new URLSearchParams(searchParams.toString());
|
||
if (q) current.set("q", q);
|
||
else current.delete("q");
|
||
current.delete("page");
|
||
const next = current.toString();
|
||
if (next !== searchParams.toString()) router.push(`${pathname}?${next}`);
|
||
}, 300);
|
||
return () => clearTimeout(handle);
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [q]);
|
||
|
||
function updateParam(key: string, value: string) {
|
||
const params = new URLSearchParams(searchParams.toString());
|
||
if (value) params.set(key, value);
|
||
else params.delete(key);
|
||
params.delete("page");
|
||
router.push(`${pathname}?${params.toString()}`);
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<SearchInput label="Mitarbeiter:innen durchsuchen" placeholder="Name, Pers.-Nr., Titel…" value={q} onChange={setQ} />
|
||
{/* aria-label rather than a visible label: the filter bar is a single
|
||
horizontal row, and each select's first option already names it on
|
||
screen. */}
|
||
{/* Der ganze Baum, nicht nur die oberste Ebene: die Auswahl greift
|
||
jeweils auf die Einheit *und alles darunter*, weshalb sich damit
|
||
auch nach einer einzelnen Abteilung oder einem Team filtern lässt.
|
||
Eingerückt statt gruppiert, weil optgroup keine Verschachtelung
|
||
kennt und die Tiefe hier beliebig ist. */}
|
||
<select
|
||
aria-label="Nach Organisationseinheit filtern"
|
||
defaultValue={searchParams.get("division") ?? ""}
|
||
onChange={(e) => updateParam("division", e.target.value)}
|
||
className={FILTER_SELECT_CLASS}
|
||
>
|
||
<option value="">Alle Einheiten</option>
|
||
{units
|
||
.filter((u) => u.unit_type !== "Gesellschaft")
|
||
.map((u) => (
|
||
<option key={u.id} value={u.id}>
|
||
{" ".repeat(Math.max(0, (depthOf.get(u.id) ?? 1) - 1) * 3)}
|
||
{u.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<select
|
||
aria-label="Nach Status filtern"
|
||
defaultValue={searchParams.get("status") ?? ""}
|
||
onChange={(e) => updateParam("status", e.target.value)}
|
||
className={FILTER_SELECT_CLASS}
|
||
>
|
||
<option value="">Alle Status</option>
|
||
{STATUS_OPTIONS.map((s) => (
|
||
<option key={s.value} value={s.value}>
|
||
{s.label}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<select
|
||
aria-label="Nach Standort filtern"
|
||
defaultValue={searchParams.get("location") ?? ""}
|
||
onChange={(e) => updateParam("location", e.target.value)}
|
||
className={FILTER_SELECT_CLASS}
|
||
>
|
||
<option value="">Alle Standorte</option>
|
||
{locations.map((l) => (
|
||
<option key={l.id} value={l.id}>
|
||
{l.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
{/* Sortiert wird nicht hier, sondern an den Spaltenköpfen der Liste.
|
||
Diese Leiste schränkt ein, *was* zu sehen ist; die Reihenfolge
|
||
gehört an die Spalte, die sie bestimmt. */}
|
||
{/* Der Dienstwagen stand hier einmal als eigenes Auswahlfeld. Er ist
|
||
jetzt eines von rund zwanzig Kriterien unter Berichte, zusammen mit
|
||
Vertragsart, Kollektivvertrag, Eintrittszeitraum und dem Rest —
|
||
dort lässt sich die Auswahl auch exportieren, was der eigentliche
|
||
Zweck der Frage war. In dieser Leiste, die vor allem zum Suchen da
|
||
ist, wäre er ein Sonderfall unter vielen gleichrangigen. */}
|
||
</div>
|
||
);
|
||
}
|