Aus dem Gespraech vom 17.09.2026. Migrationen 20260917110000 (Enum-Wert) und 20260917120000 (Logik) — getrennt, weil ein in derselben Transaktion angelegter Enum-Wert dort noch nicht benutzt werden darf. Die Besetzungsart (extern/intern) wurde bisher nur bei der Neueinstellung gesetzt und war danach unerreichbar. Eine Angabe, die mit dem ersten Tag erstarrt, obwohl gerade ihr Wechsel der Vorgang ist, um den es geht. Sie steht jetzt in "Daten aendern". Wechselt sie von Extern auf Intern, entsteht das Ereignis "Uebernahme" — in der Personalakte und im Protokoll, und im Berichtemanager als Ereignistyp auswertbar. Der umgekehrte Weg bekommt ausdruecklich keines: eine Uebernahme zurueckzunehmen gibt es fachlich nicht. Zwei Eintraege und nicht einer: die Vertragsaenderung haelt fest, dass ein Feld sich geaendert hat und worauf (und laesst sich darueber zuruecknehmen), das Ereignis, dass dieser Wechsel eine Uebernahme war. Nur das zweite laesst sich zaehlen. Eine auf spaeter datierte Uebernahme erzeugt das Ereignis sofort mit pending_id, wie die Vertragsaenderung daneben; der Nachtlauf schreibt am Stichtag nur noch die Spalte. Die Selbstpruefung haelt fest, dass er kein zweites Ereignis schreibt — sonst staende die Uebernahme doppelt in der Akte und jede Auswertung zaehlte sie zweimal. Im Assistenten erscheint das Feld nicht: dort steht die Besetzungsart im Schritt "Position", und zweimal danach zu fragen waere eine Einladung, zwei verschiedene Antworten zu geben.
186 lines
7.5 KiB
TypeScript
186 lines
7.5 KiB
TypeScript
import type { AnstehendArt } from "./dashboard-filter";
|
|
import type { EmploymentStatus, HistoryEventType, NoteCategory } from "./types";
|
|
|
|
// Die Initialen stehen in Weiss darauf, jeder Ton braucht also 4.5:1 gegen
|
|
// Weiss — nachgerechnet liegt der schwaechste bei 6.11:1. Die Reihe beginnt
|
|
// bei den beiden Markenfarben (Blau direkt, Rosa in der dunklen Stufe, weil
|
|
// #F69686 selbst nur 2.18:1 traegt) und faechert von dort auf, damit zwei
|
|
// Personen nebeneinander nicht denselben Kreis bekommen.
|
|
const AVATAR_PALETTE = [
|
|
"#164194", // Manner Blau
|
|
"#0d2857",
|
|
"#a8402e", // dunkles Manner Rosa
|
|
"#5c2e91",
|
|
"#0e700e",
|
|
"#835b00",
|
|
"#00666d",
|
|
"#8a2740",
|
|
];
|
|
|
|
// Stable per-employee avatar color when employees.avatar_color isn't set.
|
|
export function avatarColorFor(seed: string): string {
|
|
let hash = 0;
|
|
for (let i = 0; i < seed.length; i++) {
|
|
hash = (hash << 5) - hash + seed.charCodeAt(i);
|
|
hash |= 0;
|
|
}
|
|
return AVATAR_PALETTE[Math.abs(hash) % AVATAR_PALETTE.length];
|
|
}
|
|
|
|
export const STATUS_STYLES: Record<EmploymentStatus, string> = {
|
|
Aktiv: "bg-success-bg text-success-text",
|
|
Karenz: "bg-warning-bg text-warning-text",
|
|
// Rosa statt Blau: „Geplant" ist kein Hinweis und keine Warnung, sondern
|
|
// der Normalfall ohne besondere Faerbung — und dafuer ist die Markenfarbe
|
|
// die richtige. Blau wuerde hier nach Bedienelement aussehen.
|
|
Geplant: "bg-accent-200 text-brand-700",
|
|
Ausgetreten: "bg-danger-bg text-danger-text",
|
|
};
|
|
|
|
type ColorCategory = "success" | "danger" | "warning" | "info" | "purple" | "brand";
|
|
|
|
export const CATEGORY_STYLES: Record<ColorCategory, string> = {
|
|
success: "bg-success-bg text-success-text",
|
|
danger: "bg-danger-bg text-danger-text",
|
|
warning: "bg-warning-bg text-warning-text",
|
|
info: "bg-info-bg text-info-text",
|
|
purple: "bg-purple-bg text-purple-text",
|
|
// Der Chip ohne eigene Bedeutung. Auf accent-200 statt accent-100, weil
|
|
// die hellere Stufe dem Fehler-Chip zu nahe kommt (dE 5.6 gegen 13.7).
|
|
brand: "bg-accent-200 text-brand-700",
|
|
};
|
|
|
|
/** Der Punkt vor einer Zeile: dieselbe Kategorie, nur als Flaeche. */
|
|
const CATEGORY_DOT: Record<ColorCategory, string> = {
|
|
success: "bg-success-text",
|
|
danger: "bg-danger-text",
|
|
warning: "bg-warning-text",
|
|
info: "bg-info-text",
|
|
purple: "bg-purple-text",
|
|
brand: "bg-accent-600",
|
|
};
|
|
|
|
/**
|
|
* Die Ereignisse der Personalakte — die Sprache von employee_history.
|
|
*
|
|
* Getrennt von ACTION_CATEGORY darunter, und das ist der Punkt: die beiden
|
|
* sind zwei Vokabulare fuer dasselbe Geschehen. Das Protokoll schreibt
|
|
* „Neueinstellung", die Historie schreibt „Eintritt"; „Wiedereinstellung"
|
|
* steht dort als „Wiedereintritt". Die Uebersicht zeigt Historienereignisse,
|
|
* holte ihre Farbe aber aus der Protokolltabelle — und genau diese zwei
|
|
* standen nicht darin und bekamen den neutralen Chip. In „Letzte
|
|
* Aktivitaeten" war „Eintritt" deshalb rosa, waehrend die Karte daneben ihn
|
|
* laengst gruen zeigte.
|
|
*
|
|
* `Record<HistoryEventType, …>` statt `Record<string, …>`: kaeme ein
|
|
* zwoelftes Ereignis dazu, liesse der Typpruefer es nicht durch, ohne dass
|
|
* jemand eine Farbe dafuer bestimmt. Ein Nachschlagen mit Rueckfall haette
|
|
* auch dann wieder still etwas Plausibles geliefert.
|
|
*/
|
|
export const EVENT_CATEGORY: Record<HistoryEventType, ColorCategory> = {
|
|
Eintritt: "success",
|
|
Wiedereintritt: "success",
|
|
Austritt: "danger",
|
|
// Violett und nicht gruen: auf der Uebersicht steht die Rueckkehr neben dem
|
|
// Eintritt, und zwei Gruentoene nebeneinander sind keine zwei Dinge. Die
|
|
// Begruendung samt Messung steht bei ANSTEHEND_STYLES.
|
|
Rückkehr: "purple",
|
|
Versetzung: "info",
|
|
// Wie die Versetzung: eine Bewegung innerhalb des Hauses, kein Anfang und
|
|
// kein Ende. Gruen waere es nicht — die Person war schon da.
|
|
Übernahme: "info",
|
|
Beförderung: "purple",
|
|
Reorganisation: "purple",
|
|
Karenz: "warning",
|
|
Vertragsänderung: "warning",
|
|
Stammdatenänderung: "warning",
|
|
Gehaltsanpassung: "warning",
|
|
};
|
|
|
|
export function eventBadgeStyle(event: HistoryEventType): string {
|
|
return CATEGORY_STYLES[EVENT_CATEGORY[event]];
|
|
}
|
|
|
|
export function eventDotStyle(event: HistoryEventType): string {
|
|
return CATEGORY_DOT[EVENT_CATEGORY[event]];
|
|
}
|
|
|
|
// Audit-log action -> badge color, per the action list in dem Kommentar an
|
|
// audit_log in der ersten Migration.
|
|
//
|
|
// Hier ist der Rueckfall richtig und bleibt: die Aktionen schreiben die
|
|
// SQL-Funktionen als freien Text, und eine neue kann jederzeit dazukommen.
|
|
// Ihr neutraler Chip ist dann eine ehrliche Aussage. Fuer die Historie gilt
|
|
// das nicht — deren Werte sind eine geschlossene Aufzaehlung, und dort war
|
|
// derselbe Rueckfall ein Fehler.
|
|
const ACTION_CATEGORY: Record<string, ColorCategory> = {
|
|
Neueinstellung: "success",
|
|
Wiedereinstellung: "success",
|
|
Rückkehr: "success",
|
|
Austritt: "danger",
|
|
Versetzung: "info",
|
|
Ausschreibung: "info",
|
|
"Interne Besetzung": "info",
|
|
Beförderung: "purple",
|
|
Reorganisation: "purple",
|
|
"Reorganisation rückgängig": "purple",
|
|
Karenz: "warning",
|
|
Vertragsänderung: "warning",
|
|
Stammdatenänderung: "warning",
|
|
Gehaltsanpassung: "warning",
|
|
};
|
|
|
|
export function actionBadgeStyle(action: string): string {
|
|
return CATEGORY_STYLES[ACTION_CATEGORY[action] ?? "brand"];
|
|
}
|
|
|
|
/**
|
|
* Die vier Arten auf der Karte „Anstehend".
|
|
*
|
|
* Eigene Zuordnung statt actionBadgeStyle(): das war der Fehler, den sie
|
|
* ersetzt. Dort wird ueber den **Beschriftungstext** in die Tabelle der
|
|
* Protokoll-Aktionen nachgeschlagen, und die kennt eine andere Sprache —
|
|
* „Neueinstellung", nicht „Eintritt". Zwei der vier Beschriftungen standen
|
|
* gar nicht darin und fielen auf den neutralen Rosa-Chip zurueck. Ein
|
|
* Nachschlagen, das bei einem Fehlschlag still etwas Plausibles liefert,
|
|
* faellt eben nicht auf.
|
|
*
|
|
* Deshalb hier: nach dem **Wert** verschluesselt, nicht nach der Beschriftung
|
|
* — eine umbenannte Beschriftung kann die Farben so nicht mehr stillschweigend
|
|
* verstellen. Und vollstaendig ueber `Record<AnstehendArt, …>`: eine fuenfte
|
|
* Art muesste sich hier eintragen, der Typpruefer laesst sie sonst nicht durch.
|
|
*
|
|
* Die Farben selbst, nebeneinander gemessen (dE im CIELAB, Kontrast nach
|
|
* WCAG — schwaechstes Paar 14.2, schwaechster Kontrast 5.49:1):
|
|
*
|
|
* Eintritt gruen jemand kommt
|
|
* Austritt rot jemand geht
|
|
* Rueckkehr violett weder das eine noch das andere — eine eigene Klasse
|
|
* Wiedervorlage gelb ein Termin, an dem etwas zu tun ist
|
|
*
|
|
* Tuerkis fuer die Rueckkehr waere die naheliegendere Lesart gewesen, kam
|
|
* gegen das Gruen des Eintritts aber nur auf dE 13.0. Violett steht mit 30.8
|
|
* eindeutig daneben, und die vier sind das, was sie sein sollen: vier Dinge,
|
|
* die man im Vorbeigehen auseinanderhaelt.
|
|
*/
|
|
export const ANSTEHEND_STYLES: Record<AnstehendArt, string> = {
|
|
// Aus EVENT_CATEGORY abgeleitet, wo es dasselbe meint: auf der Uebersicht
|
|
// stehen die beiden Karten nebeneinander, und ein Eintritt darf links nicht
|
|
// anders aussehen als rechts. Genau das war zu sehen, bevor die Historie
|
|
// ihre eigene Zuordnung bekam.
|
|
hire: CATEGORY_STYLES[EVENT_CATEGORY.Eintritt],
|
|
exit: CATEGORY_STYLES[EVENT_CATEGORY.Austritt],
|
|
return: CATEGORY_STYLES[EVENT_CATEGORY.Rückkehr],
|
|
// Die Wiedervorlage ist kein Ereignis der Personalakte, sondern ein Termin
|
|
// aus den Notizen — sie hat in EVENT_CATEGORY nichts verloren.
|
|
note: CATEGORY_STYLES.warning,
|
|
};
|
|
|
|
export const NOTE_CATEGORY_STYLES: Record<NoteCategory, string> = {
|
|
Vertraulich: CATEGORY_STYLES.purple,
|
|
"Personalgespräch": CATEGORY_STYLES.info,
|
|
Wiedervorlage: CATEGORY_STYLES.warning,
|
|
"Lob / Anerkennung": CATEGORY_STYLES.success,
|
|
Allgemein: "bg-surface text-ink-muted",
|
|
};
|