Keep private email out of the Honestly export
The Email column exported employees.email, which is the person's private address. That does not belong in a file sent to an outside survey provider, least of all as the address invitations go to in the employer's name. The column now stays as a placeholder for the work email, which does not exist in the schema yet and will be added later. It is empty until then, but keeps its place so the column mapping set up in Honestly does not have to change once the address arrives. The export no longer reads employees.email at all, so it cannot end up in another column by accident either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -644,8 +644,9 @@ export function ReportsPageClient(props: ReportsPageClientProps) {
|
|||||||
<div className="rounded border border-border bg-white p-4">
|
<div className="rounded border border-border bg-white p-4">
|
||||||
<h3 className="mb-1 text-xs font-semibold uppercase tracking-wide text-ink-muted">Honestly-Report</h3>
|
<h3 className="mb-1 text-xs font-semibold uppercase tracking-wide text-ink-muted">Honestly-Report</h3>
|
||||||
<p className="mb-1 text-xs text-ink-muted">
|
<p className="mb-1 text-xs text-ink-muted">
|
||||||
Teilnehmerliste für die Mitarbeiterbefragung: Personalnummer, E-Mail, Name, Standort und die
|
Teilnehmerliste für die Mitarbeiterbefragung: Personalnummer, Name, Standort und die
|
||||||
Organisationskette von der eigenen Einheit bis ganz oben. Sprache immer „de“, Rolle immer „Respondee“.
|
Organisationskette von der eigenen Einheit bis ganz oben. Sprache immer „de“, Rolle immer „Respondee“.
|
||||||
|
Die Spalte „Email“ bleibt leer, bis die Firmen-E-Mail-Adresse erfasst wird.
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-2 rounded bg-surface px-2 py-1.5 text-xs font-semibold text-ink-body">Status im Export: {statusExportLabel}</p>
|
<p className="mb-2 rounded bg-surface px-2 py-1.5 text-xs font-semibold text-ink-body">Status im Export: {statusExportLabel}</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|||||||
@@ -14,12 +14,18 @@ import { ancestorsOf, type OrgMaps } from "./org";
|
|||||||
// hängt, hat hinten leere Zellen — und die oberste Einheit steht deshalb
|
// hängt, hat hinten leere Zellen — und die oberste Einheit steht deshalb
|
||||||
// nicht bei allen in derselben Spalte. So ist es bestellt.
|
// 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.
|
||||||
export const HONESTLY_SPRACHE = "de";
|
export const HONESTLY_SPRACHE = "de";
|
||||||
export const HONESTLY_ROLLE = "Respondee";
|
export const HONESTLY_ROLLE = "Respondee";
|
||||||
|
|
||||||
export type HonestlyZeile = {
|
export type HonestlyZeile = {
|
||||||
personalnummer: number | string | null;
|
personalnummer: number | string | null;
|
||||||
email: string | null;
|
|
||||||
vorname: string;
|
vorname: string;
|
||||||
nachname: string;
|
nachname: string;
|
||||||
standort: string;
|
standort: string;
|
||||||
@@ -58,7 +64,8 @@ export function honestlySpalten(tiefe: number): ExportColumn<HonestlyZeile>[] {
|
|||||||
}));
|
}));
|
||||||
return [
|
return [
|
||||||
{ header: "Personalnummer", get: (z) => z.personalnummer },
|
{ header: "Personalnummer", get: (z) => z.personalnummer },
|
||||||
{ header: "Email", get: (z) => z.email ?? "" },
|
// Platzhalter für die Firmenadresse — siehe oben.
|
||||||
|
{ header: "Email", get: () => "" },
|
||||||
{ header: "Firstname", get: (z) => z.vorname },
|
{ header: "Firstname", get: (z) => z.vorname },
|
||||||
{ header: "Last Name", get: (z) => z.nachname },
|
{ header: "Last Name", get: (z) => z.nachname },
|
||||||
{ header: "Language", get: () => HONESTLY_SPRACHE },
|
{ header: "Language", get: () => HONESTLY_SPRACHE },
|
||||||
@@ -72,7 +79,6 @@ export function honestlySpalten(tiefe: number): ExportColumn<HonestlyZeile>[] {
|
|||||||
export function baueHonestlyZeilen(
|
export function baueHonestlyZeilen(
|
||||||
personen: {
|
personen: {
|
||||||
personnel_number: number | string | null;
|
personnel_number: number | string | null;
|
||||||
email: string | null;
|
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
location_id: string;
|
location_id: string;
|
||||||
@@ -83,7 +89,6 @@ export function baueHonestlyZeilen(
|
|||||||
): { zeilen: HonestlyZeile[]; tiefe: number } {
|
): { zeilen: HonestlyZeile[]; tiefe: number } {
|
||||||
const zeilen = personen.map((p) => ({
|
const zeilen = personen.map((p) => ({
|
||||||
personalnummer: p.personnel_number,
|
personalnummer: p.personnel_number,
|
||||||
email: p.email,
|
|
||||||
vorname: p.first_name,
|
vorname: p.first_name,
|
||||||
nachname: p.last_name,
|
nachname: p.last_name,
|
||||||
standort: standortName.get(p.location_id) ?? "",
|
standort: standortName.get(p.location_id) ?? "",
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ const standorte = new Map([["wien", "Wien"], ["wolkersdorf", "Wolkersdorf"]]);
|
|||||||
function person(teil: Partial<Parameters<typeof baueHonestlyZeilen>[0][number]> = {}) {
|
function person(teil: Partial<Parameters<typeof baueHonestlyZeilen>[0][number]> = {}) {
|
||||||
return {
|
return {
|
||||||
personnel_number: 4711,
|
personnel_number: 4711,
|
||||||
email: "anna.berger@example.test",
|
// Die private Adresse, wie sie auf der Zeile aus der Datenbank steht —
|
||||||
|
// sie darf im Export nirgends auftauchen.
|
||||||
|
email: "anna.privat@example.test",
|
||||||
first_name: "Anna",
|
first_name: "Anna",
|
||||||
last_name: "Berger",
|
last_name: "Berger",
|
||||||
location_id: "wien",
|
location_id: "wien",
|
||||||
@@ -97,18 +99,33 @@ describe("baueHonestlyZeilen", () => {
|
|||||||
const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])]));
|
const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])]));
|
||||||
expect(zeile).toMatchObject({
|
expect(zeile).toMatchObject({
|
||||||
Personalnummer: 4711,
|
Personalnummer: 4711,
|
||||||
Email: "anna.berger@example.test",
|
|
||||||
Firstname: "Anna",
|
Firstname: "Anna",
|
||||||
"Last Name": "Berger",
|
"Last Name": "Berger",
|
||||||
Location: "Wien",
|
Location: "Wien",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lässt eine fehlende E-Mail leer, statt sie zu erfinden", () => {
|
it("lässt die E-Mail leer, bis es die Firmenadresse gibt", () => {
|
||||||
// Die Adresse ist privat und freiwillig (20260811140000). Eine leere
|
// Die Spalte ist ein Platzhalter für die Firmen-E-Mail. Die private
|
||||||
// Zelle fällt in Honestly auf; ein Platzhalter bekäme eine Einladung.
|
// Adresse (employees.email) gehört nicht in eine Datei an einen fremden
|
||||||
const { zeilen } = baueHonestlyZeilen([person({ email: null })], maps, standorte);
|
// Anbieter — auch nicht, wenn sie vorhanden ist.
|
||||||
expect(honestlySpalten(1)[1].get(zeilen[0])).toBe("");
|
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("");
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
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", () => {
|
it("gibt Personen ohne Einheit eine leere Kette, aber eine Zeile", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user