Auf der Anmeldeseite steht jetzt "Alles im Blick. Alles Manner." statt des bisherigen Satzes. Die Berichte sortieren Gruppen nach der Kennzahl, gross zuerst. Bei Bereich oder Standort ist das die Antwort auf die Frage, die der Bericht stellt. Bei einer Leiter ist es keine: HG09 vor HG10 ist die Reihenfolge, in der die Werte sind, und nach Haeufigkeit umgestellt liest sie sich als Zufall. Die Sonderbehandlung, die es fuer Wochentage schon gab, ist dafuer zu einer Liste "Dimensionen mit eigener Reihenfolge" verallgemeinert -- Wochentage und Hay-Grades stehen darin, und die naechste Leiter ist ein Eintrag statt einer dritten Verzweigung. Gilt damit auch fuer die Aufschluesselung innerhalb einer Gruppe und fuer die Spalten im Berichtsexport, die dieselbe Funktion nutzen. Der Generic Grade steht vor HG09: er ist keine Stufe, sondern ihr Fehlen, und vor der niedrigsten faellt das am wenigsten als Aussage auf.
176 lines
8.2 KiB
TypeScript
176 lines
8.2 KiB
TypeScript
import { EntraSignInButton } from "@/components/auth/EntraSignInButton";
|
|
import { PasswortFormular } from "@/components/auth/PasswortFormular";
|
|
import { AppWortmarke } from "@/components/brand/Logo";
|
|
import { logout, signInWithEntra } from "@/actions/auth";
|
|
|
|
// The query string is attacker-controlled, so the login page renders a message
|
|
// looked up by code rather than whatever text ?error= carries. Reflecting the
|
|
// raw parameter let anyone put arbitrary wording ("Ihr Konto wurde gesperrt,
|
|
// rufen Sie …") on the real, correctly-branded sign-in screen. Dasselbe gilt
|
|
// für die Fehlertexte, die Entra im Rückweg mitschickt.
|
|
const ERROR_MESSAGES = {
|
|
no_hr_access: {
|
|
title: "Kein HR-Zugriff",
|
|
body: "Ihr Firmenkonto ist bekannt, aber nicht für die Personalverwaltung freigeschaltet. Bitte wenden Sie sich an eine:n bestehende:n HR-Benutzer:in.",
|
|
},
|
|
sso_failed: {
|
|
title: "Anmeldung fehlgeschlagen",
|
|
body: "Die Anmeldung über das Firmenkonto konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut.",
|
|
},
|
|
// Auth.js' eigener Code für eine gescheiterte Passwortanmeldung. Im Regelfall
|
|
// kommt er hier nicht an — die Server Action fängt den Fehlschlag ab und
|
|
// zeigt ihn am Formular. Ohne diesen Eintrag fiele er aber auf `sso_failed`
|
|
// zurück, und dort stünde „über das Firmenkonto", was mit dem tatsächlichen
|
|
// Weg nichts zu tun hätte.
|
|
CredentialsSignin: {
|
|
title: "Anmeldung fehlgeschlagen",
|
|
body: "E-Mail-Adresse oder Passwort stimmt nicht.",
|
|
},
|
|
} as const;
|
|
|
|
type ErrorCode = keyof typeof ERROR_MESSAGES;
|
|
|
|
type LoginPageProps = {
|
|
searchParams: Promise<{ error?: string }>;
|
|
};
|
|
|
|
export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|
const params = await searchParams;
|
|
// Ein unbekannter Code wird nicht verschluckt, sondern auf die allgemeine
|
|
// Meldung abgebildet: Auth.js schickt bei einem Fehlschlag seine eigenen
|
|
// Codes („Configuration", „AccessDenied", „OAuthCallbackError" …), und ohne
|
|
// diese Abbildung stünde man vor einer Anmeldeseite, die so tut, als wäre
|
|
// nichts gewesen. Angezeigt wird trotzdem nur eigener Text — der Parameter
|
|
// selbst kommt nie auf die Seite.
|
|
const code = params.error
|
|
? Object.hasOwn(ERROR_MESSAGES, params.error)
|
|
? (params.error as ErrorCode)
|
|
: "sso_failed"
|
|
: null;
|
|
const error = code ? ERROR_MESSAGES[code] : null;
|
|
|
|
return (
|
|
// dvh statt vh: auf iOS zählt vh die Adressleiste mit, wodurch die Karte
|
|
// im ersten Moment unter dem Faltenrand sitzt.
|
|
<div className="min-h-dvh lg:grid lg:grid-cols-[1.05fr_1fr]">
|
|
<BrandPanel />
|
|
|
|
<main className="flex items-center justify-center px-6 py-12 lg:py-6">
|
|
<div className="w-full max-w-sm">
|
|
{/* Ohne Versatz: hier steht das Logo auf Weiss und bekommt nach §4.1
|
|
sein rosa Feld, dessen Kante mit der Ueberschrift darunter
|
|
fluchten soll. */}
|
|
<div className="lg:hidden">
|
|
<AppWortmarke hoehe={26} textKlasse="text-ink" />
|
|
</div>
|
|
|
|
<h2 className="mt-8 text-2xl font-extrabold tracking-tight text-ink lg:mt-0">Anmelden</h2>
|
|
<p className="mt-1.5 text-sm text-ink-muted">
|
|
Der Zugang läuft über Ihr Firmenkonto. Ein eigenes Passwort gibt es nicht.
|
|
</p>
|
|
|
|
{error && (
|
|
<div role="alert" className="mt-6 rounded-md border border-danger-text/20 bg-danger-bg px-4 py-3">
|
|
<p className="text-sm font-bold text-danger-text">{error.title}</p>
|
|
<p className="mt-1 text-sm text-danger-text/90">{error.body}</p>
|
|
{code === "no_hr_access" && (
|
|
<form action={logout} className="mt-3">
|
|
<button
|
|
type="submit"
|
|
className="rounded text-xs font-semibold text-danger-text underline underline-offset-2 hover:no-underline
|
|
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger-text"
|
|
>
|
|
Abmelden und mit einem anderen Konto versuchen
|
|
</button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<form action={signInWithEntra} className="mt-7">
|
|
<EntraSignInButton />
|
|
</form>
|
|
|
|
{/* Der Passwortweg steht bewusst *unter* dem Firmenkonto und ohne
|
|
eigene Überschrift: er ist die Ausnahme für die Testphase, nicht
|
|
die zweite gleichwertige Möglichkeit. Wer ein Firmenkonto hat,
|
|
soll oben klicken. */}
|
|
<div className="mt-6 flex items-center gap-3" aria-hidden>
|
|
<span className="h-px flex-1 bg-border-subtle" />
|
|
<span className="text-xs font-semibold uppercase tracking-wider text-ink-muted">oder</span>
|
|
<span className="h-px flex-1 bg-border-subtle" />
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
<PasswortFormular />
|
|
</div>
|
|
|
|
<p className="mt-6 border-t border-border-subtle pt-5 text-xs leading-relaxed text-ink-muted">
|
|
Die Anmeldung allein erteilt keinen Zugriff. HR-Rechte vergibt die Personalabteilung — bis dahin bleiben alle
|
|
Personaldaten verschlossen.
|
|
</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BrandPanel() {
|
|
return (
|
|
// Rosa und nicht Blau: Rosa ist die Farbe, in der die Marke auftritt, und
|
|
// die Flaeche ist das Groesste, was diese Seite zu vergeben hat. Blau war
|
|
// hier doppelt falsch — es drehte das Verhaeltnis der beiden Markenfarben
|
|
// um, und das Logo bringt sein eigenes rosa Feld mit (§1.2), das auf Blau
|
|
// wie ein aufgeklebtes Rechteck stand. Auf Rosa geht es in die Flaeche
|
|
// ueber, und genau das meint §3.1 mit „in einem grossflaechigen rosa
|
|
// Umfeld eingebettet".
|
|
<aside className="relative hidden overflow-hidden bg-accent-500 lg:flex lg:flex-col lg:justify-between lg:p-12">
|
|
{/* Ein feines Raster und eine leichte Tiefe zur unteren Ecke — genug
|
|
Struktur, damit die Fläche nicht wie ein Farbfehler wirkt, aber ohne
|
|
Bilddatei und ohne von der einen Schaltfläche gegenüber abzulenken.
|
|
Beides aus ink: Weiss verschwindet auf Rosa, und die Lichtblende, die
|
|
hier vorher lag, hellte die Markenfarbe so weit auf, dass sie kaum
|
|
noch die Markenfarbe war. */}
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute inset-0"
|
|
style={{
|
|
backgroundImage:
|
|
"radial-gradient(45rem 45rem at 100% 100%, rgb(35 31 32 / 0.08), transparent 60%)," +
|
|
"linear-gradient(rgb(35 31 32 / 0.04) 1px, transparent 1px)," +
|
|
"linear-gradient(90deg, rgb(35 31 32 / 0.04) 1px, transparent 1px)",
|
|
backgroundSize: "auto, 3rem 3rem, 3rem 3rem",
|
|
}}
|
|
/>
|
|
|
|
{/* -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. */}
|
|
<div className="relative -ml-3">
|
|
<AppWortmarke hoehe={36} aufRosa textKlasse="text-brand-700" />
|
|
</div>
|
|
|
|
<div className="relative max-w-md">
|
|
<p className="text-3xl font-extrabold leading-tight tracking-tight text-brand-700">
|
|
Alles im Blick. Alles Manner.
|
|
</p>
|
|
<p className="mt-4 text-sm leading-relaxed text-brand-700">
|
|
Stammdaten, Planstellen und Berichtslinien der Alpenwerk Industrie GmbH — jederzeit auch zu einem beliebigen
|
|
Stichtag.
|
|
</p>
|
|
</div>
|
|
|
|
<p className="relative text-xs font-semibold text-brand-700">Interne Anwendung · Zugriff nur für die Personalabteilung</p>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
// Die vier Quadrate, die hier als Platzhalter-Bildmarke standen, sind
|
|
// entfallen: an ihrer Stelle steht jetzt das Manner-Logo
|
|
// (components/brand/Logo.tsx).
|