Die Firmen-E-Mail als eigenes, freiwilliges Feld
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:
@@ -14,18 +14,23 @@ import { ancestorsOf, type OrgMaps } from "./org";
|
||||
// hängt, hat hinten leere Zellen — und die oberste Einheit steht deshalb
|
||||
// nicht bei allen in derselben Spalte. So ist es bestellt.
|
||||
|
||||
// Die E-Mail-Spalte ist ein Platzhalter für die **Firmenadresse**, die es
|
||||
// im Datenmodell noch nicht gibt. employees.email ist die private Adresse
|
||||
// (Migration 20260811140000) und gehört nicht in eine Datei, die an einen
|
||||
// fremden Anbieter geht — schon gar nicht als Adresse, an die Einladungen im
|
||||
// Namen des Arbeitgebers gehen. Deshalb bleibt die Spalte leer, bis die
|
||||
// Firmenadresse erfasst wird; die Spalte selbst bleibt stehen, damit die
|
||||
// Zuordnung in Honestly nicht neu eingerichtet werden muss.
|
||||
// Die E-Mail-Spalte trägt die **Firmenadresse** (employees.company_email,
|
||||
// Migration 20260923120000) und niemals die private aus employees.email: die
|
||||
// Datei geht an einen fremden Anbieter, der damit im Namen des Arbeitgebers
|
||||
// einlädt. Eine Privatadresse hat dort nichts verloren.
|
||||
//
|
||||
// Bis zur Migration war die Spalte ein leerer Platzhalter — sie stand
|
||||
// trotzdem in der Datei, damit die Zuordnung in Honestly nicht neu
|
||||
// eingerichtet werden muss. Wer keine Firmenadresse hinterlegt hat, hat sie
|
||||
// weiterhin leer; das ist eine fehlende Angabe und keine Ausrede, die private
|
||||
// zu nehmen.
|
||||
export const HONESTLY_SPRACHE = "de";
|
||||
export const HONESTLY_ROLLE = "Respondee";
|
||||
|
||||
export type HonestlyZeile = {
|
||||
personalnummer: number | string | null;
|
||||
/** Die dienstliche Adresse. Leer, wenn keine hinterlegt ist. */
|
||||
email: string;
|
||||
vorname: string;
|
||||
nachname: string;
|
||||
standort: string;
|
||||
@@ -64,8 +69,7 @@ export function honestlySpalten(tiefe: number): ExportColumn<HonestlyZeile>[] {
|
||||
}));
|
||||
return [
|
||||
{ header: "Personalnummer", get: (z) => z.personalnummer },
|
||||
// Platzhalter für die Firmenadresse — siehe oben.
|
||||
{ header: "Email", get: () => "" },
|
||||
{ header: "Email", get: (z) => z.email },
|
||||
{ header: "Firstname", get: (z) => z.vorname },
|
||||
{ header: "Last Name", get: (z) => z.nachname },
|
||||
{ header: "Language", get: () => HONESTLY_SPRACHE },
|
||||
@@ -79,6 +83,7 @@ export function honestlySpalten(tiefe: number): ExportColumn<HonestlyZeile>[] {
|
||||
export function baueHonestlyZeilen(
|
||||
personen: {
|
||||
personnel_number: number | string | null;
|
||||
company_email: string | null;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
location_id: string;
|
||||
@@ -89,6 +94,7 @@ export function baueHonestlyZeilen(
|
||||
): { zeilen: HonestlyZeile[]; tiefe: number } {
|
||||
const zeilen = personen.map((p) => ({
|
||||
personalnummer: p.personnel_number,
|
||||
email: p.company_email ?? "",
|
||||
vorname: p.first_name,
|
||||
nachname: p.last_name,
|
||||
standort: standortName.get(p.location_id) ?? "",
|
||||
|
||||
@@ -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) ?? [],
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -207,6 +207,13 @@ export type Database = {
|
||||
address_country: string | null;
|
||||
/** Private Adresse, freiwillig — eindeutig, wenn angegeben. */
|
||||
email: string | null;
|
||||
/**
|
||||
* Dienstliche Adresse, freiwillig — eindeutig, wenn angegeben.
|
||||
*
|
||||
* Getrennt von `email`: die private gehört nicht in Dateien, die
|
||||
* das Haus verlassen. Der Honestly-Export nimmt diese hier.
|
||||
*/
|
||||
company_email: string | null;
|
||||
phone: string | null;
|
||||
job_title: string;
|
||||
location_id: string;
|
||||
@@ -286,6 +293,7 @@ export type Database = {
|
||||
city?: string | null;
|
||||
address_country?: string | null;
|
||||
email?: string | null;
|
||||
company_email?: string | null;
|
||||
phone?: string | null;
|
||||
job_title: string;
|
||||
location_id: string;
|
||||
|
||||
@@ -49,6 +49,12 @@ export function vorbelegungAus(employee: EmployeeRow): Partial<HireDraftData> {
|
||||
birthDate: employee.birth_date,
|
||||
svNummer: employee.sv_nummer ?? "",
|
||||
email: employee.email ?? "",
|
||||
// Vorbelegt wie die private Adresse, obwohl eine Dienstadresse beim
|
||||
// Austritt meist abgedreht wird: der Assistent schickt das Feld in jedem
|
||||
// Fall mit, und leer geliefert löschte es die gespeicherte Adresse
|
||||
// stillschweigend. Sichtbar vorbelegt lässt sie sich bewusst ersetzen
|
||||
// oder leeren.
|
||||
companyEmail: employee.company_email ?? "",
|
||||
phone: employee.phone ?? "",
|
||||
locationId: employee.location_id,
|
||||
besetzung: employee.source,
|
||||
|
||||
Reference in New Issue
Block a user