diff --git a/actions/employees.ts b/actions/employees.ts index 1d98087..50ed41d 100644 --- a/actions/employees.ts +++ b/actions/employees.ts @@ -100,6 +100,7 @@ export async function recordKarenzReturn(payload: { export async function changeEmployeeData(payload: { employee_id: string; + effective_date: string; person: Record; contract: Record; }): Promise { diff --git a/components/employees/panels/DatenAendernPanel.tsx b/components/employees/panels/DatenAendernPanel.tsx index 0176683..3c9d18c 100644 --- a/components/employees/panels/DatenAendernPanel.tsx +++ b/components/employees/panels/DatenAendernPanel.tsx @@ -3,19 +3,19 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import { changeEmployeeData } from "@/actions/employees"; +import { CountryPicker } from "@/components/ui/CountryPicker"; import { SlideOver } from "@/components/ui/SlideOver"; import { useToast } from "@/components/ui/Toast"; +import { UN_COUNTRIES } from "@/lib/countries"; import type { ContractType, Database, EmploymentType, GenderType } from "@/lib/supabase/types"; 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 }) { const { showToast } = useToast(); const router = useRouter(); const [pending, setPending] = useState(false); + const [effectiveDate, setEffectiveDate] = useState(new Date().toISOString().slice(0, 10)); const [firstName, setFirstName] = useState(employee.first_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"); return; } + if (!effectiveDate) { + showToast("Bitte ein Wirksam-ab-Datum angeben.", "error"); + return; + } setPending(true); const result = await changeEmployeeData({ employee_id: employee.id, + effective_date: effectiveDate, person: { first_name: firstName, last_name: lastName, @@ -101,6 +106,16 @@ export function DatenAendernPanel({ open, onClose, employee }: { open: boolean; } >
+
+ + setEffectiveDate(e.target.value)} + className="w-full rounded border border-border px-3 py-2 text-sm" + /> +
+

Person

@@ -138,13 +153,7 @@ export function DatenAendernPanel({ open, onClose, employee }: { open: boolean;
- +
@@ -153,13 +162,7 @@ export function DatenAendernPanel({ open, onClose, employee }: { open: boolean;
- +
diff --git a/components/ui/CountryPicker.tsx b/components/ui/CountryPicker.tsx new file mode 100644 index 0000000..6e3db05 --- /dev/null +++ b/components/ui/CountryPicker.tsx @@ -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(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 ( +
+ { + 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 && ( +
+ {filtered.length === 0 &&
Keine Treffer
} + {filtered.slice(0, 50).map((c) => ( + + ))} +
+ )} +
+ ); +} diff --git a/lib/countries.ts b/lib/countries.ts new file mode 100644 index 0000000..1bec166 --- /dev/null +++ b/lib/countries.ts @@ -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")); diff --git a/supabase/functions.sql b/supabase/functions.sql index b60f690..dd76632 100644 --- a/supabase/functions.sql +++ b/supabase/functions.sql @@ -300,6 +300,7 @@ 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[] := '{}'; @@ -311,21 +312,21 @@ begin 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 := 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 ? 'gender' and (v_person->>'gender') <> v_old.gender::text then v_person_changes := 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 ? '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 ? 'nationality' and (v_person->>'nationality') <> v_old.nationality then v_person_changes := 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_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 ? 'email' and (v_person->>'email') <> v_old.email then v_person_changes := 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 ? '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 := 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 ? '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_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 ? '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), @@ -346,16 +347,16 @@ begin if array_length(v_person_changes, 1) > 0 then 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) - 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; if array_length(v_contract_changes, 1) > 0 then 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) - 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; $$; diff --git a/supabase/functions_4.sql b/supabase/functions_4.sql new file mode 100644 index 0000000..65495c6 --- /dev/null +++ b/supabase/functions_4.sql @@ -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; +$$; diff --git a/supabase/schema_2.sql b/supabase/schema_2.sql new file mode 100644 index 0000000..eb51022 --- /dev/null +++ b/supabase/schema_2.sql @@ -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;