Works council members, expectant mothers, parents on leave, registered disabled employees, apprentices — each has its own rules that come before a dismissal. The tool does not judge whether one is lawful, but it must not stay quiet about it either, and looking it up on the contract tab is exactly the step that gets skipped under time pressure. So: a checkbox, an optional end date, and a red warning at the top of the termination panel naming the date — or saying plainly that no end was recorded. It shows for a no-show too; the protection runs from the start of the contract, not the first day worked. The date is optional on purpose. A works council mandate has a known end, a pregnancy does not, and a mandatory field would force an invented number. A constraint says only what cannot be: an end date without the flag, which would be a leftover nobody could interpret. The field goes the whole way through — hire, data change, contract sheet, export, report criteria (as a yes/no and as a date range), and the import. A field that exists in one screen and not the next is how people stop trusting the numbers. Terminating is now offered for planned entries as well, labelled "Nicht angetreten", with No Show preselected. Without it a person who never turned up stayed a planned entry forever, since nothing else can end one. One finding worth recording: tsc has been reporting success on a broken program. A generated file under .next got corrupted when a build ran against a live dev server, and its syntax errors suppressed semantic checking everywhere else — two genuine type errors in this change went unreported until I typechecked with .next excluded. The file is removed and the ordinary typecheck is meaningful again. 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"] },
|
|
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);
|
|
}
|