Die Firmen-E-Mail als eigenes, freiwilliges Feld
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m29s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m27s

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.
This commit is contained in:
2026-09-23 10:52:26 +02:00
parent 1816ab68ad
commit 57acbfae9d
16 changed files with 948 additions and 15 deletions

View File

@@ -58,7 +58,7 @@ export async function bestandLaden(tx: Tx): Promise<Bestand> {
.select(["position_id"])
.where((eb) => eb.or([eb("valid_to", "is", null), eb("valid_to", ">=", heute)]))
.execute(),
tx.selectFrom("employees").select(["id", "personnel_number", "email", "sv_nummer"]).execute(),
tx.selectFrom("employees").select(["id", "personnel_number", "email", "company_email", "sv_nummer"]).execute(),
]);
const besetzt = new Set(besetzungen.map((b) => b.position_id));
@@ -72,6 +72,7 @@ export async function bestandLaden(tx: Tx): Promise<Bestand> {
// Ohne Adresse gibt es nichts zu vergleichen — seit sie freiwillig ist,
// kann sie fehlen.
emails: new Set(personen.filter((e) => e.email).map((e) => e.email!.toLowerCase())),
firmenEmails: new Set(personen.filter((e) => e.company_email).map((e) => e.company_email!.toLowerCase())),
svNummern: new Set(personen.filter((e) => e.sv_nummer).map((e) => normalizeSvnr(e.sv_nummer!))),
};
}
@@ -209,6 +210,7 @@ export async function laden(
city: txt(w.city),
address_country: txt(w.address_country),
email: txt(w.email)!,
company_email: txt(w.company_email),
phone: txt(w.phone),
title_prefix: liste(w.title_prefix) ?? [],
title_suffix: liste(w.title_suffix) ?? [],

View File

@@ -232,6 +232,14 @@ export const BLATT_PERSONEN: BlattSchema = {
hinweis: "Freiwillig. Eindeutig, wenn angegeben.",
beispiel: "s.aigner@example.at",
},
{
name: "Firmen-E-Mail",
ziel: "company_email",
pflicht: false,
typ: { art: "text" },
hinweis: "Dienstliche Adresse. Freiwillig, eindeutig. Geht in den Export für die Mitarbeiterbefragung.",
beispiel: "s.aigner@manner.com",
},
{ name: "Private Telefonnummer", ziel: "phone", pflicht: false, typ: { art: "text" }, hinweis: "", beispiel: "+43 660 1234567" },
{ name: "Adresse", ziel: "address", pflicht: false, typ: { art: "text" }, hinweis: "", beispiel: "Hauptstraße 1" },
{ name: "PLZ", ziel: "postal_code", pflicht: false, typ: { art: "text" }, hinweis: "", beispiel: "1010" },

View File

@@ -34,6 +34,7 @@ export type Bestand = {
planstellen: Map<string, { id: string; besetzt: boolean }>;
personalnummern: Map<number, string>;
emails: Set<string>;
firmenEmails: Set<string>;
svNummern: Set<string>;
};
@@ -44,6 +45,7 @@ export const LEERER_BESTAND: Bestand = {
planstellen: new Map(),
personalnummern: new Map(),
emails: new Set(),
firmenEmails: new Set(),
svNummern: new Set(),
};
@@ -262,6 +264,7 @@ export function pruefe(blaetter: ImportSheet[], bestand: Bestand = LEERER_BESTAN
const heute = todayIso();
const personalnummern = new Set([...bestand.personalnummern.keys()]);
const emails = new Set([...bestand.emails]);
const firmenEmails = new Set([...bestand.firmenEmails]);
const svNummern = new Set([...bestand.svNummern]);
const belegtePlanstellen = new Map<string, number>();
const eintritte = new Map<number, string>();
@@ -282,6 +285,18 @@ export function pruefe(blaetter: ImportSheet[], bestand: Bestand = LEERER_BESTAN
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) melde("Personen", z.zeile, "Private E-Mail", "Sieht nicht wie eine Adresse aus.", email);
}
// Eigener Vorrat, nicht derselbe wie bei der privaten Adresse: die beiden
// Spalten haben je einen eigenen Index, und dieselbe Adresse darf bei
// einer Person privat und dienstlich stehen.
const firmenEmail = s(w.company_email)?.toLowerCase();
if (firmenEmail) {
if (firmenEmails.has(firmenEmail)) melde("Personen", z.zeile, "Firmen-E-Mail", "Kommt bereits vor.", firmenEmail);
firmenEmails.add(firmenEmail);
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(firmenEmail)) {
melde("Personen", z.zeile, "Firmen-E-Mail", "Sieht nicht wie eine Adresse aus.", firmenEmail);
}
}
const geburt = s(w.birth_date);
const svRoh = s(w.sv_nummer);
if (svRoh) {