Files
alpenwerk-hr/lib/colors.ts
Andrei Laas 7a33e493b5
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 10m59s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m0s
Die Historie bekommt ihre eigenen Farben
In "Letzte Aktivitaeten" stand "Eintritt" weiter auf Rosa, waehrend die Karte
daneben ihn laengst gruen zeigte. Dieselbe Ursache wie bei den
Anstehend-Chips, nur eine Ecke weiter: die Uebersicht zeigt Ereignisse aus
employee_history, holte ihre Farbe aber aus ACTION_CATEGORY — und das ist die
Sprache des Protokolls. Dort heisst es "Neueinstellung" und
"Wiedereinstellung", in der Historie "Eintritt" und "Wiedereintritt". Genau
diese zwei von elf standen nicht darin und fielen auf den neutralen Chip
zurueck; die uebrigen neun trafen zufaellig.

EVENT_CATEGORY ist jetzt die Zuordnung fuer die Historie, als
Record<HistoryEventType, …> und damit vollzaehlig: ein zwoelftes Ereignis
laesst der Typpruefer nicht durch, ohne dass jemand eine Farbe dafuer
bestimmt. Ein Nachschlagen mit Rueckfall haette auch dann wieder still etwas
Plausibles geliefert.

Betroffen war nicht nur die Uebersicht — der Historie-Reiter in der
Personalakte faerbte seine Chips und seine Filterknoepfe aus derselben
falschen Tabelle. Auch die sind umgestellt.

Die Punkte vor den Zeilen lagen in einer zweiten Tabelle in page.tsx und
sagten fuer "Eintritt" bereits gruen — Punkt und Chip derselben Zeile kamen
also aus zwei Verzeichnissen, von denen eines das falsche war. Beide leiten
jetzt aus EVENT_CATEGORY ab.

Die Rueckkehr ist dabei violett geworden, auch in der Historie: auf der
Uebersicht steht sie neben dem Eintritt, und zwei Gruentoene nebeneinander
sind keine zwei Dinge. ANSTEHEND_STYLES leitet fuer Eintritt, Austritt und
Rueckkehr aus derselben Tabelle ab — die beiden Karten koennen nicht mehr
auseinanderlaufen.

ACTION_CATEGORY behaelt seinen Rueckfall, und das bleibt richtig: die
Aktionen schreiben die SQL-Funktionen als freien Text, eine neue kann
jederzeit dazukommen, und ihr neutraler Chip ist dann eine ehrliche Aussage.
Fuer eine geschlossene Aufzaehlung war derselbe Rueckfall ein Fehler.

Acht Tests, aus EVENT_TYPE_LABELS abgeleitet statt abgeschrieben: dass jedes
Ereignis eine Farbe hat, dass keines den neutralen Chip bekommt, dass Punkt
und Chip derselben Zeile zusammenpassen und dass die beiden Karten der
Uebersicht dasselbe meinen.

Lint, Typen, Schemaabgleich, 575 Tests und der Build sind sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:48:54 +02:00

183 lines
7.4 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",
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",
};