employees.email ist die private Adresse (20260811140000). Sie taugt nicht als Dienstadresse und darf auch nicht als solche benutzt werden: der Honestly-Export geht an einen fremden Anbieter, der damit im Namen des Arbeitgebers einlaedt. Die Spalte "Email" stand dort deshalb seit jeher leer, mit einem Vermerk in lib/honestly.ts, dass die Firmenadresse im Datenmodell fehlt. Jetzt gibt es sie, und die Spalte fuellt sich. Eindeutig, aber freiwillig -- mehrere Personen ohne Adresse stoeren den Index nicht, weil null nie gleich null ist. Geschrieben wird ueber `case when ? then nullif` statt `coalesce`: eine Dienstadresse muss sich auch wieder entfernen lassen. Vier SQL-Funktionen mussten mit, weil `create or replace` die ganze Fassung ersetzt und ein ausgelassenes Feld dort still verschwindet: hire_employee und rehire_employee (beide teilen sich den Schritt "Person" -- das Formular haette das Feld gezeigt und den Wert weggeworfen), change_employee_data (sonst nicht aenderbar), apply_due_pending_changes (sonst verfiele eine auf spaeter datierte Aenderung) und die Feldkarte (sonst waere der Eintrag in der Historie nicht korrigierbar). Die Selbstpruefung am Ende prueft jede einzeln.
161 lines
6.8 KiB
TypeScript
161 lines
6.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { toCsv } from "@/lib/export";
|
||
import { baueHonestlyZeilen, honestlySpalten, orgKette, ouUeberschrift } from "@/lib/honestly";
|
||
import { buildOrgMaps, type OrgUnit } from "@/lib/org";
|
||
|
||
// Die Datei geht an eine fremde Plattform, die sie über Spaltennamen
|
||
// zuordnet. Ein Fehler darin zeigt sich nicht hier, sondern dort — als
|
||
// Befragung, die an die falschen Leute geht oder nach der falschen Einheit
|
||
// auswertet. Deshalb wird hier die Form festgehalten, nicht nur der Inhalt.
|
||
|
||
const units: OrgUnit[] = [
|
||
{ id: "g", org_number: "1", name: "Manner AG", parent_id: null, unit_type: "Gesellschaft" },
|
||
{ id: "b", org_number: "2", name: "Produktion", parent_id: "g", unit_type: "Bereich" },
|
||
{ id: "a", org_number: "3", name: "Waffel", parent_id: "b", unit_type: "Abteilung" },
|
||
{ id: "t", org_number: "4", name: "Linie 1", parent_id: "a", unit_type: "Team" },
|
||
{ id: "v", org_number: "5", name: "Vertrieb", parent_id: "g", unit_type: "Bereich" },
|
||
];
|
||
const maps = buildOrgMaps(units, []);
|
||
const standorte = new Map([["wien", "Wien"], ["wolkersdorf", "Wolkersdorf"]]);
|
||
|
||
function person(teil: Partial<Parameters<typeof baueHonestlyZeilen>[0][number]> = {}) {
|
||
return {
|
||
personnel_number: 4711,
|
||
// Die private Adresse, wie sie auf der Zeile aus der Datenbank steht —
|
||
// sie darf im Export nirgends auftauchen.
|
||
email: "anna.privat@example.test",
|
||
company_email: "a.berger@firma.test",
|
||
first_name: "Anna",
|
||
last_name: "Berger",
|
||
location_id: "wien",
|
||
org_unit_id: "t",
|
||
...teil,
|
||
};
|
||
}
|
||
|
||
describe("orgKette", () => {
|
||
it("fängt bei der eigenen Einheit an und endet ganz oben", () => {
|
||
// OU ist die Einheit der Person, OU+1 die darüber — so ist es bestellt.
|
||
// ancestorsOf liefert es andersherum; ohne das Umdrehen stünde die
|
||
// Gesellschaft in der Spalte „OU".
|
||
expect(orgKette(maps, "t")).toEqual(["Linie 1", "Waffel", "Produktion", "Manner AG"]);
|
||
});
|
||
|
||
it("ist leer ohne Einheit", () => {
|
||
expect(orgKette(maps, null)).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe("Spalten", () => {
|
||
it("stehen in der vorgegebenen Reihenfolge", () => {
|
||
expect(honestlySpalten(3).map((s) => s.header)).toEqual([
|
||
"Personalnummer",
|
||
"Email",
|
||
"Firstname",
|
||
"Last Name",
|
||
"Language",
|
||
"Location",
|
||
"OU",
|
||
"OU+1",
|
||
"OU+2",
|
||
"Role",
|
||
]);
|
||
});
|
||
|
||
it("heissen OU, OU+1, OU+2 …", () => {
|
||
expect([0, 1, 2, 5].map(ouUeberschrift)).toEqual(["OU", "OU+1", "OU+2", "OU+5"]);
|
||
});
|
||
|
||
it("behalten mindestens eine OU-Spalte", () => {
|
||
// Eine Datei, deren Spalten je nach Inhalt verschwinden, bricht die
|
||
// Zuordnung, die auf der anderen Seite eingerichtet wurde.
|
||
expect(honestlySpalten(0).filter((s) => s.header.startsWith("OU"))).toHaveLength(1);
|
||
});
|
||
});
|
||
|
||
describe("baueHonestlyZeilen", () => {
|
||
it("richtet die Zahl der OU-Spalten nach der tiefsten Kette", () => {
|
||
const { tiefe } = baueHonestlyZeilen([person({ org_unit_id: "v" }), person({ org_unit_id: "t" })], maps, standorte);
|
||
expect(tiefe).toBe(4);
|
||
});
|
||
|
||
it("füllt kürzere Ketten hinten mit Leerzellen auf", () => {
|
||
const { zeilen, tiefe } = baueHonestlyZeilen([person({ org_unit_id: "v" }), person()], maps, standorte);
|
||
const spalten = honestlySpalten(tiefe);
|
||
const werte = (i: number) => spalten.map((s) => s.get(zeilen[i]));
|
||
// Vertrieb hängt direkt unter der Gesellschaft: zwei Einheiten, zwei leere.
|
||
expect(werte(0).slice(6, 10)).toEqual(["Vertrieb", "Manner AG", "", ""]);
|
||
expect(werte(1).slice(6, 10)).toEqual(["Linie 1", "Waffel", "Produktion", "Manner AG"]);
|
||
});
|
||
|
||
it("setzt Sprache und Rolle fest", () => {
|
||
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
|
||
const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])]));
|
||
expect(zeile.Language).toBe("de");
|
||
expect(zeile.Role).toBe("Respondee");
|
||
});
|
||
|
||
it("übernimmt die Personendaten unverändert", () => {
|
||
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
|
||
const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])]));
|
||
expect(zeile).toMatchObject({
|
||
Personalnummer: 4711,
|
||
Firstname: "Anna",
|
||
"Last Name": "Berger",
|
||
Location: "Wien",
|
||
});
|
||
});
|
||
|
||
it("nimmt die Firmenadresse in die E-Mail-Spalte", () => {
|
||
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
|
||
const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])]));
|
||
expect(zeile.Email).toBe("a.berger@firma.test");
|
||
});
|
||
|
||
it("lässt die Spalte leer, wenn keine Firmenadresse hinterlegt ist", () => {
|
||
// Eine fehlende Dienstadresse ist eine fehlende Angabe — und keine
|
||
// Ausrede, ersatzweise die private zu nehmen.
|
||
const { zeilen, tiefe } = baueHonestlyZeilen([person({ company_email: null })], maps, standorte);
|
||
const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])]));
|
||
expect(zeile.Email).toBe("");
|
||
});
|
||
|
||
it("trägt die private Adresse in keine Spalte ein", () => {
|
||
// Nicht nur „Email" prüfen: rutschte sie in eine andere Spalte, fiele es
|
||
// dort erst in Honestly auf.
|
||
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
|
||
const csv = toCsv(zeilen, honestlySpalten(tiefe));
|
||
expect(csv).not.toContain("anna.privat@example.test");
|
||
expect(csv).toContain("a.berger@firma.test");
|
||
});
|
||
|
||
it("nimmt auch dann nicht die private Adresse, wenn keine dienstliche da ist", () => {
|
||
const { zeilen, tiefe } = baueHonestlyZeilen([person({ company_email: null })], maps, standorte);
|
||
expect(toCsv(zeilen, honestlySpalten(tiefe))).not.toContain("anna.privat@example.test");
|
||
});
|
||
|
||
it("behält die Spalte Email, auch solange sie leer ist", () => {
|
||
// Die Zuordnung in Honestly wird einmal eingerichtet. Kommt die
|
||
// Firmenadresse später dazu, soll sich an der Datei nur der Inhalt ändern.
|
||
expect(honestlySpalten(1).map((s) => s.header)).toContain("Email");
|
||
});
|
||
|
||
it("gibt Personen ohne Einheit eine leere Kette, aber eine Zeile", () => {
|
||
// Wer nicht zugeordnet ist, soll trotzdem befragt werden.
|
||
const { zeilen, tiefe } = baueHonestlyZeilen([person({ org_unit_id: null })], maps, standorte);
|
||
expect(zeilen).toHaveLength(1);
|
||
expect(tiefe).toBe(1);
|
||
expect(honestlySpalten(tiefe).find((s) => s.header === "OU")?.get(zeilen[0])).toBe("");
|
||
});
|
||
});
|
||
|
||
describe("als CSV", () => {
|
||
it("schreibt die Kopfzeile genau so, wie Honestly sie zuordnet", () => {
|
||
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
|
||
const kopf = toCsv(zeilen, honestlySpalten(tiefe)).replace(/^/, "").split(/\r?\n/)[0];
|
||
expect(kopf).toContain("Personalnummer");
|
||
expect(kopf).toContain("Last Name");
|
||
expect(kopf).toMatch(/OU\+3.*Role/);
|
||
});
|
||
});
|