Add customer registration and deployment updates

This commit is contained in:
2026-03-12 16:02:16 +01:00
parent fb8e3c8ef6
commit 8ebb4d06e5
23 changed files with 851 additions and 425 deletions

View File

@@ -1,5 +1,5 @@
import { FormEvent, useEffect, useState } from "react";
import { apiGet, apiPost } from "../lib/api";
import { FormEvent, useState } from "react";
import { apiPost } from "../lib/api";
import { useSession } from "../lib/session";
import type { UserOption } from "../lib/types";
@@ -9,56 +9,36 @@ type FeedbackState =
| null;
export default function LoginPage() {
const [users, setUsers] = useState<UserOption[]>([]);
const [manualCode, setManualCode] = useState("");
const [showRegistration, setShowRegistration] = useState(false);
const [identifier, setIdentifier] = useState("");
const [password, setPassword] = useState("");
const [registration, setRegistration] = useState({
companyName: "",
address: "",
street: "",
houseNumber: "",
postalCode: "",
city: "",
email: "",
phoneNumber: "",
password: "",
passwordConfirmation: "",
});
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." });
async function handlePasswordLogin(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!identifier.trim() || !password.trim()) {
setFeedback({
type: "error",
text: "Bitte E-Mail oder Benutzername und Passwort eingeben.",
});
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 {
setFeedback(null);
const response = await apiPost<UserOption>("/session/password-login", {
identifier,
identifier: identifier.trim(),
password,
});
setUser(response);
@@ -69,8 +49,31 @@ export default function LoginPage() {
async function handleRegister(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (
!registration.companyName.trim()
|| !registration.street.trim()
|| !registration.houseNumber.trim()
|| !registration.postalCode.trim()
|| !registration.city.trim()
|| !registration.email.trim()
|| !registration.phoneNumber.trim()
|| !registration.password.trim()
) {
setFeedback({
type: "error",
text: "Bitte alle Pflichtfelder inklusive Telefonnummer fuer die Registrierung ausfuellen.",
});
return;
}
if (registration.password !== registration.passwordConfirmation) {
setFeedback({ type: "error", text: "Die beiden Passwoerter stimmen nicht ueberein." });
return;
}
try {
const response = await apiPost<UserOption>("/session/register", registration);
setFeedback(null);
const { passwordConfirmation, ...registrationPayload } = registration;
void passwordConfirmation;
const response = await apiPost<UserOption>("/session/register", registrationPayload);
setFeedback({
type: "success",
text: `Registrierung erfolgreich. Willkommen ${response.companyName ?? response.displayName}.`,
@@ -98,8 +101,8 @@ export default function LoginPage() {
<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.
Anmeldung per E-Mail oder Benutzername mit Passwort sowie direkte
Kundenregistrierung.
</p>
{feedback ? (
@@ -108,146 +111,156 @@ export default function LoginPage() {
</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>
<div className="auth-grid">
{!showRegistration ? (
<form className="login-panel__section" onSubmit={handlePasswordLogin}>
<label className="field">
<span>Benutzerkuerzel</span>
<span>E-Mail / Benutzername</span>
<input
value={manualCode}
onChange={(event) => setManualCode(event.target.value.toUpperCase())}
placeholder="z. B. SV"
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>
<button
type="button"
className="accent-button"
onClick={() => void handleCodeLogin(manualCode)}
className="secondary-button"
onClick={() => {
setFeedback(null);
setShowRegistration(true);
}}
>
Mit Kuerzel anmelden
Registrieren
</button>
</div>
</div>
</form>
) : (
<form className="login-panel__section" onSubmit={handleRegister}>
<div className="field-grid">
<label className="field field--wide">
<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>Strasse</span>
<input
value={registration.street}
onChange={(event) =>
setRegistration((current) => ({ ...current, street: event.target.value }))
}
placeholder="z. B. Dorfstrasse"
/>
</label>
<label className="field">
<span>Hausnummer</span>
<input
value={registration.houseNumber}
onChange={(event) =>
setRegistration((current) => ({ ...current, houseNumber: event.target.value }))
}
placeholder="z. B. 12a"
/>
</label>
<label className="field">
<span>PLZ</span>
<input
value={registration.postalCode}
onChange={(event) =>
setRegistration((current) => ({ ...current, postalCode: event.target.value }))
}
placeholder="z. B. 12345"
/>
</label>
<label className="field">
<span>Ort</span>
<input
value={registration.city}
onChange={(event) =>
setRegistration((current) => ({ ...current, city: event.target.value }))
}
placeholder="z. B. Musterstadt"
/>
</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>Telefonnummer</span>
<input
value={registration.phoneNumber}
onChange={(event) =>
setRegistration((current) => ({ ...current, phoneNumber: event.target.value }))
}
placeholder="z. B. 04531 181424"
/>
</label>
<label className="field field--wide">
<span>Passwort</span>
<input
type="password"
value={registration.password}
onChange={(event) =>
setRegistration((current) => ({ ...current, password: event.target.value }))
}
/>
</label>
<label className="field field--wide">
<span>Passwort wiederholen</span>
<input
type="password"
value={registration.passwordConfirmation}
onChange={(event) =>
setRegistration((current) => ({
...current,
passwordConfirmation: event.target.value,
}))
}
/>
</label>
</div>
<div className="page-actions">
<button type="submit" className="accent-button">
Registrieren
</button>
<button
type="button"
className="secondary-button"
onClick={() => {
setFeedback(null);
setShowRegistration(false);
}}
>
Zurück
</button>
</div>
</form>
)}
</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>