In den Berichten hing jedes Datumsfeld unmittelbar an der Adresszeile: eine Aenderung hiess router.push, also neu laden und neu rendern. Ein <input type="date"> meldet beim Tippen der Jahreszahl aber viermal -- 0002, 0020, 0202, 2026 -- und die ersten drei loesten je eine Navigation aus, die das Feld auf den Stand aus der Adresse zuruecksetzte. Mitten im Tippen. Mit dem Kalender ging es, weil der in einem Zug ein fertiges Datum setzt; das war der Hinweis darauf, wo es klemmt. DateField haelt den Tippstand jetzt bei sich und meldet nur, was eine Aussage ist: ein vollstaendiges Datum mit vierstelliger Jahreszahl ab 1000, oder das Leeren des Feldes. Beim Verlassen wird nachgereicht, was liegengeblieben ist. Der Stichtag im Organigramm ist ein blankes input und benutzt dieselbe Regel ueber istMeldbaresDatum -- zwei Fassungen davon liefen hier erfahrungsgemaess auseinander. Der Abgleich mit dem Wert von aussen laeuft waehrend des Renderns, nicht in einem Effekt: der liefe erst nach dem Zeichnen, das Feld zeigte also fuer einen Bildaufbau den alten Stand -- und die Regel gegen setState im Effekt verbietet ihn aus genau diesem Grund. Die Felder in den Panels bleiben, wie sie sind: dort ist der Zustand lokal, es gibt keine Navigation und damit auch kein Zuruecksetzen.
100 lines
4.0 KiB
TypeScript
100 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { CalendarClock } from "lucide-react";
|
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
import { useState } from "react";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { FILTER_SELECT_CLASS, istMeldbaresDatum } from "@/components/ui/Field";
|
|
import { fmtDate } from "@/lib/format";
|
|
|
|
type AsOfPickerProps = {
|
|
asOf: string;
|
|
today: string;
|
|
/** Placements projected from effective-dated changes not yet applied. */
|
|
projectedCount: number;
|
|
/** Earliest date the assignment history covers; before that it is today's placement. */
|
|
historyStartsAt: string | null;
|
|
};
|
|
|
|
export function AsOfPicker({ asOf, today, projectedCount, historyStartsAt }: AsOfPickerProps) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
|
|
function setAsOf(value: string | undefined) {
|
|
const sp = new URLSearchParams(searchParams.toString());
|
|
if (value && value !== today) sp.set("asOf", value);
|
|
else sp.delete("asOf");
|
|
router.push(sp.size > 0 ? `${pathname}?${sp}` : pathname, { scroll: false });
|
|
}
|
|
|
|
// Angeglichen während des Renderns, nicht in einem Effekt — siehe DateField.
|
|
const [entwurf, setEntwurf] = useState(asOf);
|
|
const [zuletzt, setZuletzt] = useState(asOf);
|
|
if (asOf !== zuletzt) {
|
|
setZuletzt(asOf);
|
|
setEntwurf(asOf);
|
|
}
|
|
|
|
const isToday = asOf === today;
|
|
const isFuture = asOf > today;
|
|
// Assignments only started being recorded when the history table was
|
|
// introduced; asking for a date before that yields today's placement for
|
|
// everyone, which is worth saying out loud rather than quietly implying.
|
|
const beforeHistory = historyStartsAt !== null && asOf < historyStartsAt;
|
|
|
|
return (
|
|
<div className="rounded border border-border bg-white p-3">
|
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
<label htmlFor="orgchart-asof" className="flex items-center gap-1.5 text-sm font-semibold text-ink">
|
|
<CalendarClock className="h-4 w-4 text-ink-muted" />
|
|
Stichtag
|
|
</label>
|
|
{/* Der Tippstand bleibt hier, gemeldet wird nur ein vollständiges
|
|
Datum: jede Änderung ist eine Navigation, und beim Tippen der
|
|
Jahreszahl entstehen unterwegs 0002, 0020 und 0202 — jede davon
|
|
lud die Seite neu und setzte das Feld mitten im Tippen zurück.
|
|
Dieselbe Regel wie in DateField, dort steht auch der lange Grund. */}
|
|
<input
|
|
id="orgchart-asof"
|
|
type="date"
|
|
value={entwurf}
|
|
onChange={(e) => {
|
|
const wert = e.target.value;
|
|
setEntwurf(wert);
|
|
if (istMeldbaresDatum(wert) && wert !== asOf) setAsOf(wert || undefined);
|
|
}}
|
|
onBlur={() => {
|
|
if (entwurf !== asOf && istMeldbaresDatum(entwurf)) setAsOf(entwurf || undefined);
|
|
}}
|
|
className={FILTER_SELECT_CLASS}
|
|
/>
|
|
{!isToday && (
|
|
<Button variant="ghost" size="sm" onClick={() => setAsOf(undefined)} className="!px-1 text-brand-700 hover:!bg-transparent hover:underline">
|
|
Heute
|
|
</Button>
|
|
)}
|
|
<span className="text-xs text-ink-muted">
|
|
{isToday ? "Aktuelle Organisationsstruktur." : `Struktur zum ${fmtDate(asOf)}.`}
|
|
</span>
|
|
</div>
|
|
|
|
{isFuture && (
|
|
<p className="mt-2 rounded bg-info-bg px-3 py-2 text-xs text-info-text">
|
|
Vorschau: {projectedCount > 0
|
|
? `${projectedCount} geplante Versetzung(en)/Reorganisation(en) sind eingerechnet.`
|
|
: "Für diesen Zeitraum sind keine Versetzungen vorgemerkt."}{" "}
|
|
Ein-/Austritte und Karenzen sind berücksichtigt.
|
|
</p>
|
|
)}
|
|
|
|
{beforeHistory && (
|
|
<p className="mt-2 rounded bg-warning-bg px-3 py-2 text-xs text-warning-text">
|
|
Vor dem {fmtDate(historyStartsAt)} wurde die Zuordnungshistorie noch nicht aufgezeichnet. Wer beschäftigt war,
|
|
stimmt; Team und Vorgesetzte zeigen für diesen Stichtag die heutige Zuordnung.
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|