worker_type had two values because the field was named after them: "Angestellte:r / Arbeiter:in". Lehrlinge are the third social-insurance category in Austria; until now they were filed as one of the other two, which they are not -- and which skewed every report grouped by this column by exactly those people. Adding the value is one line. The label was the work: the field was called after its two values in six places, and each of them becomes wrong with a third. They now read "Beschaeftigtengruppe", the name the import has used all along. One label deliberately keeps the old wording: app_feld_karte() in the database. That string is not a caption there but a key -- stored rows in employee_history and pending_changes carry it, and the map is how reverting or correcting a history entry finds the field again. Renaming it without rewriting those rows would make every older entry for this field unrevertable, and nobody would notice until they tried. The migration's assertion reads pg_enum rather than comparing against 'Lehrling'::worker_type: migrations run inside a transaction, and Postgres refuses to use a freshly added enum value in the transaction that added it. This has not been run against a live database here -- the CI migration job is the first real execution. Three hand-kept lists of the same enum (reports, import, the form) now have a test holding them to one another, each mutation-checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
505 lines
16 KiB
TypeScript
505 lines
16 KiB
TypeScript
import { ABSENCE_TYPES } from "@/lib/absence";
|
|
import { TITLE_PREFIXES, TITLE_SUFFIXES } from "@/lib/titles";
|
|
|
|
// Was in einer Importdatei stehen darf.
|
|
//
|
|
// Diese Datei ist die einzige Quelle für drei Dinge, die sonst
|
|
// auseinanderlaufen: die Prüfung beim Import, die Vorlage zum Herunterladen
|
|
// und die Hilfetexte in der Oberfläche. Eine Spalte, die hier nicht steht,
|
|
// gibt es nirgends — und eine, die hier dazukommt, taucht überall auf.
|
|
//
|
|
// Die Blattnamen sind zugleich die Dateinamen für den CSV-Weg: „Personen.csv"
|
|
// wird als Blatt „Personen" gelesen.
|
|
|
|
export type FeldArt =
|
|
| { art: "text" }
|
|
| { art: "datum" }
|
|
| { art: "zahl" }
|
|
| { art: "ganzzahl" }
|
|
| { art: "janein" }
|
|
| { art: "liste"; werte?: readonly string[] }
|
|
| { art: "auswahl"; werte: readonly string[] };
|
|
|
|
export type Spalte = {
|
|
/** Überschrift in der Datei. */
|
|
name: string;
|
|
/** Feld im Datensatz, den der Lader schreibt. */
|
|
ziel: string;
|
|
pflicht: boolean;
|
|
typ: FeldArt;
|
|
hinweis: string;
|
|
beispiel: string;
|
|
};
|
|
|
|
export type BlattSchema = {
|
|
name: string;
|
|
zweck: string;
|
|
/**
|
|
* Spalte, an der eine Zeile erkennbar ist. Doppelte Werte darin sind ein
|
|
* Fehler — sonst überschriebe die zweite Zeile die erste, und welche
|
|
* gewinnt, hinge an der Reihenfolge in der Datei.
|
|
*/
|
|
schluessel: string | null;
|
|
spalten: Spalte[];
|
|
};
|
|
|
|
const WOCHENTAGE = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] as const;
|
|
const VERHAELTNIS = ["Ehepartner:in", "Lebenspartner:in", "Kind", "Sonstige"] as const;
|
|
const EREIGNIS = [
|
|
"Eintritt",
|
|
"Beförderung",
|
|
"Versetzung",
|
|
"Karenz",
|
|
"Vertragsänderung",
|
|
"Stammdatenänderung",
|
|
"Austritt",
|
|
"Wiedereintritt",
|
|
"Reorganisation",
|
|
"Gehaltsanpassung",
|
|
"Rückkehr",
|
|
] as const;
|
|
|
|
export const BLATT_STANDORTE: BlattSchema = {
|
|
name: "Standorte",
|
|
zweck: "Betriebsstätten, auf die sich Personen beziehen.",
|
|
schluessel: "Bezeichnung",
|
|
spalten: [
|
|
{ name: "Bezeichnung", ziel: "name", pflicht: true, typ: { art: "text" }, hinweis: "Eindeutig.", beispiel: "Wien" },
|
|
{
|
|
name: "Land",
|
|
ziel: "country",
|
|
pflicht: true,
|
|
typ: { art: "text" },
|
|
hinweis: "Ausgeschrieben. Steuert unter anderem, ob eine SV-Nummer verlangt wird.",
|
|
beispiel: "Österreich",
|
|
},
|
|
],
|
|
};
|
|
|
|
export const BLATT_ORGANISATION: BlattSchema = {
|
|
name: "Organisation",
|
|
zweck: "Der Aufbau: Gesellschaft, Bereiche, Abteilungen, Teams.",
|
|
schluessel: "Orgnummer",
|
|
spalten: [
|
|
{
|
|
name: "Orgnummer",
|
|
ziel: "org_number",
|
|
pflicht: true,
|
|
typ: { art: "text" },
|
|
hinweis: "Eindeutig. Wird von Planstellen als Verweis benutzt.",
|
|
beispiel: "10000001",
|
|
},
|
|
{ name: "Bezeichnung", ziel: "name", pflicht: true, typ: { art: "text" }, hinweis: "", beispiel: "Produktion" },
|
|
{
|
|
name: "Art",
|
|
ziel: "unit_type",
|
|
pflicht: true,
|
|
typ: { art: "auswahl", werte: ["Gesellschaft", "Bereich", "Abteilung", "Team"] },
|
|
hinweis: "Genau eine Zeile darf „Gesellschaft“ sein — sie ist die Wurzel.",
|
|
beispiel: "Bereich",
|
|
},
|
|
{
|
|
name: "Übergeordnet",
|
|
ziel: "parent_org_number",
|
|
pflicht: false,
|
|
typ: { art: "text" },
|
|
hinweis: "Orgnummer der übergeordneten Einheit. Nur bei „Gesellschaft“ leer.",
|
|
beispiel: "10000000",
|
|
},
|
|
{
|
|
name: "Gültig ab",
|
|
ziel: "valid_from",
|
|
pflicht: false,
|
|
typ: { art: "datum" },
|
|
hinweis: "Leer = heute.",
|
|
beispiel: "01.01.2020",
|
|
},
|
|
{
|
|
name: "Gültig bis",
|
|
ziel: "valid_to",
|
|
pflicht: false,
|
|
typ: { art: "datum" },
|
|
hinweis: "Leer = unbefristet.",
|
|
beispiel: "",
|
|
},
|
|
],
|
|
};
|
|
|
|
export const BLATT_JOBKATALOG: BlattSchema = {
|
|
name: "Jobkatalog",
|
|
zweck: "Die Tätigkeiten, auf die Planstellen verweisen.",
|
|
schluessel: "Jobcode",
|
|
spalten: [
|
|
{ name: "Jobcode", ziel: "code", pflicht: true, typ: { art: "text" }, hinweis: "Eindeutig.", beispiel: "50000123" },
|
|
{
|
|
name: "Bezeichnung",
|
|
ziel: "title",
|
|
pflicht: true,
|
|
typ: { art: "text" },
|
|
hinweis: "",
|
|
beispiel: "Maschinenführer:in",
|
|
},
|
|
],
|
|
};
|
|
|
|
export const BLATT_PLANSTELLEN: BlattSchema = {
|
|
name: "Planstellen",
|
|
zweck: "Die Stellen selbst — unabhängig davon, wer sie besetzt.",
|
|
schluessel: "Planstellennummer",
|
|
spalten: [
|
|
{
|
|
name: "Planstellennummer",
|
|
ziel: "position_number",
|
|
pflicht: true,
|
|
typ: { art: "text" },
|
|
hinweis: "Eindeutig. Personen verweisen darauf.",
|
|
beispiel: "60000124",
|
|
},
|
|
{
|
|
name: "Orgnummer",
|
|
ziel: "org_number",
|
|
pflicht: true,
|
|
typ: { art: "text" },
|
|
hinweis: "Muss im Blatt „Organisation“ oder bereits im System stehen.",
|
|
beispiel: "10000001",
|
|
},
|
|
{
|
|
name: "Jobcode",
|
|
ziel: "job_code",
|
|
pflicht: true,
|
|
typ: { art: "text" },
|
|
hinweis: "Muss im Blatt „Jobkatalog“ oder bereits im System stehen.",
|
|
beispiel: "50000123",
|
|
},
|
|
{
|
|
name: "Leitung",
|
|
ziel: "is_chief",
|
|
pflicht: false,
|
|
typ: { art: "janein" },
|
|
hinweis: "Ja = Leitungsstelle der Einheit. Höchstens eine je Einheit.",
|
|
beispiel: "nein",
|
|
},
|
|
{ name: "Gültig ab", ziel: "valid_from", pflicht: false, typ: { art: "datum" }, hinweis: "Leer = heute.", beispiel: "01.01.2020" },
|
|
{ name: "Gültig bis", ziel: "valid_to", pflicht: false, typ: { art: "datum" }, hinweis: "", beispiel: "" },
|
|
],
|
|
};
|
|
|
|
export const BLATT_PERSONEN: BlattSchema = {
|
|
name: "Personen",
|
|
zweck: "Stammdaten, Vertrag und die Planstelle, auf der die Person sitzt.",
|
|
schluessel: "Personalnummer",
|
|
spalten: [
|
|
{
|
|
name: "Personalnummer",
|
|
ziel: "personnel_number",
|
|
pflicht: true,
|
|
typ: { art: "ganzzahl" },
|
|
hinweis: "Eindeutig. Historie und Angehörige verweisen darauf.",
|
|
beispiel: "2219",
|
|
},
|
|
{ name: "Vorname", ziel: "first_name", pflicht: true, typ: { art: "text" }, hinweis: "", beispiel: "Sabine" },
|
|
{ name: "Nachname", ziel: "last_name", pflicht: true, typ: { art: "text" }, hinweis: "", beispiel: "Aigner" },
|
|
{
|
|
name: "Geschlecht",
|
|
ziel: "gender",
|
|
pflicht: true,
|
|
typ: { art: "auswahl", werte: ["m", "w"] },
|
|
hinweis: "Wird für die SV-Nummer und die Anrede gebraucht.",
|
|
beispiel: "w",
|
|
},
|
|
{ name: "Geburtsdatum", ziel: "birth_date", pflicht: true, typ: { art: "datum" }, hinweis: "", beispiel: "15.08.1968" },
|
|
{
|
|
name: "SV-Nummer",
|
|
ziel: "sv_nummer",
|
|
pflicht: false,
|
|
typ: { art: "text" },
|
|
hinweis: "Zehnstellig. Prüfziffer und Geburtsdatum müssen zusammenpassen.",
|
|
beispiel: "7960 150868",
|
|
},
|
|
{
|
|
name: "Staatsbürgerschaft",
|
|
ziel: "nationality",
|
|
pflicht: false,
|
|
typ: { art: "text" },
|
|
hinweis: "Leer = Österreich.",
|
|
beispiel: "Österreich",
|
|
},
|
|
{
|
|
name: "Private E-Mail",
|
|
ziel: "email",
|
|
pflicht: false,
|
|
typ: { art: "text" },
|
|
hinweis: "Freiwillig. Eindeutig, wenn angegeben.",
|
|
beispiel: "s.aigner@example.at",
|
|
},
|
|
{ name: "Private Telefonnummer", ziel: "phone", pflicht: false, typ: { art: "text" }, hinweis: "", beispiel: "+43 660 1234567" },
|
|
{ name: "Adresse", ziel: "address", pflicht: false, typ: { art: "text" }, hinweis: "", beispiel: "Hauptstraße 1" },
|
|
{ name: "PLZ", ziel: "postal_code", pflicht: false, typ: { art: "text" }, hinweis: "", beispiel: "1010" },
|
|
{ name: "Ort", ziel: "city", pflicht: false, typ: { art: "text" }, hinweis: "", beispiel: "Wien" },
|
|
{ name: "Land", ziel: "address_country", pflicht: false, typ: { art: "text" }, hinweis: "", beispiel: "Österreich" },
|
|
{
|
|
name: "Titel vorangestellt",
|
|
ziel: "title_prefix",
|
|
pflicht: false,
|
|
// Feste Liste, weil die Datenbank eine CHECK-Bedingung darauf hat.
|
|
// Ohne die Aufzählung hier bräche der Import erst beim Schreiben ab.
|
|
typ: { art: "liste", werte: TITLE_PREFIXES },
|
|
hinweis: "Mehrere mit Semikolon. In einer CSV die Zelle in Anführungszeichen setzen.",
|
|
beispiel: "Mag.",
|
|
},
|
|
{
|
|
name: "Titel nachgestellt",
|
|
ziel: "title_suffix",
|
|
pflicht: false,
|
|
typ: { art: "liste", werte: TITLE_SUFFIXES },
|
|
hinweis: "Mehrere mit Semikolon.",
|
|
beispiel: "MSc",
|
|
},
|
|
{
|
|
name: "Tätigkeit",
|
|
ziel: "job_title",
|
|
pflicht: true,
|
|
typ: { art: "text" },
|
|
hinweis: "Die Bezeichnung der Person; kann von der Planstelle abweichen.",
|
|
beispiel: "Projektingenieur:in",
|
|
},
|
|
{
|
|
name: "Standort",
|
|
ziel: "location",
|
|
pflicht: true,
|
|
typ: { art: "text" },
|
|
hinweis: "Muss im Blatt „Standorte“ oder bereits im System stehen.",
|
|
beispiel: "Wien",
|
|
},
|
|
{
|
|
name: "Planstellennummer",
|
|
ziel: "position_number",
|
|
pflicht: true,
|
|
typ: { art: "text" },
|
|
hinweis: "Muss im Blatt „Planstellen“ oder bereits im System stehen und frei sein.",
|
|
beispiel: "60000124",
|
|
},
|
|
{ name: "Eintritt", ziel: "entry_date", pflicht: true, typ: { art: "datum" }, hinweis: "", beispiel: "01.03.2015" },
|
|
{
|
|
name: "Austritt",
|
|
ziel: "exit_date",
|
|
pflicht: false,
|
|
typ: { art: "datum" },
|
|
hinweis: "Leer = weiterhin beschäftigt. Muss nach dem Eintritt liegen.",
|
|
beispiel: "",
|
|
},
|
|
{ name: "Austrittsgrund", ziel: "exit_reason", pflicht: false, typ: { art: "text" }, hinweis: "Pflicht, wenn ein Austritt steht.", beispiel: "" },
|
|
{
|
|
name: "Beschäftigung",
|
|
ziel: "employment_type",
|
|
pflicht: false,
|
|
typ: { art: "auswahl", werte: ["Vollzeit", "Teilzeit"] },
|
|
hinweis: "Leer = Vollzeit.",
|
|
beispiel: "Vollzeit",
|
|
},
|
|
{ name: "Wochenstunden", ziel: "weekly_hours", pflicht: false, typ: { art: "zahl" }, hinweis: "Leer = 38,5.", beispiel: "38,5" },
|
|
{
|
|
name: "Arbeitstage",
|
|
ziel: "work_days",
|
|
pflicht: false,
|
|
typ: { art: "liste", werte: WOCHENTAGE },
|
|
hinweis: "Leer = Mo;Di;Mi;Do;Fr.",
|
|
beispiel: "Mo;Di;Mi;Do;Fr",
|
|
},
|
|
{
|
|
name: "Vertragsart",
|
|
ziel: "contract_type",
|
|
pflicht: false,
|
|
typ: { art: "auswahl", werte: ["unbefristet", "befristet"] },
|
|
hinweis: "Leer = unbefristet.",
|
|
beispiel: "unbefristet",
|
|
},
|
|
{
|
|
name: "Befristet bis",
|
|
ziel: "contract_end_date",
|
|
pflicht: false,
|
|
typ: { art: "datum" },
|
|
hinweis: "Pflicht bei „befristet“, sonst leer.",
|
|
beispiel: "",
|
|
},
|
|
{
|
|
name: "Verwendungsgruppe",
|
|
ziel: "paygrade",
|
|
pflicht: false,
|
|
typ: { art: "auswahl", werte: ["A", "B", "C", "D", "E", "F"] },
|
|
hinweis: "Leer = B.",
|
|
beispiel: "C",
|
|
},
|
|
{
|
|
name: "Kollektivvertrag",
|
|
ziel: "collective_agreement",
|
|
pflicht: false,
|
|
typ: { art: "auswahl", werte: ["Handel", "Süßwaren"] },
|
|
hinweis: "Leer = Handel.",
|
|
beispiel: "Handel",
|
|
},
|
|
{
|
|
name: "Beschäftigtengruppe",
|
|
ziel: "worker_type",
|
|
pflicht: false,
|
|
typ: { art: "auswahl", werte: ["Angestellte:r", "Arbeiter:in", "Lehrling"] },
|
|
hinweis: "Leer = Angestellte:r.",
|
|
beispiel: "Angestellte:r",
|
|
},
|
|
{ name: "Monatsgehalt brutto", ziel: "monthly_salary_gross", pflicht: false, typ: { art: "zahl" }, hinweis: "", beispiel: "3450,00" },
|
|
{
|
|
name: "Herkunft",
|
|
ziel: "source",
|
|
pflicht: false,
|
|
typ: { art: "auswahl", werte: ["Intern", "Extern"] },
|
|
hinweis: "Leer = Extern.",
|
|
beispiel: "Extern",
|
|
},
|
|
{ name: "Betriebsrat", ziel: "is_betriebsrat", pflicht: false, typ: { art: "janein" }, hinweis: "Leer = nein.", beispiel: "nein" },
|
|
{ name: "Dienstwagen", ziel: "has_dienstwagen", pflicht: false, typ: { art: "janein" }, hinweis: "", beispiel: "nein" },
|
|
{
|
|
name: "Antriebsart",
|
|
ziel: "dienstwagen_art",
|
|
pflicht: false,
|
|
typ: { art: "auswahl", werte: ["Verbrenner", "Elektro"] },
|
|
hinweis: "Pflicht, wenn ein Dienstwagen eingetragen ist; sonst leer lassen.",
|
|
beispiel: "",
|
|
},
|
|
{
|
|
name: "Notfallkontakt",
|
|
ziel: "emergency_contact_name",
|
|
pflicht: false,
|
|
typ: { art: "text" },
|
|
hinweis: "Name. Nur zusammen mit der Telefonnummer.",
|
|
beispiel: "",
|
|
},
|
|
{
|
|
name: "Notfallkontakt Telefon",
|
|
ziel: "emergency_contact_phone",
|
|
pflicht: false,
|
|
typ: { art: "text" },
|
|
hinweis: "Nur zusammen mit dem Namen.",
|
|
beispiel: "",
|
|
},
|
|
{
|
|
name: "Notfallkontakt Verhältnis",
|
|
ziel: "emergency_contact_relation",
|
|
pflicht: false,
|
|
typ: { art: "text" },
|
|
hinweis: "Zum Beispiel Gattin, Bruder, Freundin.",
|
|
beispiel: "",
|
|
},
|
|
{ name: "Laterale Führung", ziel: "is_laterale_fuehrung", pflicht: false, typ: { art: "janein" }, hinweis: "", beispiel: "nein" },
|
|
{ name: "C-Level", ziel: "is_c_level", pflicht: false, typ: { art: "janein" }, hinweis: "", beispiel: "nein" },
|
|
{
|
|
name: "Besonderer Kündigungsschutz",
|
|
ziel: "has_kuendigungsschutz",
|
|
pflicht: false,
|
|
typ: { art: "janein" },
|
|
hinweis: "Leer = nein.",
|
|
beispiel: "nein",
|
|
},
|
|
{
|
|
name: "Kündigungsschutz bis",
|
|
ziel: "kuendigungsschutz_bis",
|
|
pflicht: false,
|
|
typ: { art: "datum" },
|
|
hinweis: "Nur mit gesetztem Schutz; freiwillig.",
|
|
beispiel: "",
|
|
},
|
|
{
|
|
name: "Abwesenheit ab",
|
|
ziel: "karenz_start_date",
|
|
pflicht: false,
|
|
typ: { art: "datum" },
|
|
hinweis: "Beginn einer Langzeitabwesenheit.",
|
|
beispiel: "",
|
|
},
|
|
{
|
|
name: "Rückkehr geplant",
|
|
ziel: "karenz_return_date",
|
|
pflicht: false,
|
|
typ: { art: "datum" },
|
|
hinweis: "Pflicht, wenn „Abwesenheit ab“ steht.",
|
|
beispiel: "",
|
|
},
|
|
{
|
|
name: "Abwesenheitsart",
|
|
ziel: "absence_type",
|
|
pflicht: false,
|
|
typ: { art: "auswahl", werte: ABSENCE_TYPES },
|
|
hinweis: "Pflicht, wenn „Abwesenheit ab“ steht.",
|
|
beispiel: "",
|
|
},
|
|
],
|
|
};
|
|
|
|
export const BLATT_HISTORIE: BlattSchema = {
|
|
name: "Historie",
|
|
zweck: "Ereignisse je Person. Alles, was vor dem Eintritt liegt, wird abgewiesen.",
|
|
schluessel: null,
|
|
spalten: [
|
|
{
|
|
name: "Personalnummer",
|
|
ziel: "personnel_number",
|
|
pflicht: true,
|
|
typ: { art: "ganzzahl" },
|
|
hinweis: "Muss im Blatt „Personen“ oder bereits im System stehen.",
|
|
beispiel: "2219",
|
|
},
|
|
{ name: "Datum", ziel: "event_date", pflicht: true, typ: { art: "datum" }, hinweis: "", beispiel: "01.03.2015" },
|
|
{
|
|
name: "Ereignis",
|
|
ziel: "event_type",
|
|
pflicht: true,
|
|
typ: { art: "auswahl", werte: EREIGNIS },
|
|
hinweis: "",
|
|
beispiel: "Eintritt",
|
|
},
|
|
{ name: "Beschreibung", ziel: "description", pflicht: true, typ: { art: "text" }, hinweis: "", beispiel: "Eintritt als Projektingenieur:in" },
|
|
],
|
|
};
|
|
|
|
export const BLATT_ANGEHOERIGE: BlattSchema = {
|
|
name: "Angehörige",
|
|
zweck: "Angehörige je Person.",
|
|
schluessel: null,
|
|
spalten: [
|
|
{ name: "Personalnummer", ziel: "personnel_number", pflicht: true, typ: { art: "ganzzahl" }, hinweis: "", beispiel: "2219" },
|
|
{ name: "Vorname", ziel: "first_name", pflicht: true, typ: { art: "text" }, hinweis: "", beispiel: "Julian" },
|
|
{ name: "Nachname", ziel: "last_name", pflicht: true, typ: { art: "text" }, hinweis: "", beispiel: "Aigner" },
|
|
{
|
|
name: "Verhältnis",
|
|
ziel: "relationship",
|
|
pflicht: true,
|
|
typ: { art: "auswahl", werte: VERHAELTNIS },
|
|
hinweis: "",
|
|
beispiel: "Kind",
|
|
},
|
|
{ name: "Geburtsdatum", ziel: "birth_date", pflicht: true, typ: { art: "datum" }, hinweis: "", beispiel: "04.06.2014" },
|
|
{ name: "SV-Nummer", ziel: "sv_nummer", pflicht: false, typ: { art: "text" }, hinweis: "", beispiel: "" },
|
|
],
|
|
};
|
|
|
|
/**
|
|
* Reihenfolge = Ladereihenfolge.
|
|
*
|
|
* Standorte, Organisation und Jobkatalog müssen stehen, bevor Planstellen
|
|
* darauf verweisen können; Personen brauchen Planstellen; Historie und
|
|
* Angehörige brauchen Personen.
|
|
*/
|
|
export const BLAETTER: BlattSchema[] = [
|
|
BLATT_STANDORTE,
|
|
BLATT_ORGANISATION,
|
|
BLATT_JOBKATALOG,
|
|
BLATT_PLANSTELLEN,
|
|
BLATT_PERSONEN,
|
|
BLATT_HISTORIE,
|
|
BLATT_ANGEHOERIGE,
|
|
];
|
|
|
|
export function blattSchema(name: string): BlattSchema | undefined {
|
|
const gesucht = name.trim().toLowerCase();
|
|
return BLAETTER.find((b) => b.name.toLowerCase() === gesucht);
|
|
}
|