diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 53741d5..937a8d0 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -57,7 +57,10 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
-
+ {/* Ohne Versatz: hier steht das Logo auf Weiss und bekommt nach §4.1 + sein rosa Feld, dessen Kante mit der Ueberschrift darunter + fluchten soll. */} +
@@ -140,15 +143,15 @@ function BrandPanel() { }} /> - {/* -ml-4 gleicht den Freiraum des Logos aus, damit die Wortmarke - buendig mit dem Text darunter beginnt und nicht eingerueckt wirkt. */} + {/* -ml-3 gleicht den Freiraum des Logos aus (hoehe 36 / 3 = 12 px), + damit der Schriftzug buendig mit dem Text darunter beginnt. */} {/* Alles Geschriebene hier steht in brand-700. Weiss faellt auf Manner Rosa mit 2.18:1 aus, und ink traegt zwar (7.47:1), sieht aber aus wie Text, der aus Versehen auf der Marke gelandet ist. Dunkelblau auf Rosa (6.60:1) ist dieselbe Paarung, aus der der Schriftzug selbst besteht. Die Abstufung macht Groesse und Gewicht, nicht Transparenz: eine aufgehellte Schrift waere genau das, was hier durchfaellt. */} -
+
diff --git a/components/brand/Logo.tsx b/components/brand/Logo.tsx index 1b88b65..9462610 100644 --- a/components/brand/Logo.tsx +++ b/components/brand/Logo.tsx @@ -6,41 +6,35 @@ // Verwendet wird das **Markenlogo** (CD §4.1): der Schriftzug allein, ohne // den Stephansdom-Medaillon. Das Unternehmenslogo (§3.1) waere nach §3.2 das // formal richtige, wo „Josef Manner & Comp. AG" als Absender auftritt — es -// steht hier aber nicht zur Verfuegung, und das Markenlogo ist die Fassung, -// die vorliegt. +// steht hier aber nicht zur Verfuegung. // -// ═══ Freiraum ═══ +// ═══ Eine Datei, zwei Faelle ═══ // -// §4.1 verlangt „genuegend Freiraum rundum (X, X/3)". X ist der Modul des -// Logos; gelesen wird er hier als seine Hoehe, der Freiraum also als ein -// Drittel davon. Er steckt als Polsterung in diesem Bauteil und nicht in den -// Aufrufstellen — sonst haengt die Einhaltung daran, dass jede einzelne -// daran denkt. +// Die Vorlage ist ein PNG mit durchsichtigem Grund: nur der Schriftzug, blau +// (#164194, der Wert aus §1.1) mit weisser Kontur, kein Feld darunter. // -// ═══ Rosa — und warum es zwei Dateien gibt ═══ +// §1.2 laesst den Schriftzug ausschliesslich zur Gaenze auf Rosa zu, und das +// Manual kennt dafuer zwei Lagen: // -// §1.2: der Schriftzug darf ausschliesslich zur Gaenze auf Rosa stehen. Das -// Manual kennt dafuer zwei Faelle, und sie brauchen verschiedene Dateien: +// • **In einem grossflaechigen rosa Umfeld** (§3.1) wird er „mit genuegend +// Freiraum rundum integriert" — die Flaeche ist schon da. Das ist +// `aufRosa`: nur Freiraum, kein eigener Grund. // -// • **Auf Weiss** (§4.1) gehoert das Logo „in ein rechteckiges rosa Feld". -// Das ist manner-logo.svg — der Schriftzug samt seiner rosa Flaeche. +// • **Auf Weiss** (§4.1) gehoert er „in ein rechteckiges rosa Feld, das den +// Regeln des Freiraums folgt". Dieses Feld zeichnet das Bauteil selbst, +// aus derselben Polsterung. // -// • **In einem grossflaechigen rosa Umfeld** (§3.1) wird es „mit genuegend -// Freiraum rundum integriert" — die Flaeche ist dann schon da. Dafuer -// gibt es manner-logo-auf-rosa.svg, den Schriftzug ohne eigenes Feld. +// Vorher lagen dafuer zwei SVG nebeneinander, eines mit eingebackenem Feld +// und eines ohne. Das war eine Datei zu viel — und die Fassung mit Feld liess +// sich auf einer rosa Flaeche nicht sauber unterbringen, weil ihr Rechteck +// sichtbar blieb, sobald ueber der Flaeche noch irgendetwas lag. // -// Die zweite Fassung ist nicht nur schoener, sie ist noetig: mit Feld stand -// auf dem rosa Panel ein Rechteck in leicht anderem Ton. Der Ton war dabei -// derselbe — was ihn verschob, war die Lichtblende, die ueber der Panelflaeche -// liegt und unter dem Logo endet. Ohne eigenes Feld gibt es nichts mehr, wo -// sie enden koennte. -// -// Als und nicht inline: die Datei holt der Browser einmal und nimmt sie -// danach aus dem Zwischenspeicher, statt in jeder Seitenauslieferung -// mitzureisen. +// Als und nicht inline: der Browser holt die Datei einmal und nimmt sie +// danach aus dem Zwischenspeicher, statt sie in jeder Seitenauslieferung +// mitzuschleppen. -/** Seitenverhaeltnis beider Dateien: viewBox 288.6781 x 116.18912. */ -const VERHAELTNIS = 288.6781 / 116.18912; +/** Seitenverhaeltnis der Vorlage: 1203 x 484 Pixel. */ +const VERHAELTNIS = 1203 / 484; export function MannerLogo({ hoehe = 28, @@ -48,26 +42,27 @@ export function MannerLogo({ className = "", }: { hoehe?: number; - /** Fuer rosa Untergruende: die Fassung ohne eigenes Feld. */ + /** Fuer rosa Untergruende: ohne eigenes Feld, nur mit Freiraum. */ aufRosa?: boolean; className?: string; }) { const breite = Math.round(hoehe * VERHAELTNIS); + // Der Freiraum nach §4.1: X/3, wobei X als Hoehe des Schriftzugs gelesen + // ist. Er steckt hier und nicht in den Aufrufstellen — sonst haengt seine + // Einhaltung daran, dass jede einzelne daran denkt. + const freiraum = Math.round(hoehe / 3); + return ( - // next/image bringt Groessenvarianten und Ladelogik fuer Fotos mit; fuer - // eine Vektordatei fester Groesse ist beides Ballast, und die - // Bildoptimierung laesst SVG ohnehin unveraendert durch. - // eslint-disable-next-line @next/next/no-img-element - Manner + + {/* next/image bringt Groessenvarianten und Ladelogik fuer Fotos mit; fuer + eine Bildmarke fester Groesse ist beides Ballast. */} + {/* eslint-disable-next-line @next/next/no-img-element */} + Manner + ); } @@ -89,7 +84,9 @@ export function AppWortmarke({ className?: string; }) { return ( - + // Nur ein schmaler Abstand: den Hauptteil der Trennung leistet der + // Freiraum des Logos, der ohnehin frei bleiben muss. + Alpenwerk HR diff --git a/components/dashboard/AnstehendFilter.tsx b/components/dashboard/AnstehendFilter.tsx index 091475a..8759c13 100644 --- a/components/dashboard/AnstehendFilter.tsx +++ b/components/dashboard/AnstehendFilter.tsx @@ -2,7 +2,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { SegmentedControl } from "@/components/ui/SegmentedControl"; -import { actionBadgeStyle } from "@/lib/colors"; +import { ANSTEHEND_STYLES } from "@/lib/colors"; import { ANSTEHEND_ARTEN, STANDARD_ZEITRAUM, @@ -57,7 +57,7 @@ export function AnstehendFilter({ zeitraum, arten }: { zeitraum: Zeitraum; arten onClick={() => artUmschalten(a.value)} aria-pressed={aktiv} className={`rounded-full px-2 py-0.5 text-xs font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500 ${ - aktiv ? actionBadgeStyle(a.label) : "bg-surface text-ink-muted hover:text-ink" + aktiv ? ANSTEHEND_STYLES[a.value] : "bg-surface text-ink-muted hover:text-ink" }`} > {a.label} diff --git a/docs/farbschema.html b/docs/farbschema.html index 0f59cbe..7203fde 100644 --- a/docs/farbschema.html +++ b/docs/farbschema.html @@ -291,7 +291,7 @@

Navigation

+
+

Anstehend — die vier Arten

+
+ Eintritt + Austritt + Rückkehr + Wiedervorlage +
+

+ Jemand kommt (gruen), jemand geht (rot), ein Termin will erledigt werden (gelb) — + und die Rueckkehr ist keines davon, also eine eigene Klasse. Tuerkis waere die + naheliegendere Lesart gewesen, kam gegen das Gruen aber nur auf dE 13.0; Violett + steht mit 30.8 eindeutig daneben. Schwaechstes Paar der vier: 14.2.

+
+

Organigramm — die vier Knotenarten

@@ -317,39 +332,39 @@
-

5 · Das Logo, sein Freiraum und die zwei Fassungen

-

§1.2 laesst den Schriftzug nur zur Gaenze auf Rosa zu. Das Manual kennt dafuer zwei - Faelle, und sie brauchen verschiedene Dateien — deshalb liegen zwei im Projekt.

+

5 · Das Logo und sein Freiraum

+

Eine Datei: manner-logo.png, der Schriftzug mit durchsichtigem Grund. + §1.2 laesst ihn nur zur Gaenze auf Rosa zu, und das Manual kennt dafuer zwei Lagen — + beide entstehen aus derselben Datei, den Unterschied macht das Bauteil.

- - Manner + Manner
- manner-logo.svg — auf Weiss, mit eigenem rosa Feld (§4.1)
+ auf Weiss — das rosa Feld zeichnet das Bauteil (§4.1)
- Manner
- manner-logo-auf-rosa.svg — im rosa Umfeld, ohne Feld (§3.1)
+ im rosa Umfeld — nur Freiraum, kein eigenes Feld (§3.1)

Der Freiraum X/3 (gestrichelt) steckt im Bauteil selbst und nicht in den Aufrufstellen — sonst haengt seine Einhaltung daran, dass jede einzelne daran denkt. - Die Fassung mit Feld auf eine rosa Flaeche zu setzen ging schief: das Feld stand als - Rechteck in leicht anderem Ton darauf. Der Ton war derselbe — verschoben hat ihn die - Lichtblende ueber der Flaeche, die unter dem Logo endete.

+ Auf Weiss ist er zugleich der Rand des rosa Feldes.

- Nebenbei ausgemistet. Beide Dateien wogen 74 kB, weil die Vorlage aus Seite 3 - des Manuals gezogen wurde und die ganze Seite mitgenommen hat: 219 Elemente, die den - Fliesstext jener Seite zeichnen („Der Manner Schriftzug … darf nicht veraendert - werden"), rosa auf Rosa oder ausserhalb des Beschnitts. Das eigentliche Logo sind - vier Pfade. Jetzt 18,7 kB. + Warum nicht mehr als SVG. Vorher lagen hier zwei aus dem Manual gezogene SVG, + eines mit eingebackenem rosa Feld und eines ohne. Auf der rosa Flaeche blieb das + eingebackene Feld als Rechteck sichtbar, und im Betrieb wurde die Datei ueberhaupt + nicht geladen — die Anmeldeseite zeigte nur den Ersatztext „Manner". Ein PNG mit + durchsichtigem Grund hat keine dieser Angriffsflaechen: kein XML, kein Beschnitt, + kein Feld, das an der falschen Stelle endet.
diff --git a/lib/colors.ts b/lib/colors.ts index bc031d1..5f97cb2 100644 --- a/lib/colors.ts +++ b/lib/colors.ts @@ -1,3 +1,4 @@ +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 @@ -72,6 +73,42 @@ 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`: 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 = { + hire: CATEGORY_STYLES.success, + exit: CATEGORY_STYLES.danger, + return: CATEGORY_STYLES.purple, + note: CATEGORY_STYLES.warning, +}; + export const NOTE_CATEGORY_STYLES: Record = { Vertraulich: CATEGORY_STYLES.purple, "Personalgespräch": CATEGORY_STYLES.info, diff --git a/lib/employee-status-filter.ts b/lib/employee-status-filter.ts index 70a8fd5..0325d40 100644 --- a/lib/employee-status-filter.ts +++ b/lib/employee-status-filter.ts @@ -13,11 +13,15 @@ import type { EmploymentStatus } from "./types"; // // Kept deliberately close to deriveStatusAsOf, clause for clause: // -// entry_date > asOf -> Geplant // exit_date <= asOf -> Ausgetreten +// entry_date > asOf -> Geplant // karenz window covers asOf -> Karenz // otherwise -> Aktiv // +// Die Reihenfolge der ersten beiden ist nicht beliebig: ein abgeschlossener +// Austritt schlaegt einen Eintritt, der noch bevorsteht. Warum das der Fall +// ist, steht bei deriveStatusAsOf. +// // tests/integration/employee-status-filter.test.ts asserts the two agree // against a real database, which is the only place that can prove it. @@ -39,8 +43,17 @@ export function derivedStatusFilter(eb: Eb, statuses: EmploymentStatus[], asOf: const wanted = new Set(statuses); if (wanted.size === 0) return null; - // A single non-employed status is a straight date comparison. - if (wanted.size === 1 && wanted.has("Geplant")) return eb("entry_date", ">", asOf); + // „Geplant" ist ein Eintritt, der noch bevorsteht **und** nicht + // zurueckgenommen wurde. Ohne die zweite Haelfte zaehlte der Filter die + // No-Shows mit: eingestellt, nie erschienen, Austritt vor dem Eintrittstag + // verbucht — in der Liste als „Ausgetreten" ausgewiesen und trotzdem unter + // „Geplant" gefunden. + if (wanted.size === 1 && wanted.has("Geplant")) { + return eb.and([ + eb("entry_date", ">", asOf), + eb.or([eb("exit_date", "is", null), eb("exit_date", ">", asOf)]), + ]); + } if (wanted.size === 1 && wanted.has("Ausgetreten")) { return eb.and([eb("exit_date", "is not", null), eb("exit_date", "<=", asOf)]); } diff --git a/lib/reports.ts b/lib/reports.ts index 55b2e4e..ded4237 100644 --- a/lib/reports.ts +++ b/lib/reports.ts @@ -133,12 +133,31 @@ export type OrgLookups = { // gruppiert also nach der Einheit von damals. Vorher gab es diese Historie // nicht, und ein Stichtagsbericht gruppierte nach der heutigen Zuordnung — // was in der Oberfläche vermerkt werden musste, statt still falsch zu sein. +// ── Warum der Austritt zuerst geprüft wird ────────────────────────── +// +// Umgekehrt herum stand hier „Geplant" vor „Ausgetreten", und wer einen +// Eintritt in der Zukunft hatte, galt als geplant — auch dann noch, wenn der +// Austritt längst erfasst war. Genau das trifft den No-Show (Migration +// 20260814100000): jemand wird eingestellt, erscheint nie, und der Austritt +// wird noch vor dem Eintrittstag verbucht. Im Bestand sind das Zeilen mit +// Eintritt 01.10.2026 und einem Austritt, die der Filter „Geplant" mitzählte, +// während die Liste daneben „Ausgetreten" anzeigte — employees.status, das +// die SQL-Funktion beim Austritt gesetzt hat, sagte von Anfang an das +// Richtige. +// +// Ein abgeschlossener Austritt ist der stärkere Befund: er beendet das +// Verhältnis, gleichgültig ob der Eintritt schon war oder noch kommt. +// „Geplant" heißt danach genau das, was es heißen soll — ein Eintritt, der +// noch bevorsteht und nicht zurückgenommen wurde. +// +// lib/employee-status-filter.ts bildet dieselbe Reihenfolge in SQL ab; die +// beiden müssen Klausel für Klausel zusammenpassen. export function deriveStatusAsOf( e: { entry_date: string; exit_date: string | null; karenz_start_date: string | null; karenz_return_date: string | null }, asOf: string ): EmploymentStatus { - if (e.entry_date > asOf) return "Geplant"; if (e.exit_date && e.exit_date <= asOf) return "Ausgetreten"; + if (e.entry_date > asOf) return "Geplant"; if (e.karenz_start_date && e.karenz_start_date <= asOf && (!e.karenz_return_date || asOf < e.karenz_return_date)) return "Karenz"; return "Aktiv"; } diff --git a/public/brand/manner-logo-auf-rosa.svg b/public/brand/manner-logo-auf-rosa.svg deleted file mode 100644 index aa79859..0000000 --- a/public/brand/manner-logo-auf-rosa.svg +++ /dev/null @@ -1,13 +0,0 @@ - -AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYToxYWJmODI4OC1jZjYxLTQ5YWUtYWNhOC1lMGNmNjdkZGIxMDUAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaKn7G/FL2ABjmkAWE5gQvwEAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDplZmI3MzdkNi05ZmRkLTQ2YTktOGMzNy0zMTc1NjdiYWFmYmJscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoFLuW9Pn2rm9K98G6dbECIAAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggRhLsAzLclgGyj5MXqmAbsHpJPvUUCPGGvKMnBkKgQ5ekZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaI8fKLnJujpJVNZYDI3AMTkAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRMAAAAAAAAAAAAAAAAZGhhc2hYILoDinW8CdwfcmMU16OU0qPw5bx92zi82okLedFjlFF7ZG5hbWVuanVtYmYgbWFuaWZlc3RqZXhjbHVzaW9uc4GiZXN0YXJ0GQEiZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjFhYmY4Mjg4LWNmNjEtNDlhZS1hY2E4LWUwY2Y2N2RkYjEwNS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjVjMDNlNGRmLTE1Y2MtNGUxZC05YTIyLTk2NzI1MDQ3MWUzZHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCBGEuwDMtyWAbKPkxeqYBuwekk+9RQI8Ya8oycGQqBDl6JjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggtFBCWM6RaYU6rHNFs6QluEtc+RhZZat47tDGI6BZYSCiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggf2y88SRQx7lSn0IRL3Rd0HobHb0nDs/7y6r42FZa0yN0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQOnfjlijUvc9D9MCxC6p7qTjUw5hJRb7MRWLsNcJDcjtCvEegW2+X+bwN+/KpaY75bXQe7UtoF+2LLHT2dFlbWs= - - - - - - - - - - - diff --git a/public/brand/manner-logo.png b/public/brand/manner-logo.png new file mode 100644 index 0000000..a8186e0 Binary files /dev/null and b/public/brand/manner-logo.png differ diff --git a/public/brand/manner-logo.svg b/public/brand/manner-logo.svg deleted file mode 100644 index f773afb..0000000 --- a/public/brand/manner-logo.svg +++ /dev/null @@ -1,14 +0,0 @@ - -AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYToxYWJmODI4OC1jZjYxLTQ5YWUtYWNhOC1lMGNmNjdkZGIxMDUAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaKn7G/FL2ABjmkAWE5gQvwEAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDplZmI3MzdkNi05ZmRkLTQ2YTktOGMzNy0zMTc1NjdiYWFmYmJscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoFLuW9Pn2rm9K98G6dbECIAAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggRhLsAzLclgGyj5MXqmAbsHpJPvUUCPGGvKMnBkKgQ5ekZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaI8fKLnJujpJVNZYDI3AMTkAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRMAAAAAAAAAAAAAAAAZGhhc2hYILoDinW8CdwfcmMU16OU0qPw5bx92zi82okLedFjlFF7ZG5hbWVuanVtYmYgbWFuaWZlc3RqZXhjbHVzaW9uc4GiZXN0YXJ0GQEiZmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjFhYmY4Mjg4LWNmNjEtNDlhZS1hY2E4LWUwY2Y2N2RkYjEwNS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjVjMDNlNGRmLTE1Y2MtNGUxZC05YTIyLTk2NzI1MDQ3MWUzZHJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCBGEuwDMtyWAbKPkxeqYBuwekk+9RQI8Ya8oycGQqBDl6JjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggtFBCWM6RaYU6rHNFs6QluEtc+RhZZat47tDGI6BZYSCiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggf2y88SRQx7lSn0IRL3Rd0HobHb0nDs/7y6r42FZa0yN0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPZYQOnfjlijUvc9D9MCxC6p7qTjUw5hJRb7MRWLsNcJDcjtCvEegW2+X+bwN+/KpaY75bXQe7UtoF+2LLHT2dFlbWs= - - - - - - - - - - - - diff --git a/tests/unit/employee-status-filter.test.ts b/tests/unit/employee-status-filter.test.ts new file mode 100644 index 0000000..75c8afc --- /dev/null +++ b/tests/unit/employee-status-filter.test.ts @@ -0,0 +1,100 @@ +import { DummyDriver, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from "kysely"; +import { describe, expect, it } from "vitest"; +import type { Schema } from "@/lib/db/schema"; +import { derivedStatusFilter } from "@/lib/employee-status-filter"; +import { deriveStatusAsOf } from "@/lib/reports"; +import type { EmploymentStatus } from "@/lib/types"; + +// Die Liste filtert in der Datenbank, weil sie dort auch blättert. Der Filter +// ist damit die einzige Stelle, an der der Status *nicht* von +// deriveStatusAsOf() kommt, sondern von einer zweiten Fassung derselben Regel +// in SQL. Zwei Fassungen driften, und diese hier taten es: „Geplant" fragte +// nur nach dem Eintrittsdatum und zählte die No-Shows mit — Leute mit einem +// Eintritt in der Zukunft, deren Austritt längst verbucht war. +// +// tests/integration/employee-status-filter.test.ts hält die beiden gegen eine +// echte Datenbank, läuft aber mangels Seed nicht in der CI. Deshalb hier +// zweierlei ohne Datenbank: die erzeugte Abfrage lesen, und die +// TypeScript-Ableitung an denselben Fällen prüfen. + +const db = new Kysely({ + dialect: { + createAdapter: () => new PostgresAdapter(), + createDriver: () => new DummyDriver(), + createIntrospector: (d) => new PostgresIntrospector(d), + createQueryCompiler: () => new PostgresQueryCompiler(), + }, +}); + +const STICHTAG = "2026-09-15"; + +/** Die where-Klausel, die für eine Statusauswahl herauskommt. */ +function bedingung(statuses: EmploymentStatus[]): string { + return db + .selectFrom("employees") + .select("id") + .where((eb) => derivedStatusFilter(eb, statuses, STICHTAG) ?? eb.val(true)) + .compile() + .sql.replace(/^.*? where /s, "") + .replace(/\s+/g, " "); +} + +describe("derivedStatusFilter — Geplant", () => { + it("fragt nach dem Eintritt in der Zukunft", () => { + expect(bedingung(["Geplant"])).toContain('"entry_date" >'); + }); + + // Die eigentliche Zusicherung: ohne diese Hälfte war der Filter falsch. + it("schliesst aus, wessen Austritt schon vollzogen ist", () => { + const sql = bedingung(["Geplant"]); + expect(sql).toContain('"exit_date" is null'); + expect(sql).toContain('"exit_date" >'); + }); +}); + +describe("derivedStatusFilter — Ausgetreten", () => { + it("verlangt ein Austrittsdatum, das erreicht ist", () => { + const sql = bedingung(["Ausgetreten"]); + expect(sql).toContain('"exit_date" is not null'); + expect(sql).toContain('"exit_date" <='); + }); +}); + +describe("derivedStatusFilter — nicht abgedeckte Auswahl", () => { + it("filtert lieber gar nicht als falsch", () => { + // Eine Mischung aus beschäftigt und nicht beschäftigt hat keinen Weg in + // der Oberfläche; geraten wird dafür nichts. + expect(derivedStatusFilter({} as never, ["Aktiv", "Ausgetreten"], STICHTAG)).toBeNull(); + expect(derivedStatusFilter({} as never, [], STICHTAG)).toBeNull(); + }); +}); + +// Dieselben Fälle noch einmal gegen die TypeScript-Ableitung: was der Filter +// findet und was die Liste daneben anzeigt, muss dieselbe Person meinen. +describe("Filter und Anzeige meinen dasselbe", () => { + const basis = { karenz_start_date: null, karenz_return_date: null }; + + const faelle: { was: string; e: Parameters[0]; erwartet: EmploymentStatus }[] = [ + { + was: "No-Show: Eintritt in der Zukunft, Austritt bereits vollzogen", + e: { ...basis, entry_date: "2026-10-01", exit_date: "2026-09-10" }, + erwartet: "Ausgetreten", + }, + { + was: "geplanter Eintritt ohne Austritt", + e: { ...basis, entry_date: "2026-10-01", exit_date: null }, + erwartet: "Geplant", + }, + { + was: "geplanter Eintritt mit Befristung, beides in der Zukunft", + e: { ...basis, entry_date: "2026-10-01", exit_date: "2026-12-31" }, + erwartet: "Geplant", + }, + ]; + + for (const { was, e, erwartet } of faelle) { + it(was, () => { + expect(deriveStatusAsOf(e, STICHTAG)).toBe(erwartet); + }); + } +}); diff --git a/tests/unit/reports.test.ts b/tests/unit/reports.test.ts index 94801e4..6b4fb6c 100644 --- a/tests/unit/reports.test.ts +++ b/tests/unit/reports.test.ts @@ -271,6 +271,27 @@ describe("deriveStatusAsOf", () => { expect(deriveStatusAsOf(onKarenz, "2023-07-01")).toBe("Aktiv"); expect(deriveStatusAsOf(onKarenz, "2022-12-31")).toBe("Aktiv"); }); + + // Der No-Show (Migration 20260814100000): eingestellt, nie erschienen, der + // Austritt wird noch vor dem Eintrittstag verbucht. Vorher gewann hier + // „Geplant", weil der Eintritt zuerst geprüft wurde — die Liste zeigte + // „Ausgetreten" und der Filter „Geplant" fand dieselbe Person trotzdem. + it("is Ausgetreten when the exit is done, even with the entry still ahead", () => { + const noShow = { ...base, entry_date: "2026-10-01", exit_date: "2026-09-10" }; + expect(deriveStatusAsOf(noShow, "2026-09-15")).toBe("Ausgetreten"); + }); + + it("stays Geplant while the entry is ahead and nothing has been taken back", () => { + expect(deriveStatusAsOf({ ...base, entry_date: "2026-10-01" }, "2026-09-15")).toBe("Geplant"); + }); + + // Ein Austritt, der selbst noch bevorsteht, nimmt den Eintritt nicht + // zurück: am Stichtag ist beides Zukunft, und geplant ist die Person + // weiterhin. + it("stays Geplant when both entry and exit are still ahead", () => { + const geplantMitEnde = { ...base, entry_date: "2026-10-01", exit_date: "2026-12-31" }; + expect(deriveStatusAsOf(geplantMitEnde, "2026-09-15")).toBe("Geplant"); + }); }); describe("aggregateEvents", () => {