added stepsetup for initial admin setup

This commit is contained in:
2025-10-30 15:26:25 +01:00
parent fbd0f03eb2
commit 5809bb8b09
7 changed files with 2450 additions and 419 deletions

View File

@@ -1,12 +1,27 @@
// frontend/src/pages/Setup/Setup.tsx - UPDATED
import React, { useState } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import StepSetup, { Step } from '../../components/StepSetup/StepSetup';
const API_BASE_URL = '/api';
const Setup: React.FC = () => {
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({
// ===== TYP-DEFINITIONEN =====
interface SetupFormData {
password: string;
confirmPassword: string;
firstname: string;
lastname: string;
}
interface SetupStep {
id: string;
title: string;
subtitle?: string;
}
// ===== HOOK FÜR SETUP-LOGIK =====
const useSetup = () => {
const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState<SetupFormData>({
password: '',
confirmPassword: '',
firstname: '',
@@ -16,15 +31,26 @@ const Setup: React.FC = () => {
const [error, setError] = useState('');
const { checkSetupStatus } = useAuth();
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const steps: SetupStep[] = [
{
id: 'password-setup',
title: 'Passwort erstellen',
subtitle: 'Legen Sie ein sicheres Passwort fest'
},
{
id: 'profile-setup',
title: 'Profilinformationen',
subtitle: 'Geben Sie Ihre persönlichen Daten ein'
},
{
id: 'confirmation',
title: 'Bestätigung',
subtitle: 'Setup abschließen'
}
];
const validateStep1 = () => {
// ===== VALIDIERUNGS-FUNKTIONEN =====
const validateStep1 = (): boolean => {
if (formData.password.length < 6) {
setError('Das Passwort muss mindestens 6 Zeichen lang sein.');
return false;
@@ -36,7 +62,7 @@ const Setup: React.FC = () => {
return true;
};
const validateStep2 = () => {
const validateStep2 = (): boolean => {
if (!formData.firstname.trim()) {
setError('Bitte geben Sie einen Vornamen ein.');
return false;
@@ -48,21 +74,66 @@ const Setup: React.FC = () => {
return true;
};
const handleNext = () => {
setError('');
if (step === 1 && validateStep1()) {
setStep(2);
} else if (step === 2 && validateStep2()) {
handleSubmit();
const validateCurrentStep = (stepIndex: number): boolean => {
switch (stepIndex) {
case 0:
return validateStep1();
case 1:
return validateStep2();
default:
return true;
}
};
const handleBack = () => {
// ===== NAVIGATIONS-FUNKTIONEN =====
const goToNextStep = async (): Promise<void> => {
setError('');
setStep(1);
if (!validateCurrentStep(currentStep)) {
return;
}
// Wenn wir beim letzten Schritt sind, Submit ausführen
if (currentStep === steps.length - 1) {
await handleSubmit();
return;
}
// Ansonsten zum nächsten Schritt gehen
setCurrentStep(prev => prev + 1);
};
const handleSubmit = async () => {
const goToPrevStep = (): void => {
setError('');
if (currentStep > 0) {
setCurrentStep(prev => prev - 1);
}
};
const handleStepChange = (stepIndex: number): void => {
setError('');
// Nur erlauben, zu bereits validierten Schritten zu springen
// oder zum nächsten Schritt nach dem aktuellen
if (stepIndex <= currentStep + 1) {
// Vor dem Wechsel validieren
if (stepIndex > currentStep && !validateCurrentStep(currentStep)) {
return;
}
setCurrentStep(stepIndex);
}
};
// ===== FORM HANDLER =====
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (): Promise<void> => {
try {
setLoading(true);
setError('');
@@ -102,8 +173,8 @@ const Setup: React.FC = () => {
}
};
// Helper to display generated email preview
const getEmailPreview = () => {
// ===== HELPER FUNCTIONS =====
const getEmailPreview = (): string => {
if (!formData.firstname.trim() || !formData.lastname.trim()) {
return 'vorname.nachname@sp.de';
}
@@ -113,6 +184,299 @@ const Setup: React.FC = () => {
return `${cleanFirstname}.${cleanLastname}@sp.de`;
};
const isStepCompleted = (stepIndex: number): boolean => {
switch (stepIndex) {
case 0:
return formData.password.length >= 6 &&
formData.password === formData.confirmPassword;
case 1:
return !!formData.firstname.trim() && !!formData.lastname.trim();
default:
return false;
}
};
return {
// State
currentStep,
formData,
loading,
error,
steps,
// Actions
goToNextStep,
goToPrevStep,
handleStepChange,
handleInputChange,
// Helpers
getEmailPreview,
isStepCompleted
};
};
// ===== STEP-INHALTS-KOMPONENTEN =====
interface StepContentProps {
formData: SetupFormData;
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
getEmailPreview: () => string;
currentStep: number;
}
const Step1Content: React.FC<StepContentProps> = ({
formData,
onInputChange
}) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={onInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Mindestens 6 Zeichen"
required
autoComplete="new-password"
/>
</div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort bestätigen
</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={onInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Passwort wiederholen"
required
autoComplete="new-password"
/>
</div>
</div>
);
const Step2Content: React.FC<StepContentProps> = ({
formData,
onInputChange,
getEmailPreview
}) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Vorname
</label>
<input
type="text"
name="firstname"
value={formData.firstname}
onChange={onInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Max"
required
autoComplete="given-name"
/>
</div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Nachname
</label>
<input
type="text"
name="lastname"
value={formData.lastname}
onChange={onInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Mustermann"
required
autoComplete="family-name"
/>
</div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Automatisch generierte E-Mail
</label>
<div style={{
padding: '0.75rem',
backgroundColor: '#e9ecef',
border: '1px solid #ced4da',
borderRadius: '6px',
color: '#495057',
fontWeight: '500',
fontFamily: 'monospace'
}}>
{getEmailPreview()}
</div>
<div style={{
fontSize: '0.875rem',
color: '#6c757d',
marginTop: '0.25rem'
}}>
Die E-Mail wird automatisch aus Vor- und Nachname generiert
</div>
</div>
</div>
);
const Step3Content: React.FC<StepContentProps> = ({
formData,
getEmailPreview
}) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div style={{
backgroundColor: '#f8f9fa',
padding: '1.5rem',
borderRadius: '8px',
border: '1px solid #e9ecef'
}}>
<h3 style={{
marginBottom: '1rem',
color: '#2c3e50',
fontSize: '1.1rem',
fontWeight: '600'
}}>
Zusammenfassung
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#6c757d' }}>E-Mail:</span>
<span style={{ fontWeight: '500' }}>{getEmailPreview()}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#6c757d' }}>Vorname:</span>
<span style={{ fontWeight: '500' }}>{formData.firstname || '-'}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#6c757d' }}>Nachname:</span>
<span style={{ fontWeight: '500' }}>{formData.lastname || '-'}</span>
</div>
</div>
</div>
<div style={{
padding: '1rem',
backgroundColor: '#e7f3ff',
borderRadius: '6px',
border: '1px solid #b6d7e8',
color: '#2c3e50'
}}>
<strong>💡 Wichtig:</strong> Nach dem Setup können Sie sich mit Ihrer
automatisch generierten E-Mail anmelden.
</div>
</div>
);
// ===== HAUPTKOMPONENTE =====
const Setup: React.FC = () => {
const {
currentStep,
formData,
loading,
error,
steps,
goToNextStep,
goToPrevStep,
handleStepChange,
handleInputChange,
getEmailPreview
} = useSetup();
const renderStepContent = (): React.ReactNode => {
const stepProps = {
formData,
onInputChange: handleInputChange,
getEmailPreview,
currentStep
};
switch (currentStep) {
case 0:
return <Step1Content {...stepProps} />;
case 1:
return <Step2Content {...stepProps} />;
case 2:
return <Step3Content {...stepProps} />;
default:
return null;
}
};
const getNextButtonText = (): string => {
if (loading) return '⏳ Wird verarbeitet...';
switch (currentStep) {
case 0:
return 'Weiter →';
case 1:
return 'Weiter →';
case 2:
return 'Setup abschließen';
default:
return 'Weiter →';
}
};
return (
<div style={{
minHeight: '100vh',
@@ -128,10 +492,10 @@ const Setup: React.FC = () => {
borderRadius: '12px',
boxShadow: '0 10px 30px rgba(0,0,0,0.1)',
width: '100%',
maxWidth: '500px',
maxWidth: '600px',
border: '1px solid #e9ecef'
}}>
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<div style={{ textAlign: 'center', marginBottom: '1rem' }}>
<h1 style={{
fontSize: '2rem',
fontWeight: 'bold',
@@ -142,12 +506,47 @@ const Setup: React.FC = () => {
</h1>
<p style={{
color: '#6c757d',
fontSize: '1.1rem'
fontSize: '1.1rem',
marginBottom: '2rem'
}}>
Richten Sie Ihren Administrator-Account ein
</p>
</div>
{/* Aktueller Schritt Titel und Beschreibung */}
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<h2 style={{
fontSize: '1.5rem',
fontWeight: 'bold',
marginBottom: '0.5rem',
color: '#2c3e50'
}}>
{steps[currentStep].title}
</h2>
{steps[currentStep].subtitle && (
<p style={{
color: '#6c757d',
fontSize: '1rem'
}}>
{steps[currentStep].subtitle}
</p>
)}
</div>
{/* StepSetup Komponente - nur die Steps ohne Beschreibung */}
<div style={{ marginBottom: '2.5rem' }}>
<StepSetup
steps={steps.map(step => ({ ...step, subtitle: undefined }))}
current={currentStep}
onChange={handleStepChange}
orientation="horizontal"
clickable={true}
size="md"
animated={true}
/>
</div>
{/* Fehleranzeige */}
{error && (
<div style={{
backgroundColor: '#f8d7da',
@@ -161,175 +560,37 @@ const Setup: React.FC = () => {
</div>
)}
{step === 1 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Mindestens 6 Zeichen"
required
/>
</div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort bestätigen
</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Passwort wiederholen"
required
/>
</div>
</div>
)}
{step === 2 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Vorname
</label>
<input
type="text"
name="firstname"
value={formData.firstname}
onChange={handleInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Max"
required
/>
</div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Nachname
</label>
<input
type="text"
name="lastname"
value={formData.lastname}
onChange={handleInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Mustermann"
required
/>
</div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Automatisch generierte E-Mail
</label>
<div style={{
padding: '0.75rem',
backgroundColor: '#e9ecef',
border: '1px solid #ced4da',
borderRadius: '6px',
color: '#495057',
fontWeight: '500',
fontFamily: 'monospace'
}}>
{getEmailPreview()}
</div>
<div style={{
fontSize: '0.875rem',
color: '#6c757d',
marginTop: '0.25rem'
}}>
Die E-Mail wird automatisch aus Vor- und Nachname generiert
</div>
</div>
</div>
)}
{/* Schritt-Inhalt */}
<div style={{ minHeight: '200px' }}>
{renderStepContent()}
</div>
{/* Navigations-Buttons */}
<div style={{
marginTop: '2rem',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
{step === 2 && (
<button
onClick={handleBack}
style={{
padding: '0.75rem 1.5rem',
color: '#6c757d',
border: '1px solid #6c757d',
background: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: '500'
}}
disabled={loading}
>
Zurück
</button>
)}
<button
onClick={goToPrevStep}
disabled={loading || currentStep === 0}
style={{
padding: '0.75rem 1.5rem',
color: loading || currentStep === 0 ? '#adb5bd' : '#6c757d',
border: `1px solid ${loading || currentStep === 0 ? '#adb5bd' : '#6c757d'}`,
background: 'none',
borderRadius: '6px',
cursor: loading || currentStep === 0 ? 'not-allowed' : 'pointer',
fontWeight: '500',
opacity: loading || currentStep === 0 ? 0.6 : 1
}}
>
Zurück
</button>
<button
onClick={handleNext}
onClick={goToNextStep}
disabled={loading}
style={{
padding: '0.75rem 2rem',
@@ -340,28 +601,25 @@ const Setup: React.FC = () => {
cursor: loading ? 'not-allowed' : 'pointer',
fontWeight: '600',
fontSize: '1rem',
marginLeft: step === 1 ? 'auto' : '0',
transition: 'background-color 0.3s ease'
}}
>
{loading ? '⏳ Wird verarbeitet...' :
step === 1 ? 'Weiter →' : 'Setup abschließen'}
{getNextButtonText()}
</button>
</div>
{step === 2 && (
{/* Zusätzliche Informationen */}
{currentStep === 2 && !loading && (
<div style={{
marginTop: '1.5rem',
textAlign: 'center',
color: '#6c757d',
fontSize: '0.9rem',
padding: '1rem',
backgroundColor: '#e7f3ff',
borderRadius: '6px',
border: '1px solid #b6d7e8'
backgroundColor: '#f8f9fa',
borderRadius: '6px'
}}>
💡 Nach dem erfolgreichen Setup werden Sie zur Anmeldeseite weitergeleitet,
wo Sie sich mit Ihrer automatisch generierten E-Mail anmelden können.
Überprüfen Sie Ihre Daten, bevor Sie das Setup abschließen
</div>
)}
</div>