Daten aendern: effective date, searchable UN country pickers, 2 more bugfixes

Feature requests from live use:
- "Daten aendern" was missing a "Wirksam ab" field (unlike Versetzen/
  Befoerdern/Karenz, which all have one) - every change was silently
  logged with today's date. Added the field, threaded through
  change_employee_data (defaults to today if omitted).
- Staatsbuergerschaft and Wohnland now use a searchable picker
  (components/ui/CountryPicker) over the full 193-country UN member
  state list (lib/countries.ts) instead of the original ~9/5-value
  picklists. Dropped the now-too-narrow CHECK constraints
  (supabase/schema_2.sql) since the app is the source of truth for
  valid values, same approach used elsewhere for large open-ended
  pickers.

Two more real bugs found via live testing of the above (both in
change_employee_data, supabase/functions.sql + functions_4.sql):
1. `text[] || 'literal'` is ambiguous in Postgres - it can resolve to
   the array||array overload and try to parse the plain word as array
   syntax ('{...}'), failing with "malformed array literal". Hit on
   every single field-diff line the moment a user actually changed
   something (Staatsbuergerschaft first, then Beschaeftigungsausmass
   confirmed the same root cause). Fixed everywhere by switching to the
   unambiguous array_append() function.
2. The contract_end_date diff-check cast an empty string straight to
   date ("invalid input syntax for type date: ''") instead of using the
   same nullif(...,'')::date guard the UPDATE line below it already had.

Verified live end-to-end after both fixes: changed Staatsbuergerschaft
to Brasilien with a backdated effective date, save succeeded, Stammdaten
tab reflects it, and employee_history got the correct event_date
("2026-07-01") and description ("Geänderte Felder: Staatsbürgerschaft,
wirksam ab 2026-07-01"). Reverted the test employee's data back
afterward; seed data is clean again.
This commit is contained in:
2026-07-13 23:30:43 +02:00
parent e27db5f030
commit 131ca7ece7
7 changed files with 389 additions and 35 deletions

View File

@@ -100,6 +100,7 @@ export async function recordKarenzReturn(payload: {
export async function changeEmployeeData(payload: { export async function changeEmployeeData(payload: {
employee_id: string; employee_id: string;
effective_date: string;
person: Record<string, unknown>; person: Record<string, unknown>;
contract: Record<string, unknown>; contract: Record<string, unknown>;
}): Promise<ActionResult> { }): Promise<ActionResult> {

View File

@@ -3,19 +3,19 @@
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { changeEmployeeData } from "@/actions/employees"; import { changeEmployeeData } from "@/actions/employees";
import { CountryPicker } from "@/components/ui/CountryPicker";
import { SlideOver } from "@/components/ui/SlideOver"; import { SlideOver } from "@/components/ui/SlideOver";
import { useToast } from "@/components/ui/Toast"; import { useToast } from "@/components/ui/Toast";
import { UN_COUNTRIES } from "@/lib/countries";
import type { ContractType, Database, EmploymentType, GenderType } from "@/lib/supabase/types"; import type { ContractType, Database, EmploymentType, GenderType } from "@/lib/supabase/types";
type EmployeeRow = Database["public"]["Views"]["employees_directory"]["Row"]; type EmployeeRow = Database["public"]["Views"]["employees_directory"]["Row"];
const NATIONALITIES = ["Österreich", "Deutschland", "Tschechien", "Slowenien", "Türkei", "Serbien", "Kroatien", "Bosnien", "Ungarn", "Andere"];
const ADDRESS_COUNTRIES = ["Österreich", "Deutschland", "Tschechien", "Slowenien", "Andere"];
export function DatenAendernPanel({ open, onClose, employee }: { open: boolean; onClose: () => void; employee: EmployeeRow }) { export function DatenAendernPanel({ open, onClose, employee }: { open: boolean; onClose: () => void; employee: EmployeeRow }) {
const { showToast } = useToast(); const { showToast } = useToast();
const router = useRouter(); const router = useRouter();
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
const [effectiveDate, setEffectiveDate] = useState(new Date().toISOString().slice(0, 10));
const [firstName, setFirstName] = useState(employee.first_name); const [firstName, setFirstName] = useState(employee.first_name);
const [lastName, setLastName] = useState(employee.last_name); const [lastName, setLastName] = useState(employee.last_name);
@@ -47,9 +47,14 @@ export function DatenAendernPanel({ open, onClose, employee }: { open: boolean;
showToast("Bei befristetem Vertrag ist ein Enddatum erforderlich.", "error"); showToast("Bei befristetem Vertrag ist ein Enddatum erforderlich.", "error");
return; return;
} }
if (!effectiveDate) {
showToast("Bitte ein Wirksam-ab-Datum angeben.", "error");
return;
}
setPending(true); setPending(true);
const result = await changeEmployeeData({ const result = await changeEmployeeData({
employee_id: employee.id, employee_id: employee.id,
effective_date: effectiveDate,
person: { person: {
first_name: firstName, first_name: firstName,
last_name: lastName, last_name: lastName,
@@ -101,6 +106,16 @@ export function DatenAendernPanel({ open, onClose, employee }: { open: boolean;
} }
> >
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div>
<label className="mb-1 block text-sm font-semibold text-ink">Wirksam ab*</label>
<input
type="date"
value={effectiveDate}
onChange={(e) => setEffectiveDate(e.target.value)}
className="w-full rounded border border-border px-3 py-2 text-sm"
/>
</div>
<div> <div>
<h3 className="mb-3 text-sm font-bold text-ink">Person</h3> <h3 className="mb-3 text-sm font-bold text-ink">Person</h3>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
@@ -138,13 +153,7 @@ export function DatenAendernPanel({ open, onClose, employee }: { open: boolean;
</div> </div>
<div> <div>
<label className="mb-1 block text-xs font-semibold text-ink-muted">Staatsbürgerschaft</label> <label className="mb-1 block text-xs font-semibold text-ink-muted">Staatsbürgerschaft</label>
<select value={nationality} onChange={(e) => setNationality(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm"> <CountryPicker value={nationality} onChange={setNationality} countries={UN_COUNTRIES} placeholder="Staatsbürgerschaft suchen…" />
{NATIONALITIES.map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
@@ -153,13 +162,7 @@ export function DatenAendernPanel({ open, onClose, employee }: { open: boolean;
</div> </div>
<div> <div>
<label className="mb-1 block text-xs font-semibold text-ink-muted">Land</label> <label className="mb-1 block text-xs font-semibold text-ink-muted">Land</label>
<select value={addressCountry} onChange={(e) => setAddressCountry(e.target.value)} className="w-full rounded border border-border px-3 py-2 text-sm"> <CountryPicker value={addressCountry} onChange={setAddressCountry} countries={UN_COUNTRIES} placeholder="Land suchen…" />
{ADDRESS_COUNTRIES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div> </div>
</div> </div>
<div> <div>

View File

@@ -0,0 +1,68 @@
"use client";
import { useEffect, useRef, useState } from "react";
type CountryPickerProps = {
value: string;
onChange: (value: string) => void;
countries: string[];
placeholder?: string;
};
// Searchable text combobox constrained to a fixed list (UN member states)
// rather than a Lookup-style "selected card" — a country is a single text
// value, not an object with its own detail fields.
export function CountryPicker({ value, onChange, countries, placeholder = "Land suchen…" }: CountryPickerProps) {
const [query, setQuery] = useState(value);
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => setQuery(value), [value]);
useEffect(() => {
function onClickOutside(e: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setOpen(false);
setQuery(value);
}
}
document.addEventListener("mousedown", onClickOutside);
return () => document.removeEventListener("mousedown", onClickOutside);
}, [value]);
const filtered = query.trim() ? countries.filter((c) => c.toLowerCase().includes(query.trim().toLowerCase())) : countries;
return (
<div ref={containerRef} className="relative">
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
setOpen(true);
}}
onFocus={() => setOpen(true)}
placeholder={placeholder}
className="w-full rounded border border-border px-3 py-2 text-sm"
/>
{open && (
<div className="absolute z-20 mt-1 max-h-56 w-full overflow-y-auto rounded border border-border bg-white shadow-lg">
{filtered.length === 0 && <div className="px-3 py-2 text-sm text-ink-muted">Keine Treffer</div>}
{filtered.slice(0, 50).map((c) => (
<button
key={c}
type="button"
onClick={() => {
onChange(c);
setQuery(c);
setOpen(false);
}}
className="block w-full px-3 py-2 text-left text-sm hover:bg-surface"
>
{c}
</button>
))}
</div>
)}
</div>
);
}

199
lib/countries.ts Normal file
View File

@@ -0,0 +1,199 @@
// The 193 UN member states, German names, sorted alphabetically. Used for
// Staatsbürgerschaft and Wohnland — both are searchable pickers over this
// same list (see components/ui/Lookup + DatenAendernPanel).
export const UN_COUNTRIES: string[] = [
"Afghanistan",
"Ägypten",
"Albanien",
"Algerien",
"Andorra",
"Angola",
"Antigua und Barbuda",
"Äquatorialguinea",
"Argentinien",
"Armenien",
"Aserbaidschan",
"Äthiopien",
"Australien",
"Bahamas",
"Bahrain",
"Bangladesch",
"Barbados",
"Belgien",
"Belize",
"Benin",
"Bhutan",
"Bolivien",
"Bosnien und Herzegowina",
"Botsuana",
"Brasilien",
"Brunei",
"Bulgarien",
"Burkina Faso",
"Burundi",
"Chile",
"China",
"Costa Rica",
"Côte d'Ivoire",
"Dänemark",
"Deutschland",
"Dominica",
"Dominikanische Republik",
"Dschibuti",
"Ecuador",
"El Salvador",
"Eritrea",
"Estland",
"Eswatini",
"Fidschi",
"Finnland",
"Frankreich",
"Gabun",
"Gambia",
"Georgien",
"Ghana",
"Grenada",
"Griechenland",
"Guatemala",
"Guinea",
"Guinea-Bissau",
"Guyana",
"Haiti",
"Honduras",
"Indien",
"Indonesien",
"Irak",
"Iran",
"Irland",
"Island",
"Israel",
"Italien",
"Jamaika",
"Japan",
"Jemen",
"Jordanien",
"Kambodscha",
"Kamerun",
"Kanada",
"Kap Verde",
"Kasachstan",
"Katar",
"Kenia",
"Kirgisistan",
"Kiribati",
"Kolumbien",
"Komoren",
"Kongo, Demokratische Republik",
"Kongo, Republik",
"Kosovo",
"Kroatien",
"Kuba",
"Kuwait",
"Laos",
"Lesotho",
"Lettland",
"Libanon",
"Liberia",
"Libyen",
"Liechtenstein",
"Litauen",
"Luxemburg",
"Madagaskar",
"Malawi",
"Malaysia",
"Malediven",
"Mali",
"Malta",
"Marokko",
"Marshallinseln",
"Mauretanien",
"Mauritius",
"Mexiko",
"Mikronesien",
"Moldau",
"Monaco",
"Mongolei",
"Montenegro",
"Mosambik",
"Myanmar",
"Namibia",
"Nauru",
"Nepal",
"Neuseeland",
"Nicaragua",
"Niederlande",
"Niger",
"Nigeria",
"Nordkorea",
"Nordmazedonien",
"Norwegen",
"Oman",
"Österreich",
"Osttimor",
"Pakistan",
"Palau",
"Panama",
"Papua-Neuguinea",
"Paraguay",
"Peru",
"Philippinen",
"Polen",
"Portugal",
"Ruanda",
"Rumänien",
"Russland",
"Salomonen",
"Sambia",
"Samoa",
"San Marino",
"São Tomé und Príncipe",
"Saudi-Arabien",
"Schweden",
"Schweiz",
"Senegal",
"Serbien",
"Seychellen",
"Sierra Leone",
"Simbabwe",
"Singapur",
"Slowakei",
"Slowenien",
"Somalia",
"Spanien",
"Sri Lanka",
"St. Kitts und Nevis",
"St. Lucia",
"St. Vincent und die Grenadinen",
"Südafrika",
"Sudan",
"Südkorea",
"Südsudan",
"Suriname",
"Syrien",
"Tadschikistan",
"Tansania",
"Thailand",
"Togo",
"Tonga",
"Trinidad und Tobago",
"Tschad",
"Tschechien",
"Tunesien",
"Türkei",
"Turkmenistan",
"Tuvalu",
"Uganda",
"Ukraine",
"Ungarn",
"Uruguay",
"Usbekistan",
"Vanuatu",
"Venezuela",
"Vereinigte Arabische Emirate",
"Vereinigte Staaten",
"Vereinigtes Königreich",
"Vietnam",
"Weißrussland",
"Zentralafrikanische Republik",
"Zypern",
].sort((a, b) => a.localeCompare(b, "de"));

View File

@@ -300,6 +300,7 @@ create or replace function change_employee_data(payload jsonb)
returns void language plpgsql as $$ returns void language plpgsql as $$
declare declare
v_employee_id uuid := (payload->>'employee_id')::uuid; 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_old employees%rowtype;
v_name text; v_name text;
v_person_changes text[] := '{}'; v_person_changes text[] := '{}';
@@ -311,21 +312,21 @@ begin
select * into v_old from employees where id = v_employee_id; select * into v_old from employees where id = v_employee_id;
v_name := v_old.first_name || ' ' || v_old.last_name; v_name := v_old.first_name || ' ' || v_old.last_name;
if v_person ? 'first_name' and (v_person->>'first_name') <> v_old.first_name then v_person_changes := v_person_changes || 'Vorname'; end if; if v_person ? 'first_name' and (v_person->>'first_name') <> v_old.first_name then v_person_changes := array_append(v_person_changes, 'Vorname'); end if;
if v_person ? 'last_name' and (v_person->>'last_name') <> v_old.last_name then v_person_changes := v_person_changes || 'Nachname'; end if; if v_person ? 'last_name' and (v_person->>'last_name') <> v_old.last_name then v_person_changes := array_append(v_person_changes, 'Nachname'); end if;
if v_person ? 'gender' and (v_person->>'gender') <> v_old.gender::text then v_person_changes := v_person_changes || 'Geschlecht'; end if; if v_person ? 'gender' and (v_person->>'gender') <> v_old.gender::text then v_person_changes := array_append(v_person_changes, 'Geschlecht'); end if;
if v_person ? 'birth_date' and (v_person->>'birth_date')::date <> v_old.birth_date then v_person_changes := v_person_changes || 'Geburtsdatum'; end if; if v_person ? 'birth_date' and (v_person->>'birth_date')::date <> v_old.birth_date then v_person_changes := array_append(v_person_changes, 'Geburtsdatum'); end if;
if v_person ? 'sv_nummer' and coalesce(v_person->>'sv_nummer','') <> coalesce(v_old.sv_nummer,'') then v_person_changes := v_person_changes || 'SV-Nummer'; end if; if v_person ? 'sv_nummer' and coalesce(v_person->>'sv_nummer','') <> coalesce(v_old.sv_nummer,'') then v_person_changes := array_append(v_person_changes, 'SV-Nummer'); end if;
if v_person ? 'nationality' and (v_person->>'nationality') <> v_old.nationality then v_person_changes := v_person_changes || 'Staatsbürgerschaft'; end if; if v_person ? 'nationality' and (v_person->>'nationality') <> v_old.nationality then v_person_changes := array_append(v_person_changes, 'Staatsbürgerschaft'); end if;
if v_person ? 'address' and coalesce(v_person->>'address','') <> coalesce(v_old.address,'') then v_person_changes := v_person_changes || 'Adresse'; end if; if v_person ? 'address' and coalesce(v_person->>'address','') <> coalesce(v_old.address,'') then v_person_changes := array_append(v_person_changes, 'Adresse'); end if;
if v_person ? 'address_country' and coalesce(v_person->>'address_country','') <> coalesce(v_old.address_country,'') then v_person_changes := v_person_changes || 'Land'; end if; if v_person ? 'address_country' and coalesce(v_person->>'address_country','') <> coalesce(v_old.address_country,'') then v_person_changes := array_append(v_person_changes, 'Land'); end if;
if v_person ? 'email' and (v_person->>'email') <> v_old.email then v_person_changes := v_person_changes || 'E-Mail'; end if; if v_person ? 'email' and (v_person->>'email') <> v_old.email then v_person_changes := array_append(v_person_changes, 'E-Mail'); end if;
if v_person ? 'phone' and coalesce(v_person->>'phone','') <> coalesce(v_old.phone,'') then v_person_changes := v_person_changes || 'Telefon'; end if; if v_person ? 'phone' and coalesce(v_person->>'phone','') <> coalesce(v_old.phone,'') then v_person_changes := array_append(v_person_changes, 'Telefon'); end if;
if v_contract ? 'employment_type' and (v_contract->>'employment_type') <> v_old.employment_type::text then v_contract_changes := v_contract_changes || 'Beschäftigungsausmaß'; end if; if v_contract ? 'employment_type' and (v_contract->>'employment_type') <> v_old.employment_type::text then v_contract_changes := array_append(v_contract_changes, 'Beschäftigungsausmaß'); end if;
if v_contract ? 'weekly_hours' and (v_contract->>'weekly_hours')::numeric <> v_old.weekly_hours then v_contract_changes := v_contract_changes || 'Wochenstunden'; end if; if v_contract ? 'weekly_hours' and (v_contract->>'weekly_hours')::numeric <> v_old.weekly_hours then v_contract_changes := array_append(v_contract_changes, 'Wochenstunden'); end if;
if v_contract ? 'contract_type' and (v_contract->>'contract_type') <> v_old.contract_type::text then v_contract_changes := v_contract_changes || 'Vertragsart'; end if; if v_contract ? 'contract_type' and (v_contract->>'contract_type') <> v_old.contract_type::text then v_contract_changes := array_append(v_contract_changes, 'Vertragsart'); end if;
if v_contract ? 'contract_end_date' and coalesce((v_contract->>'contract_end_date')::date::text,'') <> coalesce(v_old.contract_end_date::text,'') then v_contract_changes := v_contract_changes || 'Befristet bis'; end if; if v_contract ? 'contract_end_date' and coalesce(nullif(v_contract->>'contract_end_date','')::date::text,'') <> coalesce(v_old.contract_end_date::text,'') then v_contract_changes := array_append(v_contract_changes, 'Befristet bis'); end if;
update employees set update employees set
first_name = coalesce(v_person->>'first_name', first_name), first_name = coalesce(v_person->>'first_name', first_name),
@@ -346,16 +347,16 @@ begin
if array_length(v_person_changes, 1) > 0 then if array_length(v_person_changes, 1) > 0 then
insert into employee_history (employee_id, event_date, event_type, description) insert into employee_history (employee_id, event_date, event_type, description)
values (v_employee_id, current_date, 'Stammdatenänderung', 'Geänderte Felder: ' || array_to_string(v_person_changes, ', ')); values (v_employee_id, v_effective_date, 'Stammdatenänderung', 'Geänderte Felder: ' || array_to_string(v_person_changes, ', ') || ', wirksam ab ' || v_effective_date);
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details) insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
values (auth.uid(), current_actor_name(), 'Stammdatenänderung', v_name, v_employee_id, array_to_string(v_person_changes, ', ')); values (auth.uid(), current_actor_name(), 'Stammdatenänderung', v_name, v_employee_id, array_to_string(v_person_changes, ', ') || ', wirksam ab ' || v_effective_date);
end if; end if;
if array_length(v_contract_changes, 1) > 0 then if array_length(v_contract_changes, 1) > 0 then
insert into employee_history (employee_id, event_date, event_type, description) insert into employee_history (employee_id, event_date, event_type, description)
values (v_employee_id, current_date, 'Vertragsänderung', 'Geänderte Felder: ' || array_to_string(v_contract_changes, ', ')); values (v_employee_id, v_effective_date, 'Vertragsänderung', 'Geänderte Felder: ' || array_to_string(v_contract_changes, ', ') || ', wirksam ab ' || v_effective_date);
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details) insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
values (auth.uid(), current_actor_name(), 'Vertragsänderung', v_name, v_employee_id, array_to_string(v_contract_changes, ', ')); values (auth.uid(), current_actor_name(), 'Vertragsänderung', v_name, v_employee_id, array_to_string(v_contract_changes, ', ') || ', wirksam ab ' || v_effective_date);
end if; end if;
end; end;
$$; $$;

71
supabase/functions_4.sql Normal file
View File

@@ -0,0 +1,71 @@
-- Addendum to supabase/functions.sql — run after that file (and functions_2/3.sql).
--
-- "Daten ändern" had no "Wirksam ab" field, unlike Versetzung/Beförderung/
-- Karenz — every change was silently logged with today's date regardless
-- of when it should actually take effect. Adds an effective_date input
-- (defaults to today if omitted) used for both the history event_date and
-- noted in the change description.
create or replace function change_employee_data(payload jsonb)
returns void language plpgsql as $$
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 text[] := '{}';
v_contract_changes text[] := '{}';
v_person jsonb := payload->'person';
v_contract jsonb := payload->'contract';
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;
if v_person ? 'first_name' and (v_person->>'first_name') <> v_old.first_name then v_person_changes := array_append(v_person_changes, 'Vorname'); end if;
if v_person ? 'last_name' and (v_person->>'last_name') <> v_old.last_name then v_person_changes := array_append(v_person_changes, 'Nachname'); end if;
if v_person ? 'gender' and (v_person->>'gender') <> v_old.gender::text then v_person_changes := array_append(v_person_changes, 'Geschlecht'); end if;
if v_person ? 'birth_date' and (v_person->>'birth_date')::date <> v_old.birth_date then v_person_changes := array_append(v_person_changes, 'Geburtsdatum'); end if;
if v_person ? 'sv_nummer' and coalesce(v_person->>'sv_nummer','') <> coalesce(v_old.sv_nummer,'') then v_person_changes := array_append(v_person_changes, 'SV-Nummer'); end if;
if v_person ? 'nationality' and (v_person->>'nationality') <> v_old.nationality then v_person_changes := array_append(v_person_changes, 'Staatsbürgerschaft'); end if;
if v_person ? 'address' and coalesce(v_person->>'address','') <> coalesce(v_old.address,'') then v_person_changes := array_append(v_person_changes, 'Adresse'); end if;
if v_person ? 'address_country' and coalesce(v_person->>'address_country','') <> coalesce(v_old.address_country,'') then v_person_changes := array_append(v_person_changes, 'Land'); end if;
if v_person ? 'email' and (v_person->>'email') <> v_old.email then v_person_changes := array_append(v_person_changes, 'E-Mail'); end if;
if v_person ? 'phone' and coalesce(v_person->>'phone','') <> coalesce(v_old.phone,'') then v_person_changes := array_append(v_person_changes, 'Telefon'); end if;
if v_contract ? 'employment_type' and (v_contract->>'employment_type') <> v_old.employment_type::text then v_contract_changes := array_append(v_contract_changes, 'Beschäftigungsausmaß'); end if;
if v_contract ? 'weekly_hours' and (v_contract->>'weekly_hours')::numeric <> v_old.weekly_hours then v_contract_changes := array_append(v_contract_changes, 'Wochenstunden'); end if;
if v_contract ? 'contract_type' and (v_contract->>'contract_type') <> v_old.contract_type::text then v_contract_changes := array_append(v_contract_changes, 'Vertragsart'); end if;
if v_contract ? 'contract_end_date' and coalesce(nullif(v_contract->>'contract_end_date','')::date::text,'') <> coalesce(v_old.contract_end_date::text,'') then v_contract_changes := array_append(v_contract_changes, 'Befristet bis'); end if;
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),
address_country = coalesce(v_person->>'address_country', address_country),
email = coalesce(v_person->>'email', email),
phone = coalesce(v_person->>'phone', phone),
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
where id = v_employee_id;
if array_length(v_person_changes, 1) > 0 then
insert into employee_history (employee_id, event_date, event_type, description)
values (v_employee_id, v_effective_date, 'Stammdatenänderung', 'Geänderte Felder: ' || array_to_string(v_person_changes, ', ') || ', wirksam ab ' || v_effective_date);
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
values (auth.uid(), current_actor_name(), 'Stammdatenänderung', v_name, v_employee_id, array_to_string(v_person_changes, ', ') || ', wirksam ab ' || v_effective_date);
end if;
if array_length(v_contract_changes, 1) > 0 then
insert into employee_history (employee_id, event_date, event_type, description)
values (v_employee_id, v_effective_date, 'Vertragsänderung', 'Geänderte Felder: ' || array_to_string(v_contract_changes, ', ') || ', wirksam ab ' || v_effective_date);
insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details)
values (auth.uid(), current_actor_name(), 'Vertragsänderung', v_name, v_employee_id, array_to_string(v_contract_changes, ', ') || ', wirksam ab ' || v_effective_date);
end if;
end;
$$;

11
supabase/schema_2.sql Normal file
View File

@@ -0,0 +1,11 @@
-- Addendum to supabase/schema.sql — run after that file.
--
-- Staatsbürgerschaft and Wohnland now use a searchable picker over the
-- full UN member states list (193 countries, see lib/countries.ts)
-- instead of the original ~9/5-value picklists. The old CHECK constraints
-- would reject nearly all of those values, so they're dropped here. The
-- app is the source of truth for valid values (same approach the rest of
-- the app already relies on for large open-ended pickers); the columns
-- stay plain text (nationality keeps its NOT NULL).
alter table employees drop constraint if exists employees_nationality_check;
alter table employees drop constraint if exists employees_address_country_check;