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:
@@ -77,6 +77,14 @@ export async function hireEmployee(payload: {
|
|||||||
* eindeutig, wenn angegeben.
|
* eindeutig, wenn angegeben.
|
||||||
*/
|
*/
|
||||||
email?: string;
|
email?: string;
|
||||||
|
/**
|
||||||
|
* Die **dienstliche** Adresse, freiwillig und eindeutig.
|
||||||
|
*
|
||||||
|
* Eigenes Feld neben der privaten, weil nur diese das Haus verlassen darf:
|
||||||
|
* der Export für die Mitarbeiterbefragung geht an einen fremden Anbieter,
|
||||||
|
* der in unserem Namen Einladungen verschickt.
|
||||||
|
*/
|
||||||
|
company_email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
position_id?: string;
|
position_id?: string;
|
||||||
team_id?: string;
|
team_id?: string;
|
||||||
@@ -265,6 +273,7 @@ export async function changeEmployeeData(payload: {
|
|||||||
*/
|
*/
|
||||||
export async function rehireEmployee(payload: {
|
export async function rehireEmployee(payload: {
|
||||||
employee_id: string;
|
employee_id: string;
|
||||||
|
company_email?: string;
|
||||||
rehire_date: string;
|
rehire_date: string;
|
||||||
position_id: string;
|
position_id: string;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ function employeeExportColumns(
|
|||||||
{ header: "Ort", get: (e) => e.city },
|
{ header: "Ort", get: (e) => e.city },
|
||||||
{ header: "Wohnsitzland", get: (e) => e.address_country },
|
{ header: "Wohnsitzland", get: (e) => e.address_country },
|
||||||
{ header: "Private E-Mail", get: (e) => e.email },
|
{ header: "Private E-Mail", get: (e) => e.email },
|
||||||
|
{ header: "Firmen-E-Mail", get: (e) => e.company_email },
|
||||||
{ header: "Private Telefonnummer", get: (e) => e.phone },
|
{ header: "Private Telefonnummer", get: (e) => e.phone },
|
||||||
{ header: "Bereich", get: (e) => (e.org_unit_id ? (lookups.divisionName.get(e.org_unit_id) ?? "") : "") },
|
{ header: "Bereich", get: (e) => (e.org_unit_id ? (lookups.divisionName.get(e.org_unit_id) ?? "") : "") },
|
||||||
{ header: "Abteilung", get: (e) => (e.org_unit_id ? (lookups.departmentName.get(e.org_unit_id) ?? "") : "") },
|
{ header: "Abteilung", get: (e) => (e.org_unit_id ? (lookups.departmentName.get(e.org_unit_id) ?? "") : "") },
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export function DatenAendernPanel({
|
|||||||
const [city, setCity] = useState(employee.city ?? "");
|
const [city, setCity] = useState(employee.city ?? "");
|
||||||
const [addressCountry, setAddressCountry] = useState(employee.address_country ?? "Österreich");
|
const [addressCountry, setAddressCountry] = useState(employee.address_country ?? "Österreich");
|
||||||
const [email, setEmail] = useState(employee.email ?? "");
|
const [email, setEmail] = useState(employee.email ?? "");
|
||||||
|
const [companyEmail, setCompanyEmail] = useState(employee.company_email ?? "");
|
||||||
const [phone, setPhone] = useState(employee.phone ?? "");
|
const [phone, setPhone] = useState(employee.phone ?? "");
|
||||||
const [notfallName, setNotfallName] = useState(employee.emergency_contact_name ?? "");
|
const [notfallName, setNotfallName] = useState(employee.emergency_contact_name ?? "");
|
||||||
const [notfallTelefon, setNotfallTelefon] = useState(employee.emergency_contact_phone ?? "");
|
const [notfallTelefon, setNotfallTelefon] = useState(employee.emergency_contact_phone ?? "");
|
||||||
@@ -165,6 +166,10 @@ export function DatenAendernPanel({
|
|||||||
city,
|
city,
|
||||||
address_country: addressCountry,
|
address_country: addressCountry,
|
||||||
email,
|
email,
|
||||||
|
// Getrimmt und als leere Zeichenkette durchgereicht: die Datenbank
|
||||||
|
// macht daraus null, damit sich eine Dienstadresse auch wieder
|
||||||
|
// entfernen lässt.
|
||||||
|
company_email: companyEmail.trim(),
|
||||||
phone,
|
phone,
|
||||||
emergency_contact_name: notfallName.trim(),
|
emergency_contact_name: notfallName.trim(),
|
||||||
emergency_contact_phone: notfallTelefon.trim(),
|
emergency_contact_phone: notfallTelefon.trim(),
|
||||||
@@ -308,7 +313,8 @@ export function DatenAendernPanel({
|
|||||||
<Field label="Land" dense>
|
<Field label="Land" dense>
|
||||||
{(p) => <CountryPicker {...p} value={addressCountry} onChange={setAddressCountry} countries={UN_COUNTRIES} placeholder="Land suchen…" />}
|
{(p) => <CountryPicker {...p} value={addressCountry} onChange={setAddressCountry} countries={UN_COUNTRIES} placeholder="Land suchen…" />}
|
||||||
</Field>
|
</Field>
|
||||||
<TextField label="E-Mail" dense type="email" value={email} onChange={setEmail} />
|
<TextField label="E-Mail (privat)" dense type="email" value={email} onChange={setEmail} />
|
||||||
|
<TextField label="Firmen-E-Mail" dense type="email" value={companyEmail} onChange={setCompanyEmail} />
|
||||||
<TextField label="Telefon" dense type="tel" value={phone} onChange={setPhone} />
|
<TextField label="Telefon" dense type="tel" value={phone} onChange={setPhone} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export function StammdatenTab({ employee, location, dependents }: { employee: Em
|
|||||||
["Ort", employee.city ?? "–"],
|
["Ort", employee.city ?? "–"],
|
||||||
["Land", employee.address_country ?? "–"],
|
["Land", employee.address_country ?? "–"],
|
||||||
["Private E-Mail", employee.email ?? "–"],
|
["Private E-Mail", employee.email ?? "–"],
|
||||||
|
["Firmen-E-Mail", employee.company_email ?? "–"],
|
||||||
["Private Telefonnummer", employee.phone ?? "–"],
|
["Private Telefonnummer", employee.phone ?? "–"],
|
||||||
// Der Standort ist keine Angabe zur Person, sondern die Betriebsstätte —
|
// Der Standort ist keine Angabe zur Person, sondern die Betriebsstätte —
|
||||||
// er steht deshalb am Ende und nicht zwischen Adresse und Land, wo man
|
// er steht deshalb am Ende und nicht zwischen Adresse und Land, wo man
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export function HireWizard({ open, onClose, openPositions, locations, resumeDraf
|
|||||||
birth_date: draft.birthDate,
|
birth_date: draft.birthDate,
|
||||||
sv_nummer: draft.svNummer || undefined,
|
sv_nummer: draft.svNummer || undefined,
|
||||||
email: draft.email.trim() || undefined,
|
email: draft.email.trim() || undefined,
|
||||||
|
company_email: draft.companyEmail.trim() || undefined,
|
||||||
phone: draft.phone || undefined,
|
phone: draft.phone || undefined,
|
||||||
position_id: draft.positionId,
|
position_id: draft.positionId,
|
||||||
location_id: draft.locationId,
|
location_id: draft.locationId,
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export function RehireWizard({ open, onClose, employee, openPositions, locations
|
|||||||
birth_date: draft.birthDate,
|
birth_date: draft.birthDate,
|
||||||
sv_nummer: draft.svNummer.trim(),
|
sv_nummer: draft.svNummer.trim(),
|
||||||
email: draft.email.trim(),
|
email: draft.email.trim(),
|
||||||
|
company_email: draft.companyEmail.trim(),
|
||||||
phone: draft.phone.trim(),
|
phone: draft.phone.trim(),
|
||||||
location_id: draft.locationId,
|
location_id: draft.locationId,
|
||||||
// Anschrift, Staatsbürgerschaft und Aufenthaltstitel stehen bewusst
|
// Anschrift, Staatsbürgerschaft und Aufenthaltstitel stehen bewusst
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ export function StepPerson({ draft, update, locations, onNummerBefund, nummerGes
|
|||||||
mit dem Eintritt. Freiwillig: wer keine hat oder keine angeben
|
mit dem Eintritt. Freiwillig: wer keine hat oder keine angeben
|
||||||
will, soll nicht gezwungen sein, eine zu erfinden. */}
|
will, soll nicht gezwungen sein, eine zu erfinden. */}
|
||||||
<TextField label="Private E-Mail" type="email" value={draft.email} onChange={(email) => update({ email })} />
|
<TextField label="Private E-Mail" type="email" value={draft.email} onChange={(email) => update({ email })} />
|
||||||
|
<TextField
|
||||||
|
label="Firmen-E-Mail"
|
||||||
|
type="email"
|
||||||
|
value={draft.companyEmail}
|
||||||
|
onChange={(companyEmail) => update({ companyEmail })}
|
||||||
|
/>
|
||||||
<TextField label="Private Telefonnummer" type="tel" value={draft.phone} onChange={(phone) => update({ phone })} />
|
<TextField label="Private Telefonnummer" type="tel" value={draft.phone} onChange={(phone) => update({ phone })} />
|
||||||
</div>
|
</div>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export type HireDraftData = {
|
|||||||
birthDate: string;
|
birthDate: string;
|
||||||
svNummer: string;
|
svNummer: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
companyEmail: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
locationId: string;
|
locationId: string;
|
||||||
positionId: string;
|
positionId: string;
|
||||||
@@ -82,6 +83,7 @@ export const EMPTY_HIRE_DRAFT: HireDraftData = {
|
|||||||
birthDate: "",
|
birthDate: "",
|
||||||
svNummer: "",
|
svNummer: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
companyEmail: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
locationId: "",
|
locationId: "",
|
||||||
positionId: "",
|
positionId: "",
|
||||||
|
|||||||
849
db/migrations/20260923120000_firmenemail.sql
Normal file
849
db/migrations/20260923120000_firmenemail.sql
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
-- Die Firmen-E-Mail als eigenes, freiwilliges Feld.
|
||||||
|
--
|
||||||
|
-- `employees.email` ist die **private** Adresse (Migration 20260811140000).
|
||||||
|
-- Sie taugt nicht als Dienstadresse und darf auch nicht als solche benutzt
|
||||||
|
-- werden: der Honestly-Export geht an einen fremden Anbieter, der in unserem
|
||||||
|
-- Namen Einladungen verschickt. Die Spalte dort steht deshalb seit jeher
|
||||||
|
-- leer, mit einem Vermerk in lib/honestly.ts, dass die Firmenadresse im
|
||||||
|
-- Datenmodell fehlt. Hier ist sie.
|
||||||
|
--
|
||||||
|
-- Eindeutig, aber freiwillig: eine Dienstadresse gehört genau einer Person,
|
||||||
|
-- und mehrere Personen ohne Adresse stören einen Teilindex nicht, weil null
|
||||||
|
-- in Postgres nie gleich null ist.
|
||||||
|
--
|
||||||
|
-- Die drei Funktionen stehen unverändert aus 20260917120000 hier, ergänzt um
|
||||||
|
-- je eine Zeile für das neue Feld. Sie müssen mit, weil `create or replace`
|
||||||
|
-- die ganze Fassung ersetzt:
|
||||||
|
--
|
||||||
|
-- * app_feld_karte — sonst liesse sich der Eintrag in der
|
||||||
|
-- Historie nicht nachträglich richtigstellen.
|
||||||
|
-- * change_employee_data — sonst liesse sich das Feld gar nicht ändern.
|
||||||
|
-- * apply_due_pending_changes— sonst verfiele eine auf später datierte
|
||||||
|
-- Änderung still: sie stünde in der Historie
|
||||||
|
-- und käme nie in der Zeile an. Genau dieser
|
||||||
|
-- Fehler steckte schon einmal in der Gruppe
|
||||||
|
-- `role` (siehe 20260915120000).
|
||||||
|
--
|
||||||
|
-- Geschrieben wird wie beim Notfallkontakt über `case when ? then nullif`
|
||||||
|
-- und nicht über `coalesce`: eine Dienstadresse muss sich auch wieder
|
||||||
|
-- **entfernen** lassen, etwa wenn jemand das Haus verlässt. Mit `coalesce`
|
||||||
|
-- wäre das Leeren des Feldes nicht ausdrückbar.
|
||||||
|
|
||||||
|
alter table employees add column if not exists company_email text;
|
||||||
|
|
||||||
|
comment on column employees.company_email is
|
||||||
|
'Dienstliche E-Mail-Adresse. Freiwillig. Getrennt von email, das die private Adresse führt.';
|
||||||
|
|
||||||
|
create unique index if not exists employees_company_email_key on employees (company_email);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.hire_employee(payload jsonb)
|
||||||
|
RETURNS uuid
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SET search_path TO 'public', 'pg_temp'
|
||||||
|
AS $function$
|
||||||
|
declare
|
||||||
|
v_id uuid;
|
||||||
|
v_position_id uuid := (payload->>'position_id')::uuid;
|
||||||
|
v_entry date := (payload->>'entry_date')::date;
|
||||||
|
v_besetzt uuid;
|
||||||
|
v_schutz boolean := coalesce((payload->>'has_kuendigungsschutz')::boolean, false);
|
||||||
|
v_behindert boolean := coalesce((payload->>'ist_beguenstigt_behindert')::boolean, false);
|
||||||
|
begin
|
||||||
|
perform require_hr_admin();
|
||||||
|
|
||||||
|
if v_position_id is null then
|
||||||
|
raise exception 'Es muss eine Planstelle angegeben werden.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if payload->>'personnel_number' is null or btrim(payload->>'personnel_number') = '' then
|
||||||
|
raise exception 'Es muss eine Personalnummer angegeben werden.';
|
||||||
|
end if;
|
||||||
|
if exists (select 1 from employees where personnel_number = (payload->>'personnel_number')::int) then
|
||||||
|
raise exception 'Die Personalnummer % ist bereits vergeben.', payload->>'personnel_number';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
declare
|
||||||
|
v_ab date;
|
||||||
|
v_bis date;
|
||||||
|
begin
|
||||||
|
select valid_from, valid_to into v_ab, v_bis from om_positions where id = v_position_id;
|
||||||
|
if v_ab is null then
|
||||||
|
raise exception 'Die Planstelle existiert nicht.';
|
||||||
|
end if;
|
||||||
|
if v_entry < v_ab then
|
||||||
|
raise exception 'Die Planstelle gilt erst ab %. Ein Eintritt am % ist darauf nicht möglich.', v_ab, v_entry;
|
||||||
|
end if;
|
||||||
|
if v_bis is not null and v_entry >= v_bis then
|
||||||
|
raise exception 'Die Planstelle gilt nur bis %. Ein Eintritt am % ist darauf nicht möglich.', v_bis, v_entry;
|
||||||
|
end if;
|
||||||
|
end;
|
||||||
|
|
||||||
|
select pa.employee_id into v_besetzt
|
||||||
|
from position_assignments pa
|
||||||
|
where pa.position_id = v_position_id
|
||||||
|
and (pa.valid_to is null or pa.valid_to > v_entry);
|
||||||
|
if v_besetzt is not null then
|
||||||
|
raise exception 'Diese Planstelle ist bereits besetzt.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
insert into employees (
|
||||||
|
personnel_number, first_name, last_name, gender, birth_date, sv_nummer, nationality, email, company_email, phone,
|
||||||
|
address, postal_code, city, address_country, location_id, job_title,
|
||||||
|
employment_type, weekly_hours, contract_type, contract_end_date, paygrade,
|
||||||
|
source, status, entry_date, title_prefix, title_suffix,
|
||||||
|
worker_type, mitarbeiterart, collective_agreement, work_days,
|
||||||
|
is_betriebsrat, has_dienstwagen, is_laterale_fuehrung, is_c_level,
|
||||||
|
has_kuendigungsschutz, kuendigungsschutz_grund, kuendigungsschutz_ab, kuendigungsschutz_bis,
|
||||||
|
ist_beguenstigt_behindert, behinderung_grad, behinderung_ab, behinderung_bis,
|
||||||
|
hat_aufenthaltstitel, aufenthaltstitel_bis,
|
||||||
|
dienstwagen_art, emergency_contact_name, emergency_contact_phone, emergency_contact_relation
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
(payload->>'personnel_number')::int, payload->>'first_name', payload->>'last_name', (payload->>'gender')::gender_type,
|
||||||
|
(payload->>'birth_date')::date, payload->>'sv_nummer',
|
||||||
|
coalesce(payload->>'nationality', 'Österreich'), payload->>'email', payload->>'company_email', payload->>'phone',
|
||||||
|
payload->>'address', payload->>'postal_code', payload->>'city',
|
||||||
|
coalesce(payload->>'address_country', 'Österreich'),
|
||||||
|
(payload->>'location_id')::uuid,
|
||||||
|
(select j.title from om_positions p join jobs j on j.id = p.job_id where p.id = v_position_id),
|
||||||
|
coalesce((payload->>'employment_type')::employment_type, 'Vollzeit'),
|
||||||
|
coalesce((payload->>'weekly_hours')::numeric, 38.5),
|
||||||
|
coalesce((payload->>'contract_type')::contract_type, 'unbefristet'),
|
||||||
|
nullif(payload->>'contract_end_date', '')::date,
|
||||||
|
coalesce((payload->>'paygrade')::paygrade_type, 'B'),
|
||||||
|
coalesce((payload->>'source')::source_type, 'Extern'),
|
||||||
|
case when v_entry > current_date then 'Geplant' else 'Aktiv' end::employment_status,
|
||||||
|
v_entry,
|
||||||
|
coalesce(array(select jsonb_array_elements_text(payload->'title_prefix')), '{}'),
|
||||||
|
coalesce(array(select jsonb_array_elements_text(payload->'title_suffix')), '{}'),
|
||||||
|
coalesce((payload->>'worker_type')::worker_type, 'Angestellte:r'),
|
||||||
|
coalesce(nullif(payload->>'mitarbeiterart', ''), 'Standard'),
|
||||||
|
coalesce((payload->>'collective_agreement')::collective_agreement, 'Süßwaren'),
|
||||||
|
coalesce(nullif(array(select jsonb_array_elements_text(payload->'work_days'))::text[], '{}'), '{Mo,Di,Mi,Do,Fr}'),
|
||||||
|
coalesce((payload->>'is_betriebsrat')::boolean, false),
|
||||||
|
coalesce((payload->>'has_dienstwagen')::boolean, false),
|
||||||
|
coalesce((payload->>'is_laterale_fuehrung')::boolean, false),
|
||||||
|
coalesce((payload->>'is_c_level')::boolean, false),
|
||||||
|
v_schutz,
|
||||||
|
-- Grund, Beginn und Ende nur, wenn der Schutz überhaupt gesetzt ist:
|
||||||
|
-- sonst bliebe ein Rest ohne Bezug stehen, und die Bedingungen wiesen
|
||||||
|
-- den ganzen Vorgang zurück.
|
||||||
|
case when v_schutz then nullif(payload->>'kuendigungsschutz_grund', '') else null end,
|
||||||
|
case when v_schutz then nullif(payload->>'kuendigungsschutz_ab', '')::date else null end,
|
||||||
|
case when v_schutz then nullif(payload->>'kuendigungsschutz_bis', '')::date else null end,
|
||||||
|
v_behindert,
|
||||||
|
case when v_behindert then nullif(payload->>'behinderung_grad', '')::integer else null end,
|
||||||
|
case when v_behindert then nullif(payload->>'behinderung_ab', '')::date else null end,
|
||||||
|
case when v_behindert then nullif(payload->>'behinderung_bis', '')::date else null end,
|
||||||
|
coalesce((payload->>'hat_aufenthaltstitel')::boolean, false),
|
||||||
|
case when coalesce((payload->>'hat_aufenthaltstitel')::boolean, false)
|
||||||
|
then nullif(payload->>'aufenthaltstitel_bis', '')::date else null end,
|
||||||
|
case when coalesce((payload->>'has_dienstwagen')::boolean, false) then coalesce(nullif(payload->>'dienstwagen_art', ''), 'Verbrenner') else null end,
|
||||||
|
nullif(payload->>'emergency_contact_name', ''),
|
||||||
|
nullif(payload->>'emergency_contact_phone', ''),
|
||||||
|
nullif(payload->>'emergency_contact_relation', '')
|
||||||
|
)
|
||||||
|
returning id into v_id;
|
||||||
|
|
||||||
|
insert into position_assignments (position_id, employee_id, valid_from)
|
||||||
|
values (v_position_id, v_id, v_entry);
|
||||||
|
|
||||||
|
insert into employee_history (employee_id, event_date, event_type, description)
|
||||||
|
values (v_id, v_entry, 'Eintritt', 'Eintritt auf Planstelle ' ||
|
||||||
|
(select position_number from om_positions where id = v_position_id));
|
||||||
|
|
||||||
|
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
||||||
|
values (app_current_user_id(), current_actor_name(), 'Neueinstellung',
|
||||||
|
(payload->>'first_name') || ' ' || (payload->>'last_name'), v_id, 'Eintritt am ' || v_entry);
|
||||||
|
|
||||||
|
return v_id;
|
||||||
|
end;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
grant execute on function public.hire_employee(payload jsonb) to alpenwerk_app;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.rehire_employee(payload jsonb)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SET search_path TO 'public', 'pg_temp'
|
||||||
|
AS $function$
|
||||||
|
declare
|
||||||
|
v_employee_id uuid := (payload->>'employee_id')::uuid;
|
||||||
|
v_date date := (payload->>'rehire_date')::date;
|
||||||
|
v_position_id uuid := (payload->>'position_id')::uuid;
|
||||||
|
v_name text;
|
||||||
|
v_alt employees%rowtype;
|
||||||
|
v_besetzt uuid;
|
||||||
|
v_schutz boolean;
|
||||||
|
v_behindert boolean;
|
||||||
|
begin
|
||||||
|
perform require_hr_admin();
|
||||||
|
|
||||||
|
select * into v_alt from employees where id = v_employee_id;
|
||||||
|
if v_alt.id is null then
|
||||||
|
raise exception 'Die Person existiert nicht.';
|
||||||
|
end if;
|
||||||
|
v_name := v_alt.first_name || ' ' || v_alt.last_name;
|
||||||
|
|
||||||
|
if v_position_id is null then
|
||||||
|
raise exception 'Für die Wiedereinstellung muss eine Planstelle angegeben werden.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Dieselben Prüfungen wie bei einer Neueinstellung. Sie fehlten hier, und
|
||||||
|
-- das hiess: eine Wiedereinstellung auf eine besetzte oder noch nicht
|
||||||
|
-- gültige Planstelle scheiterte erst an einem Index oder gar nicht.
|
||||||
|
declare
|
||||||
|
v_ab date;
|
||||||
|
v_bis date;
|
||||||
|
begin
|
||||||
|
select valid_from, valid_to into v_ab, v_bis from om_positions where id = v_position_id;
|
||||||
|
if v_ab is null then
|
||||||
|
raise exception 'Die Planstelle existiert nicht.';
|
||||||
|
end if;
|
||||||
|
if v_date < v_ab then
|
||||||
|
raise exception 'Die Planstelle gilt erst ab %. Ein Eintritt am % ist darauf nicht möglich.', v_ab, v_date;
|
||||||
|
end if;
|
||||||
|
if v_bis is not null and v_date >= v_bis then
|
||||||
|
raise exception 'Die Planstelle gilt nur bis %. Ein Eintritt am % ist darauf nicht möglich.', v_bis, v_date;
|
||||||
|
end if;
|
||||||
|
end;
|
||||||
|
|
||||||
|
select pa.employee_id into v_besetzt
|
||||||
|
from position_assignments pa
|
||||||
|
where pa.position_id = v_position_id
|
||||||
|
and (pa.valid_to is null or pa.valid_to > v_date);
|
||||||
|
if v_besetzt is not null then
|
||||||
|
raise exception 'Diese Planstelle ist bereits besetzt.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Die beiden Kennzeichen zuerst: an ihnen hängen Felder, die ohne sie nicht
|
||||||
|
-- dastehen dürfen (chk_kuendigungsschutz_grund, chk_behinderung).
|
||||||
|
v_schutz := coalesce((payload->>'has_kuendigungsschutz')::boolean, v_alt.has_kuendigungsschutz);
|
||||||
|
v_behindert := coalesce((payload->>'ist_beguenstigt_behindert')::boolean, v_alt.ist_beguenstigt_behindert);
|
||||||
|
|
||||||
|
update employees set
|
||||||
|
status = case when v_date <= current_date then 'Aktiv' else 'Geplant' end,
|
||||||
|
entry_date = v_date,
|
||||||
|
exit_date = null,
|
||||||
|
exit_reason = null,
|
||||||
|
austrittsart = null,
|
||||||
|
|
||||||
|
first_name = coalesce(payload->>'first_name', first_name),
|
||||||
|
last_name = coalesce(payload->>'last_name', last_name),
|
||||||
|
gender = coalesce((payload->>'gender')::gender_type, gender),
|
||||||
|
birth_date = coalesce((payload->>'birth_date')::date, birth_date),
|
||||||
|
sv_nummer = case when payload ? 'sv_nummer' then nullif(payload->>'sv_nummer', '') else sv_nummer end,
|
||||||
|
nationality = coalesce(payload->>'nationality', nationality),
|
||||||
|
email = case when payload ? 'email' then nullif(payload->>'email', '') else email end,
|
||||||
|
company_email = case when payload ? 'company_email' then nullif(payload->>'company_email', '') else company_email end,
|
||||||
|
phone = case when payload ? 'phone' then nullif(payload->>'phone', '') else phone end,
|
||||||
|
address = coalesce(payload->>'address', address),
|
||||||
|
postal_code = coalesce(payload->>'postal_code', postal_code),
|
||||||
|
city = coalesce(payload->>'city', city),
|
||||||
|
address_country = coalesce(payload->>'address_country', address_country),
|
||||||
|
location_id = coalesce((payload->>'location_id')::uuid, location_id),
|
||||||
|
title_prefix = case when payload ? 'title_prefix'
|
||||||
|
then coalesce(array(select jsonb_array_elements_text(payload->'title_prefix')), '{}')
|
||||||
|
else title_prefix end,
|
||||||
|
title_suffix = case when payload ? 'title_suffix'
|
||||||
|
then coalesce(array(select jsonb_array_elements_text(payload->'title_suffix')), '{}')
|
||||||
|
else title_suffix end,
|
||||||
|
emergency_contact_name = case when payload ? 'emergency_contact_name' then nullif(payload->>'emergency_contact_name', '') else emergency_contact_name end,
|
||||||
|
emergency_contact_phone = case when payload ? 'emergency_contact_phone' then nullif(payload->>'emergency_contact_phone', '') else emergency_contact_phone end,
|
||||||
|
emergency_contact_relation = case when payload ? 'emergency_contact_relation' then nullif(payload->>'emergency_contact_relation', '') else emergency_contact_relation end,
|
||||||
|
hat_aufenthaltstitel = coalesce((payload->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel),
|
||||||
|
aufenthaltstitel_bis = case
|
||||||
|
when coalesce((payload->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel)
|
||||||
|
then case when payload ? 'aufenthaltstitel_bis' then nullif(payload->>'aufenthaltstitel_bis', '')::date else aufenthaltstitel_bis end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- Die Tätigkeit kommt von der Planstelle, nicht aus dem payload: sie
|
||||||
|
-- steht am Platz, den die Person einnimmt, und nicht an ihr.
|
||||||
|
job_title = (select j.title from om_positions p join jobs j on j.id = p.job_id where p.id = v_position_id),
|
||||||
|
employment_type = coalesce((payload->>'employment_type')::employment_type, employment_type),
|
||||||
|
weekly_hours = coalesce((payload->>'weekly_hours')::numeric, weekly_hours),
|
||||||
|
contract_type = coalesce((payload->>'contract_type')::contract_type, contract_type),
|
||||||
|
contract_end_date = case when payload ? 'contract_end_date' then nullif(payload->>'contract_end_date', '')::date else contract_end_date end,
|
||||||
|
paygrade = coalesce((payload->>'paygrade')::paygrade_type, paygrade),
|
||||||
|
source = coalesce((payload->>'source')::source_type, source),
|
||||||
|
|
||||||
|
worker_type = coalesce((payload->>'worker_type')::worker_type, worker_type),
|
||||||
|
mitarbeiterart = coalesce(nullif(payload->>'mitarbeiterart', ''), mitarbeiterart),
|
||||||
|
collective_agreement = coalesce((payload->>'collective_agreement')::collective_agreement, collective_agreement),
|
||||||
|
work_days = case when payload ? 'work_days'
|
||||||
|
then coalesce(nullif(array(select jsonb_array_elements_text(payload->'work_days'))::text[], '{}'), work_days)
|
||||||
|
else work_days end,
|
||||||
|
is_betriebsrat = coalesce((payload->>'is_betriebsrat')::boolean, is_betriebsrat),
|
||||||
|
has_dienstwagen = coalesce((payload->>'has_dienstwagen')::boolean, has_dienstwagen),
|
||||||
|
dienstwagen_art = case
|
||||||
|
when coalesce((payload->>'has_dienstwagen')::boolean, has_dienstwagen)
|
||||||
|
then coalesce(nullif(payload->>'dienstwagen_art', ''), dienstwagen_art, 'Verbrenner')
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
is_laterale_fuehrung = coalesce((payload->>'is_laterale_fuehrung')::boolean, is_laterale_fuehrung),
|
||||||
|
is_c_level = coalesce((payload->>'is_c_level')::boolean, is_c_level),
|
||||||
|
|
||||||
|
has_kuendigungsschutz = v_schutz,
|
||||||
|
kuendigungsschutz_grund = case when v_schutz then
|
||||||
|
case when payload ? 'kuendigungsschutz_grund' then nullif(payload->>'kuendigungsschutz_grund', '') else kuendigungsschutz_grund end
|
||||||
|
else null end,
|
||||||
|
kuendigungsschutz_ab = case when v_schutz then
|
||||||
|
case when payload ? 'kuendigungsschutz_ab' then nullif(payload->>'kuendigungsschutz_ab', '')::date else kuendigungsschutz_ab end
|
||||||
|
else null end,
|
||||||
|
kuendigungsschutz_bis = case when v_schutz then
|
||||||
|
case when payload ? 'kuendigungsschutz_bis' then nullif(payload->>'kuendigungsschutz_bis', '')::date else kuendigungsschutz_bis end
|
||||||
|
else null end,
|
||||||
|
|
||||||
|
ist_beguenstigt_behindert = v_behindert,
|
||||||
|
behinderung_grad = case when v_behindert then
|
||||||
|
case when payload ? 'behinderung_grad' then nullif(payload->>'behinderung_grad', '')::integer else behinderung_grad end
|
||||||
|
else null end,
|
||||||
|
behinderung_ab = case when v_behindert then
|
||||||
|
case when payload ? 'behinderung_ab' then nullif(payload->>'behinderung_ab', '')::date else behinderung_ab end
|
||||||
|
else null end,
|
||||||
|
behinderung_bis = case when v_behindert then
|
||||||
|
case when payload ? 'behinderung_bis' then nullif(payload->>'behinderung_bis', '')::date else behinderung_bis end
|
||||||
|
else null end
|
||||||
|
where id = v_employee_id;
|
||||||
|
|
||||||
|
insert into position_assignments (position_id, employee_id, valid_from)
|
||||||
|
values (v_position_id, v_employee_id, v_date);
|
||||||
|
|
||||||
|
insert into employee_history (employee_id, event_date, event_type, description)
|
||||||
|
values (v_employee_id, v_date, 'Wiedereintritt',
|
||||||
|
'Wiedereinstellung zum ' || v_date || ' auf Planstelle ' ||
|
||||||
|
(select position_number from om_positions where id = v_position_id));
|
||||||
|
|
||||||
|
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
||||||
|
values (app_current_user_id(), current_actor_name(), 'Wiedereinstellung', v_name, v_employee_id,
|
||||||
|
'Wiedereintritt am ' || v_date);
|
||||||
|
end;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
grant execute on function public.rehire_employee(payload jsonb) to alpenwerk_app;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.app_feld_karte()
|
||||||
|
RETURNS jsonb
|
||||||
|
LANGUAGE sql
|
||||||
|
IMMUTABLE
|
||||||
|
SET search_path TO 'public', 'pg_temp'
|
||||||
|
AS $function$
|
||||||
|
select jsonb_build_object(
|
||||||
|
'Vorname', jsonb_build_array('first_name', 'text', 'person'),
|
||||||
|
'Nachname', jsonb_build_array('last_name', 'text', 'person'),
|
||||||
|
'Geschlecht', jsonb_build_array('gender', 'gender_type', 'person'),
|
||||||
|
'Geburtsdatum', jsonb_build_array('birth_date', 'date', 'person'),
|
||||||
|
'SV-Nummer', jsonb_build_array('sv_nummer', 'text', 'person'),
|
||||||
|
'Staatsbürgerschaft', jsonb_build_array('nationality', 'text', 'person'),
|
||||||
|
'Adresse', jsonb_build_array('address', 'text', 'person'),
|
||||||
|
'Postleitzahl', jsonb_build_array('postal_code', 'text', 'person'),
|
||||||
|
'Ort', jsonb_build_array('city', 'text', 'person'),
|
||||||
|
'Land', jsonb_build_array('address_country', 'text', 'person'),
|
||||||
|
'E-Mail', jsonb_build_array('email', 'text', 'person'),
|
||||||
|
'Firmen-E-Mail', jsonb_build_array('company_email', 'text', 'person'),
|
||||||
|
'Telefon', jsonb_build_array('phone', 'text', 'person'),
|
||||||
|
'Notfallkontakt', jsonb_build_array('emergency_contact_name', 'text', 'person'),
|
||||||
|
'Notfallkontakt Telefon', jsonb_build_array('emergency_contact_phone', 'text', 'person'),
|
||||||
|
'Notfallkontakt Verhältnis', jsonb_build_array('emergency_contact_relation', 'text', 'person'),
|
||||||
|
'Aufenthaltstitel', jsonb_build_array('hat_aufenthaltstitel', 'boolean', 'person'),
|
||||||
|
'Aufenthaltstitel bis', jsonb_build_array('aufenthaltstitel_bis', 'date', 'person'),
|
||||||
|
'Titel (vorangestellt)', jsonb_build_array('title_prefix', 'liste', 'person'),
|
||||||
|
'Titel (nachgestellt)', jsonb_build_array('title_suffix', 'liste', 'person'),
|
||||||
|
'Beschäftigungsausmaß', jsonb_build_array('employment_type', 'employment_type', 'contract'),
|
||||||
|
'Wochenstunden', jsonb_build_array('weekly_hours', 'numeric', 'contract'),
|
||||||
|
'Vertragsart', jsonb_build_array('contract_type', 'contract_type', 'contract'),
|
||||||
|
'Befristet bis', jsonb_build_array('contract_end_date', 'date', 'contract'),
|
||||||
|
'Angestellte:r/Arbeiter:in', jsonb_build_array('worker_type', 'worker_type', 'role'),
|
||||||
|
'Mitarbeiterart', jsonb_build_array('mitarbeiterart', 'text', 'role'),
|
||||||
|
'Besetzungsart', jsonb_build_array('source', 'source_type', 'role'),
|
||||||
|
'Kollektivvertrag', jsonb_build_array('collective_agreement', 'collective_agreement', 'role'),
|
||||||
|
'Arbeitstage', jsonb_build_array('work_days', 'liste', 'role'),
|
||||||
|
'Betriebsrat', jsonb_build_array('is_betriebsrat', 'boolean', 'role'),
|
||||||
|
'Dienstwagen', jsonb_build_array('has_dienstwagen', 'boolean', 'role'),
|
||||||
|
'Laterale Führung', jsonb_build_array('is_laterale_fuehrung', 'boolean', 'role'),
|
||||||
|
'C-Level', jsonb_build_array('is_c_level', 'boolean', 'role'),
|
||||||
|
'Dienstwagen Antrieb', jsonb_build_array('dienstwagen_art', 'text', 'role'),
|
||||||
|
'Besonderer Kündigungsschutz', jsonb_build_array('has_kuendigungsschutz', 'boolean', 'role'),
|
||||||
|
'Kündigungsschutz Personenkreis', jsonb_build_array('kuendigungsschutz_grund', 'text', 'role'),
|
||||||
|
'Kündigungsschutz ab', jsonb_build_array('kuendigungsschutz_ab', 'date', 'role'),
|
||||||
|
'Kündigungsschutz bis', jsonb_build_array('kuendigungsschutz_bis', 'date', 'role'),
|
||||||
|
'Begünstigt behindert', jsonb_build_array('ist_beguenstigt_behindert', 'boolean', 'role'),
|
||||||
|
-- 'integer' und nicht 'numeric': der Typ geht in update_history_entry
|
||||||
|
-- unmittelbar als Umwandlung ins SQL (`%L::%s`). 'numeric' liefe über
|
||||||
|
-- eine zweite, stillschweigende Umwandlung auf die Spalte und rundete
|
||||||
|
-- dabei — der Grad ist eine ganze Zahl aus einem Bescheid.
|
||||||
|
'Grad der Behinderung', jsonb_build_array('behinderung_grad', 'integer', 'role'),
|
||||||
|
'Behinderung ab', jsonb_build_array('behinderung_ab', 'date', 'role'),
|
||||||
|
'Behinderung bis', jsonb_build_array('behinderung_bis', 'date', 'role'),
|
||||||
|
'Teilzeitvariante', jsonb_build_array('teilzeit_art', 'text', 'role'),
|
||||||
|
'Teilzeit bis', jsonb_build_array('teilzeit_bis', 'date', 'role'),
|
||||||
|
-- Die Felder der Abwesenheit. Dritter Eintrag null: sie gehören zu
|
||||||
|
-- keiner Gruppe im payload einer geplanten Änderung, weil Abwesenheit
|
||||||
|
-- und Rückkehr ihre eigenen Vorgänge haben.
|
||||||
|
'Status', jsonb_build_array('status', 'employment_status', null),
|
||||||
|
'Art der Abwesenheit', jsonb_build_array('absence_type', 'text', null),
|
||||||
|
'Abwesend ab', jsonb_build_array('karenz_start_date', 'date', null),
|
||||||
|
'Geplante Rückkehr', jsonb_build_array('karenz_return_date', 'date', null)
|
||||||
|
);
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.change_employee_data(payload jsonb)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SET search_path TO 'public', 'pg_temp'
|
||||||
|
AS $function$
|
||||||
|
declare
|
||||||
|
v_employee_id uuid := (payload->>'employee_id')::uuid;
|
||||||
|
v_effective_date date := coalesce(nullif(payload->>'effective_date', '')::date, current_date);
|
||||||
|
v_old employees%rowtype;
|
||||||
|
v_name text;
|
||||||
|
v_person_changes jsonb := '[]'::jsonb;
|
||||||
|
v_contract_changes jsonb := '[]'::jsonb;
|
||||||
|
v_person jsonb := payload->'person';
|
||||||
|
v_contract jsonb := payload->'contract';
|
||||||
|
v_role jsonb := payload->'role';
|
||||||
|
v_immediate boolean;
|
||||||
|
v_new_work_days text[];
|
||||||
|
v_new_title_prefix text[];
|
||||||
|
v_new_title_suffix text[];
|
||||||
|
v_pending_id uuid;
|
||||||
|
begin
|
||||||
|
perform require_hr_admin();
|
||||||
|
select * into v_old from employees where id = v_employee_id;
|
||||||
|
v_name := v_old.first_name || ' ' || v_old.last_name;
|
||||||
|
v_immediate := v_effective_date <= current_date;
|
||||||
|
|
||||||
|
-- Der `?`-Test bleibt: ein fehlender Schlüssel heisst „nicht übermittelt",
|
||||||
|
-- nicht „geleert". Ohne ihn würde jedes nicht gesendete Feld als Änderung
|
||||||
|
-- auf null gemeldet.
|
||||||
|
if v_person ? 'first_name' then v_person_changes := app_aenderung(v_person_changes, 'Vorname', v_old.first_name, v_person->>'first_name'); end if;
|
||||||
|
if v_person ? 'last_name' then v_person_changes := app_aenderung(v_person_changes, 'Nachname', v_old.last_name, v_person->>'last_name'); end if;
|
||||||
|
if v_person ? 'gender' then v_person_changes := app_aenderung(v_person_changes, 'Geschlecht', v_old.gender::text, v_person->>'gender'); end if;
|
||||||
|
-- Datumswerte über ::date::text vergleichen, damit „2026-8-3" und
|
||||||
|
-- „2026-08-03" nicht als Änderung gelten.
|
||||||
|
if v_person ? 'birth_date' then v_person_changes := app_aenderung(v_person_changes, 'Geburtsdatum', v_old.birth_date::text, (nullif(v_person->>'birth_date','')::date)::text); end if;
|
||||||
|
if v_person ? 'sv_nummer' then v_person_changes := app_aenderung(v_person_changes, 'SV-Nummer', v_old.sv_nummer, v_person->>'sv_nummer'); end if;
|
||||||
|
if v_person ? 'nationality' then v_person_changes := app_aenderung(v_person_changes, 'Staatsbürgerschaft', v_old.nationality, v_person->>'nationality'); end if;
|
||||||
|
if v_person ? 'address' then v_person_changes := app_aenderung(v_person_changes, 'Adresse', v_old.address, v_person->>'address'); end if;
|
||||||
|
if v_person ? 'postal_code' then v_person_changes := app_aenderung(v_person_changes, 'Postleitzahl', v_old.postal_code, v_person->>'postal_code'); end if;
|
||||||
|
if v_person ? 'city' then v_person_changes := app_aenderung(v_person_changes, 'Ort', v_old.city, v_person->>'city'); end if;
|
||||||
|
if v_person ? 'address_country' then v_person_changes := app_aenderung(v_person_changes, 'Land', v_old.address_country, v_person->>'address_country'); end if;
|
||||||
|
if v_person ? 'email' then v_person_changes := app_aenderung(v_person_changes, 'E-Mail', v_old.email, v_person->>'email'); end if;
|
||||||
|
if v_person ? 'phone' then v_person_changes := app_aenderung(v_person_changes, 'Telefon', v_old.phone, v_person->>'phone'); end if;
|
||||||
|
if v_person ? 'company_email' then v_person_changes := app_aenderung(v_person_changes, 'Firmen-E-Mail', v_old.company_email, v_person->>'company_email'); end if;
|
||||||
|
if v_person ? 'emergency_contact_name' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt', v_old.emergency_contact_name, v_person->>'emergency_contact_name'); end if;
|
||||||
|
if v_person ? 'emergency_contact_phone' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt Telefon', v_old.emergency_contact_phone, v_person->>'emergency_contact_phone'); end if;
|
||||||
|
if v_person ? 'emergency_contact_relation' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt Verhältnis', v_old.emergency_contact_relation, v_person->>'emergency_contact_relation'); end if;
|
||||||
|
if v_person ? 'hat_aufenthaltstitel' then v_person_changes := app_aenderung(v_person_changes, 'Aufenthaltstitel', v_old.hat_aufenthaltstitel::text, v_person->>'hat_aufenthaltstitel'); end if;
|
||||||
|
if v_person ? 'aufenthaltstitel_bis' then v_person_changes := app_aenderung(v_person_changes, 'Aufenthaltstitel bis', v_old.aufenthaltstitel_bis::text, (nullif(v_person->>'aufenthaltstitel_bis','')::date)::text); end if;
|
||||||
|
|
||||||
|
if v_person ? 'title_prefix' then
|
||||||
|
v_new_title_prefix := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_person->'title_prefix') elem), '{}');
|
||||||
|
v_person_changes := app_aenderung(v_person_changes, 'Titel (vorangestellt)',
|
||||||
|
array_to_string(v_old.title_prefix, ', '), array_to_string(v_new_title_prefix, ', '));
|
||||||
|
end if;
|
||||||
|
if v_person ? 'title_suffix' then
|
||||||
|
v_new_title_suffix := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_person->'title_suffix') elem), '{}');
|
||||||
|
v_person_changes := app_aenderung(v_person_changes, 'Titel (nachgestellt)',
|
||||||
|
array_to_string(v_old.title_suffix, ', '), array_to_string(v_new_title_suffix, ', '));
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if v_contract ? 'employment_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Beschäftigungsausmaß', v_old.employment_type::text, v_contract->>'employment_type'); end if;
|
||||||
|
-- Über ::numeric::text, damit „38.50" und „38.5" gleich zählen.
|
||||||
|
if v_contract ? 'weekly_hours' then v_contract_changes := app_aenderung(v_contract_changes, 'Wochenstunden', v_old.weekly_hours::text, (nullif(v_contract->>'weekly_hours','')::numeric)::text); end if;
|
||||||
|
if v_contract ? 'contract_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Vertragsart', v_old.contract_type::text, v_contract->>'contract_type'); end if;
|
||||||
|
if v_contract ? 'contract_end_date' then v_contract_changes := app_aenderung(v_contract_changes, 'Befristet bis', v_old.contract_end_date::text, (nullif(v_contract->>'contract_end_date','')::date)::text); end if;
|
||||||
|
|
||||||
|
if v_role ? 'worker_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Angestellte:r/Arbeiter:in', v_old.worker_type::text, v_role->>'worker_type'); end if;
|
||||||
|
if v_role ? 'mitarbeiterart' then v_contract_changes := app_aenderung(v_contract_changes, 'Mitarbeiterart', v_old.mitarbeiterart, v_role->>'mitarbeiterart'); end if;
|
||||||
|
if v_role ? 'source' then v_contract_changes := app_aenderung(v_contract_changes, 'Besetzungsart', v_old.source::text, v_role->>'source'); end if;
|
||||||
|
if v_role ? 'collective_agreement' then v_contract_changes := app_aenderung(v_contract_changes, 'Kollektivvertrag', v_old.collective_agreement::text, v_role->>'collective_agreement'); end if;
|
||||||
|
if v_role ? 'work_days' then
|
||||||
|
v_new_work_days := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_role->'work_days') elem), '{}');
|
||||||
|
v_contract_changes := app_aenderung(v_contract_changes, 'Arbeitstage',
|
||||||
|
array_to_string(v_old.work_days, ', '), array_to_string(v_new_work_days, ', '));
|
||||||
|
end if;
|
||||||
|
if v_role ? 'is_betriebsrat' then v_contract_changes := app_aenderung(v_contract_changes, 'Betriebsrat', v_old.is_betriebsrat::text, v_role->>'is_betriebsrat'); end if;
|
||||||
|
if v_role ? 'has_dienstwagen' then v_contract_changes := app_aenderung(v_contract_changes, 'Dienstwagen', v_old.has_dienstwagen::text, v_role->>'has_dienstwagen'); end if;
|
||||||
|
if v_role ? 'is_laterale_fuehrung' then v_contract_changes := app_aenderung(v_contract_changes, 'Laterale Führung', v_old.is_laterale_fuehrung::text, v_role->>'is_laterale_fuehrung'); end if;
|
||||||
|
if v_role ? 'is_c_level' then v_contract_changes := app_aenderung(v_contract_changes, 'C-Level', v_old.is_c_level::text, v_role->>'is_c_level'); end if;
|
||||||
|
if v_role ? 'has_kuendigungsschutz' then v_contract_changes := app_aenderung(v_contract_changes, 'Besonderer Kündigungsschutz', v_old.has_kuendigungsschutz::text, v_role->>'has_kuendigungsschutz'); end if;
|
||||||
|
if v_role ? 'kuendigungsschutz_grund' then v_contract_changes := app_aenderung(v_contract_changes, 'Kündigungsschutz Personenkreis', v_old.kuendigungsschutz_grund, nullif(v_role->>'kuendigungsschutz_grund', '')); end if;
|
||||||
|
if v_role ? 'kuendigungsschutz_ab' then v_contract_changes := app_aenderung(v_contract_changes, 'Kündigungsschutz ab', v_old.kuendigungsschutz_ab::text, (nullif(v_role->>'kuendigungsschutz_ab','')::date)::text); end if;
|
||||||
|
if v_role ? 'kuendigungsschutz_bis' then v_contract_changes := app_aenderung(v_contract_changes, 'Kündigungsschutz bis', v_old.kuendigungsschutz_bis::text, (nullif(v_role->>'kuendigungsschutz_bis','')::date)::text); end if;
|
||||||
|
if v_role ? 'ist_beguenstigt_behindert' then v_contract_changes := app_aenderung(v_contract_changes, 'Begünstigt behindert', v_old.ist_beguenstigt_behindert::text, v_role->>'ist_beguenstigt_behindert'); end if;
|
||||||
|
if v_role ? 'behinderung_grad' then v_contract_changes := app_aenderung(v_contract_changes, 'Grad der Behinderung', v_old.behinderung_grad::text, (nullif(v_role->>'behinderung_grad','')::integer)::text); end if;
|
||||||
|
if v_role ? 'behinderung_ab' then v_contract_changes := app_aenderung(v_contract_changes, 'Behinderung ab', v_old.behinderung_ab::text, (nullif(v_role->>'behinderung_ab','')::date)::text); end if;
|
||||||
|
if v_role ? 'behinderung_bis' then v_contract_changes := app_aenderung(v_contract_changes, 'Behinderung bis', v_old.behinderung_bis::text, (nullif(v_role->>'behinderung_bis','')::date)::text); end if;
|
||||||
|
if v_role ? 'teilzeit_art' then v_contract_changes := app_aenderung(v_contract_changes, 'Teilzeitvariante', v_old.teilzeit_art, nullif(v_role->>'teilzeit_art', '')); end if;
|
||||||
|
if v_role ? 'teilzeit_bis' then v_contract_changes := app_aenderung(v_contract_changes, 'Teilzeit bis', v_old.teilzeit_bis::text, (nullif(v_role->>'teilzeit_bis','')::date)::text); end if;
|
||||||
|
if v_role ? 'dienstwagen_art' then v_contract_changes := app_aenderung(v_contract_changes, 'Dienstwagen Antrieb', v_old.dienstwagen_art, nullif(v_role->>'dienstwagen_art', '')); end if;
|
||||||
|
|
||||||
|
if v_immediate then
|
||||||
|
update employees set
|
||||||
|
first_name = coalesce(v_person->>'first_name', first_name),
|
||||||
|
last_name = coalesce(v_person->>'last_name', last_name),
|
||||||
|
gender = coalesce((v_person->>'gender')::gender_type, gender),
|
||||||
|
birth_date = coalesce((v_person->>'birth_date')::date, birth_date),
|
||||||
|
sv_nummer = coalesce(v_person->>'sv_nummer', sv_nummer),
|
||||||
|
nationality = coalesce(v_person->>'nationality', nationality),
|
||||||
|
address = coalesce(v_person->>'address', address),
|
||||||
|
postal_code = coalesce(v_person->>'postal_code', postal_code),
|
||||||
|
city = coalesce(v_person->>'city', city),
|
||||||
|
address_country = coalesce(v_person->>'address_country', address_country),
|
||||||
|
email = coalesce(v_person->>'email', email),
|
||||||
|
phone = coalesce(v_person->>'phone', phone),
|
||||||
|
company_email = case when v_person ? 'company_email' then nullif(v_person->>'company_email', '') else company_email end,
|
||||||
|
emergency_contact_name = case when v_person ? 'emergency_contact_name' then nullif(v_person->>'emergency_contact_name', '') else emergency_contact_name end,
|
||||||
|
emergency_contact_phone = case when v_person ? 'emergency_contact_phone' then nullif(v_person->>'emergency_contact_phone', '') else emergency_contact_phone end,
|
||||||
|
emergency_contact_relation = case when v_person ? 'emergency_contact_relation' then nullif(v_person->>'emergency_contact_relation', '') else emergency_contact_relation end,
|
||||||
|
hat_aufenthaltstitel = coalesce((v_person->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel),
|
||||||
|
-- Fällt der Titel weg, fällt das Datum mit. Sonst bliebe ein
|
||||||
|
-- Enddatum ohne Titel stehen, und chk_aufenthaltstitel_bis liesse
|
||||||
|
-- die ganze Änderung scheitern statt das Offensichtliche zu tun.
|
||||||
|
aufenthaltstitel_bis = case
|
||||||
|
when coalesce((v_person->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel) then
|
||||||
|
case when v_person ? 'aufenthaltstitel_bis'
|
||||||
|
then nullif(v_person->>'aufenthaltstitel_bis','')::date
|
||||||
|
else aufenthaltstitel_bis end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
title_prefix = case when v_person ? 'title_prefix' then v_new_title_prefix else title_prefix end,
|
||||||
|
title_suffix = case when v_person ? 'title_suffix' then v_new_title_suffix else title_suffix end,
|
||||||
|
employment_type = coalesce((v_contract->>'employment_type')::employment_type, employment_type),
|
||||||
|
weekly_hours = coalesce((v_contract->>'weekly_hours')::numeric, weekly_hours),
|
||||||
|
contract_type = coalesce((v_contract->>'contract_type')::contract_type, contract_type),
|
||||||
|
contract_end_date = case when v_contract ? 'contract_end_date' then nullif(v_contract->>'contract_end_date','')::date else contract_end_date end,
|
||||||
|
worker_type = coalesce((v_role->>'worker_type')::worker_type, worker_type),
|
||||||
|
-- Ohne nullif: die Spalte ist NOT NULL, ein leerer Text waere kein
|
||||||
|
-- geloeschter Wert, sondern ein Wert, den chk_mitarbeiterart abweist.
|
||||||
|
mitarbeiterart = coalesce(nullif(v_role->>'mitarbeiterart', ''), mitarbeiterart),
|
||||||
|
source = coalesce((nullif(v_role->>'source', ''))::source_type, source),
|
||||||
|
collective_agreement = coalesce((v_role->>'collective_agreement')::collective_agreement, collective_agreement),
|
||||||
|
work_days = case when v_role ? 'work_days' then v_new_work_days else work_days end,
|
||||||
|
is_betriebsrat = coalesce((v_role->>'is_betriebsrat')::boolean, is_betriebsrat),
|
||||||
|
has_dienstwagen = coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen),
|
||||||
|
is_laterale_fuehrung = coalesce((v_role->>'is_laterale_fuehrung')::boolean, is_laterale_fuehrung),
|
||||||
|
is_c_level = coalesce((v_role->>'is_c_level')::boolean, is_c_level),
|
||||||
|
has_kuendigungsschutz = coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz),
|
||||||
|
teilzeit_art = case when v_role ? 'teilzeit_art' then nullif(v_role->>'teilzeit_art', '') else teilzeit_art end,
|
||||||
|
-- Ohne Variante kein Enddatum: chk_teilzeit_bis verlangt es so, und
|
||||||
|
-- ein Datum ohne Sache wäre ein Rest, den niemand mehr deutet.
|
||||||
|
teilzeit_bis = case
|
||||||
|
when coalesce(nullif(v_role->>'teilzeit_art', ''), case when v_role ? 'teilzeit_art' then null else teilzeit_art end) is null then null
|
||||||
|
when v_role ? 'teilzeit_bis' then nullif(v_role->>'teilzeit_bis','')::date
|
||||||
|
else teilzeit_bis
|
||||||
|
end,
|
||||||
|
-- Fällt der Schutz weg, fallen Grund und beide Daten mit. Andernfalls
|
||||||
|
-- bliebe ein Rest ohne Bezug stehen — die Bedingungen verbieten das,
|
||||||
|
-- und der Vorgang schlüge fehl, statt das Offensichtliche zu tun.
|
||||||
|
kuendigungsschutz_grund = case
|
||||||
|
when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then
|
||||||
|
case when v_role ? 'kuendigungsschutz_grund'
|
||||||
|
then nullif(v_role->>'kuendigungsschutz_grund','')
|
||||||
|
else kuendigungsschutz_grund end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
kuendigungsschutz_ab = case
|
||||||
|
when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then
|
||||||
|
case when v_role ? 'kuendigungsschutz_ab'
|
||||||
|
then nullif(v_role->>'kuendigungsschutz_ab','')::date
|
||||||
|
else kuendigungsschutz_ab end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
kuendigungsschutz_bis = case
|
||||||
|
when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then
|
||||||
|
case when v_role ? 'kuendigungsschutz_bis'
|
||||||
|
then nullif(v_role->>'kuendigungsschutz_bis','')::date
|
||||||
|
else kuendigungsschutz_bis end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
ist_beguenstigt_behindert = coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert),
|
||||||
|
behinderung_grad = case
|
||||||
|
when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then
|
||||||
|
case when v_role ? 'behinderung_grad' then nullif(v_role->>'behinderung_grad','')::integer else behinderung_grad end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
behinderung_ab = case
|
||||||
|
when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then
|
||||||
|
case when v_role ? 'behinderung_ab' then nullif(v_role->>'behinderung_ab','')::date else behinderung_ab end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
behinderung_bis = case
|
||||||
|
when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then
|
||||||
|
case when v_role ? 'behinderung_bis' then nullif(v_role->>'behinderung_bis','')::date else behinderung_bis end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
dienstwagen_art = case
|
||||||
|
when coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen) then
|
||||||
|
coalesce(nullif(v_role->>'dienstwagen_art', ''), dienstwagen_art, 'Verbrenner')
|
||||||
|
else null
|
||||||
|
end
|
||||||
|
where id = v_employee_id;
|
||||||
|
elsif jsonb_array_length(v_person_changes) > 0 or jsonb_array_length(v_contract_changes) > 0 then
|
||||||
|
insert into pending_org_changes (employee_id, change_type, effective_date, payload)
|
||||||
|
values (v_employee_id, 'contract_change', v_effective_date, payload)
|
||||||
|
returning id into v_pending_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if jsonb_array_length(v_person_changes) > 0 then
|
||||||
|
insert into employee_history (employee_id, event_date, event_type, description, changes, pending_id)
|
||||||
|
values (v_employee_id, v_effective_date, 'Stammdatenänderung',
|
||||||
|
'Geänderte Felder: ' || app_aenderungsfelder(v_person_changes) || ', wirksam ab ' || v_effective_date, v_person_changes, v_pending_id);
|
||||||
|
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes)
|
||||||
|
values (app_current_user_id(), current_actor_name(), 'Stammdatenänderung', v_name, v_employee_id,
|
||||||
|
app_aenderungsfelder(v_person_changes) || ', wirksam ab ' || v_effective_date, v_person_changes);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if jsonb_array_length(v_contract_changes) > 0 then
|
||||||
|
insert into employee_history (employee_id, event_date, event_type, description, changes, pending_id)
|
||||||
|
values (v_employee_id, v_effective_date, 'Vertragsänderung',
|
||||||
|
'Geänderte Felder: ' || app_aenderungsfelder(v_contract_changes) || ', wirksam ab ' || v_effective_date, v_contract_changes, v_pending_id);
|
||||||
|
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes)
|
||||||
|
values (app_current_user_id(), current_actor_name(), 'Vertragsänderung', v_name, v_employee_id,
|
||||||
|
app_aenderungsfelder(v_contract_changes) || ', wirksam ab ' || v_effective_date, v_contract_changes);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- ── Die Übernahme ────────────────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- Nur in dieser einen Richtung. Wer von extern nach intern wechselt, wird
|
||||||
|
-- übernommen; der umgekehrte Weg ist kein Vorgang, den es fachlich gibt.
|
||||||
|
--
|
||||||
|
-- Das Ereignis entsteht **zusätzlich** zur Vertragsänderung darüber und
|
||||||
|
-- nicht an ihrer Stelle: die eine hält fest, dass ein Feld sich geändert
|
||||||
|
-- hat und worauf (und lässt sich darüber zurücknehmen), das andere, dass
|
||||||
|
-- dieser Wechsel eine Übernahme war — und nur das lässt sich zählen.
|
||||||
|
--
|
||||||
|
-- `v_effective_date`, nicht `now()`: eine auf später datierte Übernahme
|
||||||
|
-- gehört in die Akte auf den Tag, an dem sie gilt. Bei einer sofortigen
|
||||||
|
-- ist das heute.
|
||||||
|
if v_role ? 'source' and v_old.source::text = 'Extern' and v_role->>'source' = 'Intern' then
|
||||||
|
insert into employee_history (employee_id, event_date, event_type, description, pending_id)
|
||||||
|
values (v_employee_id, v_effective_date, 'Übernahme',
|
||||||
|
'Übernahme von extern auf intern, wirksam ab ' || v_effective_date, v_pending_id);
|
||||||
|
|
||||||
|
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
|
||||||
|
values (app_current_user_id(), current_actor_name(), 'Übernahme', v_name, v_employee_id,
|
||||||
|
'Von extern auf intern, wirksam ab ' || v_effective_date);
|
||||||
|
end if;
|
||||||
|
end;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
|
||||||
|
-- ═══ Der Nachtlauf holt nach, was er bisher liegenliess ═══
|
||||||
|
--
|
||||||
|
-- Eine auf später datierte Änderung landet als 'contract_change' in
|
||||||
|
-- pending_org_changes; apply_due_pending_changes schreibt sie am Stichtag in
|
||||||
|
-- die Zeile. Bisher schrieb es dabei **nur** Person und Vertrag zurück — die
|
||||||
|
-- ganze Gruppe `role` fehlte. Betriebsrat, Dienstwagen, Kollektivvertrag,
|
||||||
|
-- Arbeitstage, Teilzeitvariante und der Kündigungsschutz wurden also erfasst,
|
||||||
|
-- in der Historie vermerkt, als Vertragsänderung protokolliert — und am
|
||||||
|
-- Stichtag nicht angewendet. Sichtbar wurde das nie: der Eintrag in der Akte
|
||||||
|
-- steht da, nur die Spalte blieb alt.
|
||||||
|
--
|
||||||
|
-- Das ist älter als diese Migration und gehört streng genommen nicht zu ihr.
|
||||||
|
-- Die neuen Felder erben den Fehler aber unmittelbar, und ihn nur für sie zu
|
||||||
|
-- beheben hiesse, zwei Klassen von Feldern mit zwei Verhaltensweisen zu
|
||||||
|
-- hinterlassen. Deshalb hier vollständig.
|
||||||
|
--
|
||||||
|
-- Die Reihenfolge der Zuweisungen entspricht der in change_employee_data;
|
||||||
|
-- dieselben Kopplungen (fällt das Kennzeichen, fällt das Datum) gelten hier
|
||||||
|
-- genauso, sonst wiese eine Bedingung den Nachtlauf zurück.
|
||||||
|
CREATE OR REPLACE FUNCTION public.apply_due_pending_changes()
|
||||||
|
RETURNS integer
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path TO 'public', 'pg_temp'
|
||||||
|
AS $function$
|
||||||
|
declare
|
||||||
|
v_rec record;
|
||||||
|
v_count integer := 0;
|
||||||
|
v_role jsonb;
|
||||||
|
begin
|
||||||
|
for v_rec in
|
||||||
|
select * from pending_org_changes
|
||||||
|
where status = 'pending' and effective_date <= current_date
|
||||||
|
order by effective_date, created_at
|
||||||
|
loop
|
||||||
|
if v_rec.change_type = 'transfer' then
|
||||||
|
update employees set job_title = coalesce(v_rec.payload->>'new_title', job_title)
|
||||||
|
where id = v_rec.employee_id;
|
||||||
|
|
||||||
|
elsif v_rec.change_type = 'promotion' then
|
||||||
|
update employees set
|
||||||
|
job_title = coalesce(v_rec.payload->>'new_title', job_title),
|
||||||
|
paygrade = coalesce((v_rec.payload->>'new_paygrade')::paygrade_type, paygrade)
|
||||||
|
where id = v_rec.employee_id;
|
||||||
|
|
||||||
|
elsif v_rec.change_type = 'karenz_start' then
|
||||||
|
update employees set
|
||||||
|
status = 'Karenz',
|
||||||
|
karenz_return_date = (v_rec.payload->>'planned_return_date')::date,
|
||||||
|
absence_type = coalesce(nullif(v_rec.payload->>'absence_type', ''), absence_type)
|
||||||
|
where id = v_rec.employee_id;
|
||||||
|
|
||||||
|
elsif v_rec.change_type = 'karenz_return' then
|
||||||
|
update employees set
|
||||||
|
status = 'Aktiv',
|
||||||
|
karenz_return_date = null,
|
||||||
|
karenz_start_date = null,
|
||||||
|
absence_type = null,
|
||||||
|
employment_type = coalesce((v_rec.payload->>'employment_type')::employment_type, employment_type),
|
||||||
|
weekly_hours = coalesce((v_rec.payload->>'weekly_hours')::numeric, weekly_hours),
|
||||||
|
-- Auch bei einer *geplanten* Rückkehr: sonst käme jemand am
|
||||||
|
-- Stichtag mit reduzierten Stunden zurück, und der Grund dafür
|
||||||
|
-- wäre verschwunden.
|
||||||
|
teilzeit_art = case when v_rec.payload ? 'teilzeit_art'
|
||||||
|
then nullif(v_rec.payload->>'teilzeit_art', '') else teilzeit_art end,
|
||||||
|
teilzeit_bis = case when v_rec.payload ? 'teilzeit_art'
|
||||||
|
then nullif(v_rec.payload->>'teilzeit_bis', '')::date else teilzeit_bis end
|
||||||
|
where id = v_rec.employee_id;
|
||||||
|
|
||||||
|
elsif v_rec.change_type = 'contract_change' then
|
||||||
|
v_role := v_rec.payload->'role';
|
||||||
|
update employees set
|
||||||
|
first_name = coalesce(v_rec.payload->'person'->>'first_name', first_name),
|
||||||
|
last_name = coalesce(v_rec.payload->'person'->>'last_name', last_name),
|
||||||
|
gender = coalesce((v_rec.payload->'person'->>'gender')::gender_type, gender),
|
||||||
|
birth_date = coalesce((v_rec.payload->'person'->>'birth_date')::date, birth_date),
|
||||||
|
sv_nummer = coalesce(v_rec.payload->'person'->>'sv_nummer', sv_nummer),
|
||||||
|
nationality = coalesce(v_rec.payload->'person'->>'nationality', nationality),
|
||||||
|
address = coalesce(v_rec.payload->'person'->>'address', address),
|
||||||
|
postal_code = coalesce(v_rec.payload->'person'->>'postal_code', postal_code),
|
||||||
|
city = coalesce(v_rec.payload->'person'->>'city', city),
|
||||||
|
address_country = coalesce(v_rec.payload->'person'->>'address_country', address_country),
|
||||||
|
email = coalesce(v_rec.payload->'person'->>'email', email),
|
||||||
|
phone = coalesce(v_rec.payload->'person'->>'phone', phone),
|
||||||
|
company_email = case when v_rec.payload->'person' ? 'company_email' then nullif(v_rec.payload->'person'->>'company_email', '') else company_email end,
|
||||||
|
employment_type = coalesce((v_rec.payload->'contract'->>'employment_type')::employment_type, employment_type),
|
||||||
|
weekly_hours = coalesce((v_rec.payload->'contract'->>'weekly_hours')::numeric, weekly_hours),
|
||||||
|
contract_type = coalesce((v_rec.payload->'contract'->>'contract_type')::contract_type, contract_type),
|
||||||
|
contract_end_date = case when v_rec.payload->'contract' ? 'contract_end_date'
|
||||||
|
then nullif(v_rec.payload->'contract'->>'contract_end_date','')::date
|
||||||
|
else contract_end_date end,
|
||||||
|
worker_type = coalesce((v_role->>'worker_type')::worker_type, worker_type),
|
||||||
|
mitarbeiterart = coalesce(nullif(v_role->>'mitarbeiterart', ''), mitarbeiterart),
|
||||||
|
source = coalesce((nullif(v_role->>'source', ''))::source_type, source),
|
||||||
|
collective_agreement = coalesce((v_role->>'collective_agreement')::collective_agreement, collective_agreement),
|
||||||
|
work_days = case when v_role ? 'work_days'
|
||||||
|
then coalesce((select array_agg(elem) from jsonb_array_elements_text(v_role->'work_days') elem), '{}')
|
||||||
|
else work_days end,
|
||||||
|
is_betriebsrat = coalesce((v_role->>'is_betriebsrat')::boolean, is_betriebsrat),
|
||||||
|
has_dienstwagen = coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen),
|
||||||
|
is_laterale_fuehrung = coalesce((v_role->>'is_laterale_fuehrung')::boolean, is_laterale_fuehrung),
|
||||||
|
is_c_level = coalesce((v_role->>'is_c_level')::boolean, is_c_level),
|
||||||
|
dienstwagen_art = case
|
||||||
|
when coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen) then
|
||||||
|
coalesce(nullif(v_role->>'dienstwagen_art', ''), dienstwagen_art, 'Verbrenner')
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
teilzeit_art = case when v_role ? 'teilzeit_art' then nullif(v_role->>'teilzeit_art', '') else teilzeit_art end,
|
||||||
|
teilzeit_bis = case
|
||||||
|
when coalesce(nullif(v_role->>'teilzeit_art', ''), case when v_role ? 'teilzeit_art' then null else teilzeit_art end) is null then null
|
||||||
|
when v_role ? 'teilzeit_bis' then nullif(v_role->>'teilzeit_bis','')::date
|
||||||
|
else teilzeit_bis
|
||||||
|
end,
|
||||||
|
hat_aufenthaltstitel = coalesce((v_rec.payload->'person'->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel),
|
||||||
|
aufenthaltstitel_bis = case
|
||||||
|
when coalesce((v_rec.payload->'person'->>'hat_aufenthaltstitel')::boolean, hat_aufenthaltstitel) then
|
||||||
|
case when v_rec.payload->'person' ? 'aufenthaltstitel_bis'
|
||||||
|
then nullif(v_rec.payload->'person'->>'aufenthaltstitel_bis','')::date
|
||||||
|
else aufenthaltstitel_bis end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
has_kuendigungsschutz = coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz),
|
||||||
|
kuendigungsschutz_grund = case
|
||||||
|
when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then
|
||||||
|
case when v_role ? 'kuendigungsschutz_grund' then nullif(v_role->>'kuendigungsschutz_grund','') else kuendigungsschutz_grund end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
kuendigungsschutz_ab = case
|
||||||
|
when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then
|
||||||
|
case when v_role ? 'kuendigungsschutz_ab' then nullif(v_role->>'kuendigungsschutz_ab','')::date else kuendigungsschutz_ab end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
kuendigungsschutz_bis = case
|
||||||
|
when coalesce((v_role->>'has_kuendigungsschutz')::boolean, has_kuendigungsschutz) then
|
||||||
|
case when v_role ? 'kuendigungsschutz_bis' then nullif(v_role->>'kuendigungsschutz_bis','')::date else kuendigungsschutz_bis end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
ist_beguenstigt_behindert = coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert),
|
||||||
|
behinderung_grad = case
|
||||||
|
when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then
|
||||||
|
case when v_role ? 'behinderung_grad' then nullif(v_role->>'behinderung_grad','')::integer else behinderung_grad end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
behinderung_ab = case
|
||||||
|
when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then
|
||||||
|
case when v_role ? 'behinderung_ab' then nullif(v_role->>'behinderung_ab','')::date else behinderung_ab end
|
||||||
|
else null
|
||||||
|
end,
|
||||||
|
behinderung_bis = case
|
||||||
|
when coalesce((v_role->>'ist_beguenstigt_behindert')::boolean, ist_beguenstigt_behindert) then
|
||||||
|
case when v_role ? 'behinderung_bis' then nullif(v_role->>'behinderung_bis','')::date else behinderung_bis end
|
||||||
|
else null
|
||||||
|
end
|
||||||
|
where id = v_rec.employee_id;
|
||||||
|
|
||||||
|
elsif v_rec.change_type = 'dependent_add' then
|
||||||
|
insert into employee_dependents (employee_id, first_name, last_name, relationship, sv_nummer, birth_date)
|
||||||
|
values (v_rec.employee_id, v_rec.payload->>'first_name', v_rec.payload->>'last_name',
|
||||||
|
(v_rec.payload->>'relationship')::text,
|
||||||
|
nullif(v_rec.payload->>'sv_nummer', ''), (v_rec.payload->>'birth_date')::date);
|
||||||
|
|
||||||
|
elsif v_rec.change_type = 'dependent_remove' then
|
||||||
|
delete from employee_dependents where id = (v_rec.payload->>'dependent_id')::uuid;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
update pending_org_changes set status = 'applied', applied_at = now() where id = v_rec.id;
|
||||||
|
v_count := v_count + 1;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
return v_count;
|
||||||
|
end;
|
||||||
|
$function$;
|
||||||
|
|
||||||
|
grant execute on function public.apply_due_pending_changes() to alpenwerk_app;
|
||||||
|
grant execute on function public.change_employee_data(payload jsonb) to alpenwerk_app;
|
||||||
|
|
||||||
|
-- Selbstprüfung.
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if not exists (
|
||||||
|
select 1 from information_schema.columns
|
||||||
|
where table_schema = 'public' and table_name = 'employees' and column_name = 'company_email'
|
||||||
|
) then
|
||||||
|
raise exception 'Die Spalte company_email fehlt.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if pg_get_functiondef('public.change_employee_data(jsonb)'::regprocedure) not like '%company_email%' then
|
||||||
|
raise exception 'change_employee_data kennt die Firmen-E-Mail nicht — sie liesse sich nicht aendern.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Der Nachtlauf ist die Stelle, an der so etwas schon einmal still
|
||||||
|
-- verschwunden ist: erfasst, in der Historie sichtbar, nie angekommen.
|
||||||
|
if pg_get_functiondef('public.apply_due_pending_changes()'::regprocedure) not like '%company_email%' then
|
||||||
|
raise exception 'Der Nachtlauf kennt die Firmen-E-Mail nicht — eine auf spaeter datierte Aenderung verfiele.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if app_feld_karte() -> 'Firmen-E-Mail' is null then
|
||||||
|
raise exception 'Die Feldkarte kennt die Firmen-E-Mail nicht — der Eintrag in der Historie waere nicht korrigierbar.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Ohne das nimmt der Einstellungsassistent das Feld entgegen und wirft es
|
||||||
|
-- weg: das Formular zeigte eine Adresse, gespeichert waere keine.
|
||||||
|
if pg_get_functiondef('public.hire_employee(jsonb)'::regprocedure) not like '%company_email%' then
|
||||||
|
raise exception 'hire_employee kennt die Firmen-E-Mail nicht — bei der Einstellung ginge sie verloren.';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Der Wiedereintritt teilt sich den Schritt „Person" mit der Einstellung:
|
||||||
|
-- das Feld steht dort im Formular, ob diese Funktion es kennt oder nicht.
|
||||||
|
if pg_get_functiondef('public.rehire_employee(jsonb)'::regprocedure) not like '%company_email%' then
|
||||||
|
raise exception 'rehire_employee kennt die Firmen-E-Mail nicht — beim Wiedereintritt ginge sie verloren.';
|
||||||
|
end if;
|
||||||
|
end $$;
|
||||||
@@ -14,18 +14,23 @@ 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
|
// Die E-Mail-Spalte trägt die **Firmenadresse** (employees.company_email,
|
||||||
// im Datenmodell noch nicht gibt. employees.email ist die private Adresse
|
// Migration 20260923120000) und niemals die private aus employees.email: die
|
||||||
// (Migration 20260811140000) und gehört nicht in eine Datei, die an einen
|
// Datei geht an einen fremden Anbieter, der damit im Namen des Arbeitgebers
|
||||||
// fremden Anbieter geht — schon gar nicht als Adresse, an die Einladungen im
|
// einlädt. Eine Privatadresse hat dort nichts verloren.
|
||||||
// Namen des Arbeitgebers gehen. Deshalb bleibt die Spalte leer, bis die
|
//
|
||||||
// Firmenadresse erfasst wird; die Spalte selbst bleibt stehen, damit die
|
// Bis zur Migration war die Spalte ein leerer Platzhalter — sie stand
|
||||||
// Zuordnung in Honestly nicht neu eingerichtet werden muss.
|
// 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_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;
|
||||||
|
/** Die dienstliche Adresse. Leer, wenn keine hinterlegt ist. */
|
||||||
|
email: string;
|
||||||
vorname: string;
|
vorname: string;
|
||||||
nachname: string;
|
nachname: string;
|
||||||
standort: string;
|
standort: string;
|
||||||
@@ -64,8 +69,7 @@ export function honestlySpalten(tiefe: number): ExportColumn<HonestlyZeile>[] {
|
|||||||
}));
|
}));
|
||||||
return [
|
return [
|
||||||
{ header: "Personalnummer", get: (z) => z.personalnummer },
|
{ header: "Personalnummer", get: (z) => z.personalnummer },
|
||||||
// Platzhalter für die Firmenadresse — siehe oben.
|
{ header: "Email", get: (z) => z.email },
|
||||||
{ 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 },
|
||||||
@@ -79,6 +83,7 @@ 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;
|
||||||
|
company_email: string | null;
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
location_id: string;
|
location_id: string;
|
||||||
@@ -89,6 +94,7 @@ 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.company_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) ?? "",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export async function bestandLaden(tx: Tx): Promise<Bestand> {
|
|||||||
.select(["position_id"])
|
.select(["position_id"])
|
||||||
.where((eb) => eb.or([eb("valid_to", "is", null), eb("valid_to", ">=", heute)]))
|
.where((eb) => eb.or([eb("valid_to", "is", null), eb("valid_to", ">=", heute)]))
|
||||||
.execute(),
|
.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));
|
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,
|
// Ohne Adresse gibt es nichts zu vergleichen — seit sie freiwillig ist,
|
||||||
// kann sie fehlen.
|
// kann sie fehlen.
|
||||||
emails: new Set(personen.filter((e) => e.email).map((e) => e.email!.toLowerCase())),
|
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!))),
|
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),
|
city: txt(w.city),
|
||||||
address_country: txt(w.address_country),
|
address_country: txt(w.address_country),
|
||||||
email: txt(w.email)!,
|
email: txt(w.email)!,
|
||||||
|
company_email: txt(w.company_email),
|
||||||
phone: txt(w.phone),
|
phone: txt(w.phone),
|
||||||
title_prefix: liste(w.title_prefix) ?? [],
|
title_prefix: liste(w.title_prefix) ?? [],
|
||||||
title_suffix: liste(w.title_suffix) ?? [],
|
title_suffix: liste(w.title_suffix) ?? [],
|
||||||
|
|||||||
@@ -232,6 +232,14 @@ export const BLATT_PERSONEN: BlattSchema = {
|
|||||||
hinweis: "Freiwillig. Eindeutig, wenn angegeben.",
|
hinweis: "Freiwillig. Eindeutig, wenn angegeben.",
|
||||||
beispiel: "s.aigner@example.at",
|
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: "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: "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" },
|
{ 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 }>;
|
planstellen: Map<string, { id: string; besetzt: boolean }>;
|
||||||
personalnummern: Map<number, string>;
|
personalnummern: Map<number, string>;
|
||||||
emails: Set<string>;
|
emails: Set<string>;
|
||||||
|
firmenEmails: Set<string>;
|
||||||
svNummern: Set<string>;
|
svNummern: Set<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ export const LEERER_BESTAND: Bestand = {
|
|||||||
planstellen: new Map(),
|
planstellen: new Map(),
|
||||||
personalnummern: new Map(),
|
personalnummern: new Map(),
|
||||||
emails: new Set(),
|
emails: new Set(),
|
||||||
|
firmenEmails: new Set(),
|
||||||
svNummern: new Set(),
|
svNummern: new Set(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -262,6 +264,7 @@ export function pruefe(blaetter: ImportSheet[], bestand: Bestand = LEERER_BESTAN
|
|||||||
const heute = todayIso();
|
const heute = todayIso();
|
||||||
const personalnummern = new Set([...bestand.personalnummern.keys()]);
|
const personalnummern = new Set([...bestand.personalnummern.keys()]);
|
||||||
const emails = new Set([...bestand.emails]);
|
const emails = new Set([...bestand.emails]);
|
||||||
|
const firmenEmails = new Set([...bestand.firmenEmails]);
|
||||||
const svNummern = new Set([...bestand.svNummern]);
|
const svNummern = new Set([...bestand.svNummern]);
|
||||||
const belegtePlanstellen = new Map<string, number>();
|
const belegtePlanstellen = new Map<string, number>();
|
||||||
const eintritte = new Map<number, string>();
|
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);
|
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 geburt = s(w.birth_date);
|
||||||
const svRoh = s(w.sv_nummer);
|
const svRoh = s(w.sv_nummer);
|
||||||
if (svRoh) {
|
if (svRoh) {
|
||||||
|
|||||||
@@ -207,6 +207,13 @@ export type Database = {
|
|||||||
address_country: string | null;
|
address_country: string | null;
|
||||||
/** Private Adresse, freiwillig — eindeutig, wenn angegeben. */
|
/** Private Adresse, freiwillig — eindeutig, wenn angegeben. */
|
||||||
email: string | null;
|
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;
|
phone: string | null;
|
||||||
job_title: string;
|
job_title: string;
|
||||||
location_id: string;
|
location_id: string;
|
||||||
@@ -286,6 +293,7 @@ export type Database = {
|
|||||||
city?: string | null;
|
city?: string | null;
|
||||||
address_country?: string | null;
|
address_country?: string | null;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
|
company_email?: string | null;
|
||||||
phone?: string | null;
|
phone?: string | null;
|
||||||
job_title: string;
|
job_title: string;
|
||||||
location_id: string;
|
location_id: string;
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ export function vorbelegungAus(employee: EmployeeRow): Partial<HireDraftData> {
|
|||||||
birthDate: employee.birth_date,
|
birthDate: employee.birth_date,
|
||||||
svNummer: employee.sv_nummer ?? "",
|
svNummer: employee.sv_nummer ?? "",
|
||||||
email: employee.email ?? "",
|
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 ?? "",
|
phone: employee.phone ?? "",
|
||||||
locationId: employee.location_id,
|
locationId: employee.location_id,
|
||||||
besetzung: employee.source,
|
besetzung: employee.source,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function person(teil: Partial<Parameters<typeof baueHonestlyZeilen>[0][number]>
|
|||||||
// Die private Adresse, wie sie auf der Zeile aus der Datenbank steht —
|
// Die private Adresse, wie sie auf der Zeile aus der Datenbank steht —
|
||||||
// sie darf im Export nirgends auftauchen.
|
// sie darf im Export nirgends auftauchen.
|
||||||
email: "anna.privat@example.test",
|
email: "anna.privat@example.test",
|
||||||
|
company_email: "a.berger@firma.test",
|
||||||
first_name: "Anna",
|
first_name: "Anna",
|
||||||
last_name: "Berger",
|
last_name: "Berger",
|
||||||
location_id: "wien",
|
location_id: "wien",
|
||||||
@@ -105,12 +106,17 @@ describe("baueHonestlyZeilen", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lässt die E-Mail leer, bis es die Firmenadresse gibt", () => {
|
it("nimmt die Firmenadresse in die E-Mail-Spalte", () => {
|
||||||
// Die Spalte ist ein Platzhalter für die Firmen-E-Mail. Die private
|
|
||||||
// Adresse (employees.email) gehört nicht in eine Datei an einen fremden
|
|
||||||
// Anbieter — auch nicht, wenn sie vorhanden ist.
|
|
||||||
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
|
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
|
||||||
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.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("");
|
expect(zeile.Email).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,6 +126,12 @@ describe("baueHonestlyZeilen", () => {
|
|||||||
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
|
const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte);
|
||||||
const csv = toCsv(zeilen, honestlySpalten(tiefe));
|
const csv = toCsv(zeilen, honestlySpalten(tiefe));
|
||||||
expect(csv).not.toContain("anna.privat@example.test");
|
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", () => {
|
it("behält die Spalte Email, auch solange sie leer ist", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user