Vorgemerkte Planstellen, Stichtag fuer Einheiten, Kuendigungsschutz und Elternteilzeit
F.24 -- eine Planstelle, auf die eine Versetzung vorgemerkt ist, galt als frei. Die Vormerkung steht in pending_org_changes, "besetzt?" wurde allein an position_assignments gefragt. Auffallen wuerde es erst in der Nacht des Wirksamkeitstages: dort legt der Nachtlauf die zweite Besetzung an und laeuft in den Teilindex, der genau eine laufende Besetzung je Planstelle zulaesst. Er arbeitet in einer Transaktion ueber alle faelligen Vorgaenge -- eine einzige solche Buchung haette ihn vollstaendig zum Stehen gebracht, auch fuer alle anderen. Neue Auskunft planstelle_vorgemerkt, abgefragt in hire_employee, rehire_employee, transfer_employee und promote_employee, mit Datum in der Meldung. Die eigene Vormerkung zaehlt nicht als Hindernis. D.06 -- die Struktursicht zeigte Einheiten, die es am gewaehlten Stichtag noch nicht gab: org_units wurde ungefiltert gelesen, waehrend Planstellen und Besetzungen daneben auf den Stichtag eingeschraenkt waren. Die Druckansicht filterte schon immer richtig; jetzt tun es alle drei Stellen. H.09 -- der Kuendigungsschutz stand in der Akte nur als "bis TT.MM.JJJJ", obwohl Personenkreis, Beginn, Ende und die ganze Begunstigung erfasst werden. Der Personenkreis ist die eigentliche Auskunft. Die Behinderung bekommt eine eigene Zeile: sie fuehrt oft zu Kuendigungsschutz, ist aber ein eigener Bescheid. Elternteilzeit und Wiedereingliederungsteilzeit sind jetzt auch bei der Stundenaenderung waehlbar. Sie standen nur bei der Rueckkehr aus einer Abwesenheit, mit der Begruendung, dass sie typischerweise dann beginnen -- typischerweise ist aber nicht immer. Die Ueberschneidung der beiden Listen ist damit gewollt; der Test prueft nicht mehr auf Partition, sondern darauf, dass keine Teilzeit an keinem der beiden Wege haengt.
This commit is contained in:
@@ -27,6 +27,10 @@ export default async function OrgChartPage({ searchParams }: { searchParams: Pro
|
|||||||
// valid_from/valid_to nur hier: die Angaben zur Einheit zeigen sie,
|
// valid_from/valid_to nur hier: die Angaben zur Einheit zeigen sie,
|
||||||
// der Druck braucht sie nicht.
|
// der Druck braucht sie nicht.
|
||||||
.select(["id", "org_number", "name", "parent_id", "unit_type", "valid_from", "valid_to"])
|
.select(["id", "org_number", "name", "parent_id", "unit_type", "valid_from", "valid_to"])
|
||||||
|
// Zum Stichtag — sonst zeigt die Struktursicht zum 01.01.2020
|
||||||
|
// Einheiten, die es damals nicht gab (D.06).
|
||||||
|
.where("valid_from", "<=", asOf)
|
||||||
|
.where((eb) => eb.or([eb("valid_to", "is", null), eb("valid_to", ">", asOf)]))
|
||||||
.orderBy("org_number")
|
.orderBy("org_number")
|
||||||
.execute(),
|
.execute(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -5,6 +5,45 @@ import type { Database } from "@/lib/types";
|
|||||||
|
|
||||||
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
type EmployeeRow = Database["public"]["Tables"]["employees"]["Row"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der Kündigungsschutz mit Grund und Zeitraum.
|
||||||
|
*
|
||||||
|
* Hier stand nur „bis TT.MM.JJJJ". Erfasst werden aber Personenkreis, Beginn
|
||||||
|
* und Ende — und der Personenkreis ist die eigentliche Auskunft: „bis 2030"
|
||||||
|
* sagt nicht, warum. Gemeldet am 29.09. von Lara und im Testprotokoll (H.09).
|
||||||
|
*
|
||||||
|
* Die Wortwahl für den einfachsten Fall bleibt: steht nur das Kennzeichen da,
|
||||||
|
* heisst es weiter „Ja (Ende offen)".
|
||||||
|
*/
|
||||||
|
function kuendigungsschutzText(e: EmployeeRow): string {
|
||||||
|
if (!e.has_kuendigungsschutz) return "Nein";
|
||||||
|
const teile = [
|
||||||
|
e.kuendigungsschutz_grund,
|
||||||
|
e.kuendigungsschutz_ab ? `ab ${fmtDate(e.kuendigungsschutz_ab)}` : null,
|
||||||
|
e.kuendigungsschutz_bis ? `bis ${fmtDate(e.kuendigungsschutz_bis)}` : null,
|
||||||
|
].filter((t): t is string => Boolean(t));
|
||||||
|
|
||||||
|
if (teile.length === 0) return "Ja (Ende offen)";
|
||||||
|
return e.kuendigungsschutz_bis ? teile.join(" · ") : `${teile.join(" · ")} · Ende offen`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Begünstigung als eigene Zeile, nicht an den Kündigungsschutz gehängt.
|
||||||
|
*
|
||||||
|
* Sie führt zwar oft zu besonderem Kündigungsschutz, ist aber ein eigener
|
||||||
|
* Bescheid — und der Grad gehört zur Behinderung, nicht zum Schutz.
|
||||||
|
*/
|
||||||
|
function behinderungText(e: EmployeeRow): string {
|
||||||
|
if (!e.ist_beguenstigt_behindert) return "Nein";
|
||||||
|
const teile = [
|
||||||
|
e.behinderung_grad !== null ? `${e.behinderung_grad} %` : null,
|
||||||
|
e.behinderung_ab ? `Bescheid ab ${fmtDate(e.behinderung_ab)}` : null,
|
||||||
|
e.behinderung_bis ? `bis ${fmtDate(e.behinderung_bis)}` : null,
|
||||||
|
].filter((t): t is string => Boolean(t));
|
||||||
|
|
||||||
|
return teile.length > 0 ? teile.join(" · ") : "Ja";
|
||||||
|
}
|
||||||
|
|
||||||
export function VertragTab({ employee }: { employee: EmployeeRow }) {
|
export function VertragTab({ employee }: { employee: EmployeeRow }) {
|
||||||
// Früher stand hier eine einzige Zeile „Merkmale" mit allem, was zutraf,
|
// Früher stand hier eine einzige Zeile „Merkmale" mit allem, was zutraf,
|
||||||
// durch Kommas getrennt — und ein Gedankenstrich, wenn nichts zutraf. Damit
|
// durch Kommas getrennt — und ein Gedankenstrich, wenn nichts zutraf. Damit
|
||||||
@@ -39,14 +78,17 @@ export function VertragTab({ employee }: { employee: EmployeeRow }) {
|
|||||||
: `${employee.teilzeit_art} (Ende offen)`
|
: `${employee.teilzeit_art} (Ende offen)`
|
||||||
: "–",
|
: "–",
|
||||||
],
|
],
|
||||||
[
|
// Grund und Zeitraum stehen dabei, nicht nur das Enddatum. Gemeldet am
|
||||||
"Besonderer Kündigungsschutz",
|
// 29.09.: erfasst werden sieben Felder — Personenkreis, Beginn, Ende,
|
||||||
employee.has_kuendigungsschutz
|
// Behinderung mit Grad und Bescheidzeitraum —, sichtbar war in der Akte
|
||||||
? employee.kuendigungsschutz_bis
|
// nur „bis TT.MM.JJJJ". Wer den Grund brauchte, fand ihn nur in der
|
||||||
? `bis ${fmtDate(employee.kuendigungsschutz_bis)}`
|
// Historie oder über einen Bericht.
|
||||||
: "Ja (Ende offen)"
|
["Besonderer Kündigungsschutz", kuendigungsschutzText(employee)],
|
||||||
: "Nein",
|
// Eigene Zeile und nicht an den Kündigungsschutz gehängt: die Begünstigung
|
||||||
],
|
// ist ein Bescheid für sich. Sie führt zwar oft zu besonderem
|
||||||
|
// Kündigungsschutz, ist aber nicht dasselbe, und der Grad gehört zur
|
||||||
|
// Behinderung, nicht zum Schutz.
|
||||||
|
["Begünstigt behindert", behinderungText(employee)],
|
||||||
];
|
];
|
||||||
if (employee.exit_date) rows.push(["Austrittsdatum", fmtDate(employee.exit_date)]);
|
if (employee.exit_date) rows.push(["Austrittsdatum", fmtDate(employee.exit_date)]);
|
||||||
|
|
||||||
|
|||||||
240
db/migrations/20260929200000_vorgemerkte_planstelle.sql
Normal file
240
db/migrations/20260929200000_vorgemerkte_planstelle.sql
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
-- Eine Planstelle, auf die jemand vorgemerkt ist, ist nicht frei
|
||||||
|
--
|
||||||
|
-- Gemeldet im Test vom 29.09. (F.24): eine Person wurde auf eine Planstelle
|
||||||
|
-- eingestellt, auf die bereits eine Versetzung zum 20.10. vorgemerkt war. Die
|
||||||
|
-- Planstellenliste führte die Stelle als unbesetzt, der Assistent bot sie an,
|
||||||
|
-- und das Organigramm zum 20.10. zeigte sie zweifach besetzt.
|
||||||
|
--
|
||||||
|
-- ═══ Warum das schlimmer ist, als es aussieht ═════════════════════
|
||||||
|
--
|
||||||
|
-- Eine vorgemerkte Versetzung steht in pending_org_changes und noch nicht in
|
||||||
|
-- position_assignments. „Besetzt?" wird aber allein an position_assignments
|
||||||
|
-- gefragt — die Stelle ist zum Zeitpunkt der Buchung tatsächlich frei.
|
||||||
|
--
|
||||||
|
-- Auffallen würde es erst in der Nacht zum 20.10.: dann legt der Nachtlauf die
|
||||||
|
-- Besetzung der vorgemerkten Person an und läuft in
|
||||||
|
-- `position_assignments_one_holder`, den Teilindex, der genau eine laufende
|
||||||
|
-- Besetzung je Planstelle zulässt. Der Nachtlauf arbeitet in **einer**
|
||||||
|
-- Transaktion über alle fälligen Vorgänge; eine einzige solche Buchung bringt
|
||||||
|
-- ihn also vollständig zum Stehen — auch für alle anderen. Der Fehler entsteht
|
||||||
|
-- heute und schlägt Wochen später bei jemand anderem zu.
|
||||||
|
--
|
||||||
|
-- ═══ Was hier steht ═══════════════════════════════════════════════
|
||||||
|
--
|
||||||
|
-- Eine Auskunft, die pending_org_changes mitliest, und ihr Aufruf in den vier
|
||||||
|
-- Funktionen, die jemanden auf eine Planstelle setzen: hire_employee,
|
||||||
|
-- rehire_employee, transfer_employee, promote_employee.
|
||||||
|
--
|
||||||
|
-- Abgewiesen wird mit Datum — „ab dem 20.10.2026 vorgemerkt" sagt der Person
|
||||||
|
-- vor dem Bildschirm, was zu tun ist. Die eigene Vormerkung zählt nicht als
|
||||||
|
-- Hindernis: wer dieselbe Versetzung noch einmal erfasst, korrigiert sie.
|
||||||
|
|
||||||
|
create or replace function planstelle_vorgemerkt(p_position_id uuid, p_ab date, p_ausser uuid default null)
|
||||||
|
returns date language sql stable
|
||||||
|
set search_path = public, pg_temp
|
||||||
|
as $$
|
||||||
|
select min(o.effective_date)
|
||||||
|
from pending_org_changes o
|
||||||
|
where o.status = 'pending'
|
||||||
|
-- Nur die beiden Vorgänge, die eine Planstelle belegen. Eine
|
||||||
|
-- Vertragsänderung oder eine Karenz trägt keine Zielplanstelle.
|
||||||
|
and o.change_type in ('transfer', 'promotion')
|
||||||
|
and o.effective_date >= p_ab
|
||||||
|
and (p_ausser is null or o.employee_id <> p_ausser)
|
||||||
|
and nullif(o.payload->>'target_position_id', '')::uuid = p_position_id;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
comment on function planstelle_vorgemerkt(uuid, date, uuid) is
|
||||||
|
'Ab wann eine Planstelle durch eine vorgemerkte Versetzung oder Beförderung belegt ist, oder null. Die Vormerkung der übergebenen Person zählt nicht mit.';
|
||||||
|
|
||||||
|
grant execute on function planstelle_vorgemerkt(uuid, date, uuid) to alpenwerk_app;
|
||||||
|
|
||||||
|
|
||||||
|
-- ═══ hire_employee ════════════════════════════════════════════════
|
||||||
|
do $migration$
|
||||||
|
declare
|
||||||
|
v_alt constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_entry);$anker$;
|
||||||
|
v_neu constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_entry);
|
||||||
|
|
||||||
|
declare
|
||||||
|
v_vorgemerkt date := planstelle_vorgemerkt(v_position_id, v_entry, null);
|
||||||
|
begin
|
||||||
|
if v_vorgemerkt is not null then
|
||||||
|
raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.',
|
||||||
|
to_char(v_vorgemerkt, 'DD.MM.YYYY');
|
||||||
|
end if;
|
||||||
|
end;
|
||||||
|
$anker$;
|
||||||
|
v_def text;
|
||||||
|
v_anzahl int;
|
||||||
|
begin
|
||||||
|
select pg_get_functiondef(p.oid) into v_def
|
||||||
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
where n.nspname = 'public' and p.proname = 'hire_employee' and p.prokind = 'f';
|
||||||
|
if v_def is null then raise exception 'hire_employee ist nicht vorhanden.'; end if;
|
||||||
|
|
||||||
|
v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt);
|
||||||
|
if v_anzahl <> 1 then
|
||||||
|
raise exception 'Der Anker in hire_employee kommt % mal vor, erwartet einmal.', v_anzahl;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
execute replace(v_def, v_alt, v_neu);
|
||||||
|
end
|
||||||
|
$migration$;
|
||||||
|
|
||||||
|
-- ═══ rehire_employee ══════════════════════════════════════════════
|
||||||
|
do $migration$
|
||||||
|
declare
|
||||||
|
v_alt constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_date);$anker$;
|
||||||
|
v_neu constant text := $anker$and (pa.valid_to is null or pa.valid_to > v_date);
|
||||||
|
|
||||||
|
declare
|
||||||
|
v_vorgemerkt date := planstelle_vorgemerkt(v_position_id, v_date, v_employee_id);
|
||||||
|
begin
|
||||||
|
if v_vorgemerkt is not null then
|
||||||
|
raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.',
|
||||||
|
to_char(v_vorgemerkt, 'DD.MM.YYYY');
|
||||||
|
end if;
|
||||||
|
end;
|
||||||
|
$anker$;
|
||||||
|
v_def text;
|
||||||
|
v_anzahl int;
|
||||||
|
begin
|
||||||
|
select pg_get_functiondef(p.oid) into v_def
|
||||||
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
where n.nspname = 'public' and p.proname = 'rehire_employee' and p.prokind = 'f';
|
||||||
|
if v_def is null then raise exception 'rehire_employee ist nicht vorhanden.'; end if;
|
||||||
|
|
||||||
|
v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt);
|
||||||
|
if v_anzahl <> 1 then
|
||||||
|
raise exception 'Der Anker in rehire_employee kommt % mal vor, erwartet einmal.', v_anzahl;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
execute replace(v_def, v_alt, v_neu);
|
||||||
|
end
|
||||||
|
$migration$;
|
||||||
|
|
||||||
|
-- ═══ transfer_employee ════════════════════════════════════════════
|
||||||
|
do $migration$
|
||||||
|
declare
|
||||||
|
v_alt constant text := $anker$where pa.position_id = v_target_position and pa.valid_to is null;$anker$;
|
||||||
|
v_neu constant text := $anker$where pa.position_id = v_target_position and pa.valid_to is null;
|
||||||
|
|
||||||
|
declare
|
||||||
|
v_vorgemerkt date := planstelle_vorgemerkt(v_target_position, v_effective, v_employee_id);
|
||||||
|
begin
|
||||||
|
if v_vorgemerkt is not null then
|
||||||
|
raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.',
|
||||||
|
to_char(v_vorgemerkt, 'DD.MM.YYYY');
|
||||||
|
end if;
|
||||||
|
end;
|
||||||
|
$anker$;
|
||||||
|
v_def text;
|
||||||
|
v_anzahl int;
|
||||||
|
begin
|
||||||
|
select pg_get_functiondef(p.oid) into v_def
|
||||||
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
where n.nspname = 'public' and p.proname = 'transfer_employee' and p.prokind = 'f';
|
||||||
|
if v_def is null then raise exception 'transfer_employee ist nicht vorhanden.'; end if;
|
||||||
|
|
||||||
|
v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt);
|
||||||
|
if v_anzahl <> 1 then
|
||||||
|
raise exception 'Der Anker in transfer_employee kommt % mal vor, erwartet einmal.', v_anzahl;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
execute replace(v_def, v_alt, v_neu);
|
||||||
|
end
|
||||||
|
$migration$;
|
||||||
|
|
||||||
|
-- ═══ promote_employee ═════════════════════════════════════════════
|
||||||
|
--
|
||||||
|
-- Ohne Zielplanstelle ist v_position null; die Auskunft liefert dann nichts,
|
||||||
|
-- und der Zweig läuft durch. Eine Beförderung auf derselben Stelle braucht
|
||||||
|
-- deshalb keine Sonderbehandlung.
|
||||||
|
do $migration$
|
||||||
|
declare
|
||||||
|
v_alt constant text := $anker$where pa.position_id = v_position and pa.valid_to is null;$anker$;
|
||||||
|
v_neu constant text := $anker$where pa.position_id = v_position and pa.valid_to is null;
|
||||||
|
|
||||||
|
declare
|
||||||
|
v_vorgemerkt date := planstelle_vorgemerkt(v_position, v_effective_date, v_employee_id);
|
||||||
|
begin
|
||||||
|
if v_vorgemerkt is not null then
|
||||||
|
raise exception 'Für diese Planstelle ist ab dem % bereits eine Versetzung vorgemerkt.',
|
||||||
|
to_char(v_vorgemerkt, 'DD.MM.YYYY');
|
||||||
|
end if;
|
||||||
|
end;
|
||||||
|
$anker$;
|
||||||
|
v_def text;
|
||||||
|
v_anzahl int;
|
||||||
|
begin
|
||||||
|
select pg_get_functiondef(p.oid) into v_def
|
||||||
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
where n.nspname = 'public' and p.proname = 'promote_employee' and p.prokind = 'f';
|
||||||
|
if v_def is null then raise exception 'promote_employee ist nicht vorhanden.'; end if;
|
||||||
|
|
||||||
|
v_anzahl := (length(v_def) - length(replace(v_def, v_alt, ''))) / length(v_alt);
|
||||||
|
if v_anzahl <> 1 then
|
||||||
|
raise exception 'Der Anker in promote_employee kommt % mal vor, erwartet einmal.', v_anzahl;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
execute replace(v_def, v_alt, v_neu);
|
||||||
|
end
|
||||||
|
$migration$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ═══ Selbstprüfung ════════════════════════════════════════════════
|
||||||
|
do $$
|
||||||
|
declare
|
||||||
|
v_name text;
|
||||||
|
v_def text;
|
||||||
|
begin
|
||||||
|
foreach v_name in array array['hire_employee', 'rehire_employee', 'transfer_employee', 'promote_employee'] loop
|
||||||
|
select pg_get_functiondef(p.oid) into v_def
|
||||||
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
where n.nspname = 'public' and p.proname = v_name and p.prokind = 'f';
|
||||||
|
|
||||||
|
if v_def not like '%planstelle_vorgemerkt(%' then
|
||||||
|
raise exception '% fragt die vorgemerkten Planstellen nicht ab.', v_name;
|
||||||
|
end if;
|
||||||
|
if v_def not like '%require_hr_admin()%' then
|
||||||
|
raise exception '% prueft die Rechte nicht.', v_name;
|
||||||
|
end if;
|
||||||
|
if v_def not like '%search_path%' then
|
||||||
|
raise exception '% hat keinen festen search_path.', v_name;
|
||||||
|
end if;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
-- Was an diesen Funktionen schon einmal verlorenging. Die Statusumwandlung
|
||||||
|
-- im Wiedereintritt ist zweimal verschwunden; sie wird hier mitgeprüft,
|
||||||
|
-- weil diese Migration die Funktion erneut erzeugt.
|
||||||
|
select pg_get_functiondef(p.oid) into v_def
|
||||||
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
where n.nspname = 'public' and p.proname = 'rehire_employee' and p.prokind = 'f';
|
||||||
|
if v_def not like '%end)::employment_status%' then
|
||||||
|
raise exception 'rehire_employee wandelt den Status nicht um.';
|
||||||
|
end if;
|
||||||
|
if v_def not like '%cornerstone_id%' then
|
||||||
|
raise exception 'rehire_employee hat die Cornerstone-ID verloren.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_get_functiondef(p.oid) into v_def
|
||||||
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
where n.nspname = 'public' and p.proname = 'promote_employee' and p.prokind = 'f';
|
||||||
|
if v_def not like '%valid_from >= v_effective_date%' then
|
||||||
|
raise exception 'promote_employee raeumt die Besetzung vom selben Tag nicht mehr ab.';
|
||||||
|
end if;
|
||||||
|
if v_def not like '%target_position_id%' then
|
||||||
|
raise exception 'promote_employee hat die Zielplanstelle verloren.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
select pg_get_functiondef(p.oid) into v_def
|
||||||
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
||||||
|
where n.nspname = 'public' and p.proname = 'transfer_employee' and p.prokind = 'f';
|
||||||
|
if v_def not like '%valid_from >= v_effective%' then
|
||||||
|
raise exception 'transfer_employee raeumt die Besetzung vom selben Tag nicht mehr ab.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
raise notice 'Vorgemerkte Planstellen: alle vier Buchungswege pruefen sie.';
|
||||||
|
end
|
||||||
|
$$;
|
||||||
@@ -50,7 +50,20 @@ export const RUECKKEHR_GRUENDE = ["Wiedereingliederungsteilzeit", "Elternteilzei
|
|||||||
* geregelte Teilzeiten, die man später wiederfinden können muss — im
|
* geregelte Teilzeiten, die man später wiederfinden können muss — im
|
||||||
* Zweifel Jahre danach, wenn jemand fragt, warum die Stunden damals sanken.
|
* Zweifel Jahre danach, wenn jemand fragt, warum die Stunden damals sanken.
|
||||||
*/
|
*/
|
||||||
export const STUNDEN_GRUENDE = ["Vertragliche Stundenänderung", "Bildungsteilzeit", "Pflegeteilzeit"] as const;
|
export const STUNDEN_GRUENDE = [
|
||||||
|
"Vertragliche Stundenänderung",
|
||||||
|
"Bildungsteilzeit",
|
||||||
|
"Pflegeteilzeit",
|
||||||
|
// Die beiden standen zuerst nur in RUECKKEHR_GRUENDE, weil sie typischerweise
|
||||||
|
// dann beginnen, wenn eine Abwesenheit endet. Typischerweise ist aber nicht
|
||||||
|
// immer: eine Elternteilzeit kann auch vereinbart werden, ohne dass in
|
||||||
|
// diesem System je eine Karenz stand — etwa beim zweiten Elternteil oder
|
||||||
|
// wenn die Karenz vor der Einführung lag. Gemeldet von Lara am 29.09.:
|
||||||
|
// „Teilzeitvariante Elternteilzeit fehlt". Der Bericht konnte schon immer
|
||||||
|
// nach allen vier filtern; erfassen liessen sich nur zwei.
|
||||||
|
"Elternteilzeit",
|
||||||
|
"Wiedereingliederungsteilzeit",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export type StundenGrund = (typeof STUNDEN_GRUENDE)[number];
|
export type StundenGrund = (typeof STUNDEN_GRUENDE)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,16 @@ export async function loadOrgAsOf(tx: Tx, asOf: string): Promise<OrgAsOfResult>
|
|||||||
// Ohne die 1000-Zeilen-Grenze der API-Schicht fällt das seitenweise
|
// Ohne die 1000-Zeilen-Grenze der API-Schicht fällt das seitenweise
|
||||||
// Nachladen weg: sechs Abfragen, jede vollständig.
|
// Nachladen weg: sechs Abfragen, jede vollständig.
|
||||||
const [units, positions, assignments, employees, pending, earliest] = await Promise.all([
|
const [units, positions, assignments, employees, pending, earliest] = await Promise.all([
|
||||||
tx.selectFrom("org_units").select(["id", "parent_id"]).orderBy("id").execute(),
|
// Zum Stichtag, wie die Planstellen und die Besetzungen darunter. Hier
|
||||||
|
// stand die Tabelle ungefiltert: eine Einheit, die es 2020 noch nicht gab,
|
||||||
|
// erschien in der Organisation von 2020. Gemeldet am 29.09. (D.06).
|
||||||
|
tx
|
||||||
|
.selectFrom("org_units")
|
||||||
|
.select(["id", "parent_id"])
|
||||||
|
.where("valid_from", "<=", asOf)
|
||||||
|
.where((eb) => eb.or([eb("valid_to", "is", null), eb("valid_to", ">", asOf)]))
|
||||||
|
.orderBy("id")
|
||||||
|
.execute(),
|
||||||
|
|
||||||
tx
|
tx
|
||||||
.selectFrom("om_positions as p")
|
.selectFrom("om_positions as p")
|
||||||
|
|||||||
@@ -92,11 +92,30 @@ describe("Gründe für weniger Stunden", () => {
|
|||||||
expect([...STUNDEN_GRUENDE]).toContain("Pflegeteilzeit");
|
expect([...STUNDEN_GRUENDE]).toContain("Pflegeteilzeit");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("verteilt die vier Teilzeiten vollständig und ohne Überschneidung", () => {
|
it("bietet alle vier Teilzeiten bei der Stundenänderung an", () => {
|
||||||
// Keine darf verlorengehen, und keine an beiden Stellen auftauchen —
|
// Bis zum 29.09. standen Eltern- und Wiedereingliederungsteilzeit nur bei
|
||||||
// sonst hinge dieselbe Sache an zwei Vorgängen.
|
// der Rückkehr — mit der Begründung, dass sie typischerweise dann
|
||||||
const verteilt = [...RUECKKEHR_GRUENDE, ...STUNDEN_GRUENDE].filter((g) => TEILZEITEN.includes(g));
|
// beginnen. Typischerweise ist nicht immer, und wer sie ohne
|
||||||
expect(new Set(verteilt)).toEqual(new Set(TEILZEITEN));
|
// vorangegangene Abwesenheit vereinbart, fand sie nirgends. Gemeldet von
|
||||||
expect(verteilt.length).toBe(TEILZEITEN.length);
|
// Lara: „Teilzeitvariante Elternteilzeit fehlt".
|
||||||
|
for (const teilzeit of TEILZEITEN) {
|
||||||
|
expect([...STUNDEN_GRUENDE], `„${teilzeit}" fehlt bei der Stundenänderung`).toContain(teilzeit);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("überschneidet sich dabei bewusst mit der Rückkehr", () => {
|
||||||
|
// Die Überschneidung ist kein Versehen: es ist derselbe Wert in derselben
|
||||||
|
// Spalte, nur aus zwei Anlässen erfasst. Was es nicht geben darf, ist eine
|
||||||
|
// Teilzeit, die an *keinem* der beiden Wege hängt.
|
||||||
|
const beide = TEILZEITEN.filter(
|
||||||
|
(t) => ([...RUECKKEHR_GRUENDE] as string[]).includes(t) && ([...STUNDEN_GRUENDE] as string[]).includes(t)
|
||||||
|
);
|
||||||
|
expect(new Set(beide)).toEqual(new Set(RUECKKEHR_GRUENDE));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("führt den Normalfall nicht als Teilzeit", () => {
|
||||||
|
// „Vertragliche Stundenänderung" ist keine Variante, sondern ihr Fehlen:
|
||||||
|
// sie landet als null in teilzeit_art.
|
||||||
|
expect([...TEILZEITEN]).not.toContain("Vertragliche Stundenänderung");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user