Initial MUH app implementation
This commit is contained in:
255
frontend/src/pages/LoginPage.tsx
Normal file
255
frontend/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { apiGet, apiPost } from "../lib/api";
|
||||
import { useSession } from "../lib/session";
|
||||
import type { UserOption } from "../lib/types";
|
||||
|
||||
type FeedbackState =
|
||||
| { type: "error"; text: string }
|
||||
| { type: "success"; text: string }
|
||||
| null;
|
||||
|
||||
export default function LoginPage() {
|
||||
const [users, setUsers] = useState<UserOption[]>([]);
|
||||
const [manualCode, setManualCode] = useState("");
|
||||
const [identifier, setIdentifier] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [registration, setRegistration] = useState({
|
||||
companyName: "",
|
||||
address: "",
|
||||
email: "",
|
||||
password: "",
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [feedback, setFeedback] = useState<FeedbackState>(null);
|
||||
const { setUser } = useSession();
|
||||
|
||||
async function loadUsers() {
|
||||
setLoading(true);
|
||||
setFeedback(null);
|
||||
try {
|
||||
const response = await apiGet<UserOption[]>("/session/users");
|
||||
setUsers(response);
|
||||
} catch (loadError) {
|
||||
setFeedback({ type: "error", text: (loadError as Error).message });
|
||||
setUsers([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadUsers();
|
||||
}, []);
|
||||
|
||||
async function handleCodeLogin(code: string) {
|
||||
if (!code.trim()) {
|
||||
setFeedback({ type: "error", text: "Bitte ein Benutzerkuerzel eingeben oder auswaehlen." });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await apiPost<UserOption>("/session/login", { code });
|
||||
setUser(response);
|
||||
} catch (loginError) {
|
||||
setFeedback({ type: "error", text: (loginError as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePasswordLogin(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const response = await apiPost<UserOption>("/session/password-login", {
|
||||
identifier,
|
||||
password,
|
||||
});
|
||||
setUser(response);
|
||||
} catch (loginError) {
|
||||
setFeedback({ type: "error", text: (loginError as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const response = await apiPost<UserOption>("/session/register", registration);
|
||||
setFeedback({
|
||||
type: "success",
|
||||
text: `Registrierung erfolgreich. Willkommen ${response.companyName ?? response.displayName}.`,
|
||||
});
|
||||
setUser(response);
|
||||
} catch (registrationError) {
|
||||
setFeedback({ type: "error", text: (registrationError as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<section className="login-hero">
|
||||
<div className="login-hero__copy">
|
||||
<p className="eyebrow">MUH-App</p>
|
||||
<h1>Moderne Steuerung fuer Milchproben und Therapien.</h1>
|
||||
<p className="hero-text">
|
||||
Fokus auf klare Arbeitsablaeufe, schnelle Probenbearbeitung und ein Portal
|
||||
fuer Verwaltung, Berichtsdruck und Versandstatus.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-hero__panel">
|
||||
<div className="panel-glow" />
|
||||
<p className="eyebrow">Zugang</p>
|
||||
<h2>Anmelden oder registrieren</h2>
|
||||
<p className="muted-text">
|
||||
Weiterhin moeglich: Direktanmeldung per Benutzerkuerzel. Neu: Login mit
|
||||
E-Mail/Benutzername und Passwort.
|
||||
</p>
|
||||
|
||||
{feedback ? (
|
||||
<div className={`alert ${feedback.type === "success" ? "alert--success" : "alert--error"}`}>
|
||||
{feedback.text}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="login-panel__section">
|
||||
<div className="section-card__header">
|
||||
<div>
|
||||
<p className="eyebrow">Schnelllogin</p>
|
||||
<h3>Benutzerkuerzel</h3>
|
||||
</div>
|
||||
<button type="button" className="secondary-button" onClick={() => void loadUsers()}>
|
||||
Neu laden
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="empty-state">Benutzer werden geladen ...</div>
|
||||
) : users.length ? (
|
||||
<div className="user-grid">
|
||||
{users.map((user) => (
|
||||
<button
|
||||
key={user.id}
|
||||
type="button"
|
||||
className="user-card"
|
||||
onClick={() => void handleCodeLogin(user.code)}
|
||||
>
|
||||
<span className="user-card__code">{user.code}</span>
|
||||
<strong>{user.displayName}</strong>
|
||||
<small>
|
||||
{user.role === "ADMIN"
|
||||
? "Admin"
|
||||
: user.role === "CUSTOMER"
|
||||
? "Kunde"
|
||||
: "App"}
|
||||
</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="page-stack">
|
||||
<div className="empty-state">
|
||||
Es wurden keine aktiven Benutzer geladen. Das Kuersel kann trotzdem direkt
|
||||
eingegeben werden.
|
||||
</div>
|
||||
<label className="field">
|
||||
<span>Benutzerkuerzel</span>
|
||||
<input
|
||||
value={manualCode}
|
||||
onChange={(event) => setManualCode(event.target.value.toUpperCase())}
|
||||
placeholder="z. B. SV"
|
||||
/>
|
||||
</label>
|
||||
<div className="page-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="accent-button"
|
||||
onClick={() => void handleCodeLogin(manualCode)}
|
||||
>
|
||||
Mit Kuerzel anmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="divider-label">oder mit Passwort</div>
|
||||
|
||||
<div className="auth-grid">
|
||||
<form className="login-panel__section" onSubmit={handlePasswordLogin}>
|
||||
<p className="eyebrow">Login</p>
|
||||
<h3>E-Mail oder Benutzername</h3>
|
||||
<label className="field">
|
||||
<span>E-Mail / Benutzername</span>
|
||||
<input
|
||||
value={identifier}
|
||||
onChange={(event) => setIdentifier(event.target.value)}
|
||||
placeholder="z. B. admin oder name@hof.de"
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Passwort</span>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<div className="page-actions">
|
||||
<button type="submit" className="accent-button">
|
||||
Mit Passwort anmelden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form className="login-panel__section" onSubmit={handleRegister}>
|
||||
<p className="eyebrow">Kundenregistrierung</p>
|
||||
<h3>Neues Kundenkonto anlegen</h3>
|
||||
<label className="field">
|
||||
<span>Firmenname</span>
|
||||
<input
|
||||
value={registration.companyName}
|
||||
onChange={(event) =>
|
||||
setRegistration((current) => ({ ...current, companyName: event.target.value }))
|
||||
}
|
||||
placeholder="z. B. Muster Agrar GmbH"
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Adresse</span>
|
||||
<textarea
|
||||
value={registration.address}
|
||||
onChange={(event) =>
|
||||
setRegistration((current) => ({ ...current, address: event.target.value }))
|
||||
}
|
||||
placeholder="Strasse, Hausnummer, PLZ Ort"
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>E-Mail</span>
|
||||
<input
|
||||
type="email"
|
||||
value={registration.email}
|
||||
onChange={(event) =>
|
||||
setRegistration((current) => ({ ...current, email: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Passwort</span>
|
||||
<input
|
||||
type="password"
|
||||
value={registration.password}
|
||||
onChange={(event) =>
|
||||
setRegistration((current) => ({ ...current, password: event.target.value }))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<div className="page-actions">
|
||||
<button type="submit" className="accent-button">
|
||||
Registrieren
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user