**Das Logo auf der Anmeldeseite.** Statt des Schriftzugs stand dort der Ersatztext "Manner". Die SVG-Fassung war gueltiges XML, lag im Repository und war committet — warum sie im Betrieb nicht geladen wurde, laesst sich von hier aus nicht feststellen; dafuer braucht es die Antwort des Servers auf die URL. Statt das weiter zu raten, faellt die Angriffsflaeche weg: es gibt jetzt eine Datei, manner-logo.png, der Schriftzug mit durchsichtigem Grund. Kein XML, kein Beschnitt, kein eingebackenes Feld. Das Blau darin ist #164194, also der Wert aus §1.1. Damit verschwinden auch die zwei Fassungen. §1.2 laesst den Schriftzug nur zur Gaenze auf Rosa zu, und das Manual kennt dafuer zwei Lagen: auf Weiss gehoert er nach §4.1 in ein rechteckiges rosa Feld, in einem grossflaechigen rosa Umfeld nach §3.1 nur mit Freiraum. Beides entsteht jetzt aus derselben Datei — das Feld zeichnet das Bauteil, aus derselben Polsterung wie den Freiraum. **Der Filter "Geplant" fand Ausgetretene.** Die Ableitung pruefte den Eintritt vor dem Austritt, und wer einen Eintritt in der Zukunft hatte, galt als geplant — auch wenn der Austritt laengst verbucht war. Das trifft genau den No-Show (Migration 20260814100000): eingestellt, nie erschienen, Austritt vor dem Eintrittstag. Im Bestand sind das Zeilen mit Eintritt 01.10.2026, die der Filter mitzaehlte, waehrend die Liste daneben "Ausgetreten" anzeigte. employees.status, das die SQL-Funktion beim Austritt setzt, sagte von Anfang an das Richtige; falsch war die Ableitung in der Anwendung. Ein abgeschlossener Austritt wird jetzt zuerst geprueft: er beendet das Verhaeltnis, gleichgueltig ob der Eintritt schon war oder noch kommt. Ein Austritt, der selbst noch bevorsteht, nimmt den Eintritt nicht zurueck — wer am 01.10. anfaengt und am 31.12. aufhoert, ist heute geplant. Die SQL-Fassung in lib/employee-status-filter.ts bildet dieselbe Reihenfolge ab. Keine Migration: beide Fassungen der Regel liegen in TypeScript. In SQL wird nur der Karenz-Teil wiederholt, fuer die Fuehrungslinie, und der ist nicht betroffen. **Die vier Anstehend-Chips.** Zwei davon standen in der Markenfarbe, weil die Farbe ueber den Beschriftungstext aus der Tabelle der Protokoll-Aktionen geholt wurde — und die kennt eine andere Sprache: "Neueinstellung", nicht "Eintritt". Wer dort nicht steht, bekam den neutralen Chip. Ein Nachschlagen, das bei einem Fehlschlag still etwas Plausibles liefert, faellt eben nicht auf. Die vier haben jetzt eine eigene Zuordnung, nach dem Wert verschluesselt und nicht nach der Beschriftung: Eintritt gruen, Austritt rot, Wiedervorlage gelb, Rueckkehr violett. Tuerkis waere fuer die Rueckkehr die naheliegendere Lesart gewesen, kam gegen das Gruen des Eintritts aber nur auf dE 13.0; Violett steht mit 30.8 eindeutig daneben. Schwaechstes Paar der vier: 14.2, schwaechster Kontrast 5.49:1. Zehn Tests dazu, darunter die drei Faelle, an denen der Filter gescheitert war. Lint, Typen, Schemaabgleich, 562 Tests und der Build sind sauber. Im Browser nicht gesehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
4.6 KiB
TypeScript
119 lines
4.6 KiB
TypeScript
import type { AnstehendArt } from "./dashboard-filter";
|
|
import type { EmploymentStatus, 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",
|
|
};
|
|
|
|
// Audit-log / activity-feed action -> badge color, per the action list in
|
|
// dem Kommentar an audit_log in der ersten Migration.
|
|
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> = {
|
|
hire: CATEGORY_STYLES.success,
|
|
exit: CATEGORY_STYLES.danger,
|
|
return: CATEGORY_STYLES.purple,
|
|
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",
|
|
};
|