mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
added admin setup
This commit is contained in:
@@ -12,6 +12,7 @@ import ShiftPlanCreate from './pages/ShiftPlans/ShiftPlanCreate';
|
||||
import EmployeeManagement from './pages/Employees/EmployeeManagement';
|
||||
import Settings from './pages/Settings/Settings';
|
||||
import Help from './pages/Help/Help';
|
||||
import Setup from './pages/Setup/Setup';
|
||||
|
||||
// Protected Route Component direkt in App.tsx
|
||||
const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }> = ({
|
||||
@@ -50,51 +51,67 @@ function App() {
|
||||
return (
|
||||
<NotificationProvider>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<NotificationContainer />
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/shift-plans" element={
|
||||
<ProtectedRoute>
|
||||
<ShiftPlanList />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/shift-plans/new" element={
|
||||
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
||||
<ShiftPlanCreate />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/employees" element={
|
||||
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
||||
<EmployeeManagement />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/settings" element={
|
||||
<ProtectedRoute roles={['admin']}>
|
||||
<Settings />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/help" element={
|
||||
<ProtectedRoute>
|
||||
<Help />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
</Routes>
|
||||
</Router>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
</NotificationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppContent() {
|
||||
const { loading, needsSetup } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<div>⏳ Lade Anwendung...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<NotificationContainer />
|
||||
<Routes>
|
||||
{needsSetup ? (
|
||||
<Route path="*" element={<Setup />} />
|
||||
) : (
|
||||
<>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/shift-plans" element={
|
||||
<ProtectedRoute>
|
||||
<ShiftPlanList />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/shift-plans/new" element={
|
||||
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
||||
<ShiftPlanCreate />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/employees" element={
|
||||
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
||||
<EmployeeManagement />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/settings" element={
|
||||
<ProtectedRoute roles={['admin']}>
|
||||
<Settings />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/help" element={
|
||||
<ProtectedRoute>
|
||||
<Help />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
</>
|
||||
)}
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -9,6 +9,8 @@ interface AuthContextType {
|
||||
hasRole: (roles: string[]) => boolean;
|
||||
loading: boolean;
|
||||
refreshUser: () => void; // NEU: Force refresh
|
||||
needsSetup: boolean;
|
||||
checkSetupStatus: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
@@ -16,16 +18,31 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [needsSetup, setNeedsSetup] = useState(false);
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0); // NEU: Refresh trigger
|
||||
|
||||
// User beim Start laden
|
||||
useEffect(() => {
|
||||
const savedUser = authService.getCurrentUser();
|
||||
if (savedUser) {
|
||||
setUser(savedUser);
|
||||
console.log('✅ User from localStorage:', savedUser.email);
|
||||
const checkSetupStatus = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/setup/status');
|
||||
const data = await response.json();
|
||||
setNeedsSetup(data.needsSetup);
|
||||
} catch (error) {
|
||||
console.error('Error checking setup status:', error);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// Check setup status and load user on mount
|
||||
useEffect(() => {
|
||||
const initializeApp = async () => {
|
||||
await checkSetupStatus();
|
||||
const savedUser = authService.getCurrentUser();
|
||||
if (savedUser) {
|
||||
setUser(savedUser);
|
||||
console.log('✅ User from localStorage:', savedUser.email);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
initializeApp();
|
||||
}, []);
|
||||
|
||||
// NEU: User vom Server laden wenn nötig
|
||||
@@ -78,7 +95,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
logout,
|
||||
hasRole,
|
||||
loading,
|
||||
refreshUser // NEU
|
||||
refreshUser,
|
||||
needsSetup,
|
||||
checkSetupStatus
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
228
frontend/src/pages/Setup/Setup.tsx
Normal file
228
frontend/src/pages/Setup/Setup.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const Setup: React.FC = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const [formData, setFormData] = useState({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
department: ''
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const validateStep1 = () => {
|
||||
if (formData.password.length < 6) {
|
||||
setError('Das Passwort muss mindestens 6 Zeichen lang sein.');
|
||||
return false;
|
||||
}
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError('Die Passwörter stimmen nicht überein.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateStep2 = () => {
|
||||
if (!formData.name.trim()) {
|
||||
setError('Bitte geben Sie einen Namen ein.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
setError('');
|
||||
if (step === 1 && validateStep1()) {
|
||||
setStep(2);
|
||||
} else if (step === 2 && validateStep2()) {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setError('');
|
||||
setStep(1);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const adminEmail = 'admin@instandhaltung.de';
|
||||
const response = await fetch('/api/setup/admin', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: adminEmail,
|
||||
password: formData.password,
|
||||
name: formData.name,
|
||||
phone: formData.phone || undefined,
|
||||
department: formData.department || undefined
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Setup fehlgeschlagen');
|
||||
}
|
||||
|
||||
// Automatically log in after setup
|
||||
await login({ email: adminEmail, password: formData.password });
|
||||
navigate('/');
|
||||
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Erstkonfiguration</h1>
|
||||
<p className="text-gray-600">
|
||||
Konfigurieren Sie den Administrator-Account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Admin E-Mail
|
||||
</label>
|
||||
<div className="p-2 bg-gray-100 border rounded">
|
||||
admin@instandhaltung.de
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Passwort
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Mindestens 6 Zeichen"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Passwort bestätigen
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Passwort wiederholen"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Vollständiger Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Telefon (optional)
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="+49 123 456789"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Abteilung (optional)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="department"
|
||||
value={formData.department}
|
||||
onChange={handleInputChange}
|
||||
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="z.B. IT"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex justify-between">
|
||||
{step === 2 && (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
||||
disabled={loading}
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={loading}
|
||||
className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 ${
|
||||
step === 1 ? 'ml-auto' : ''
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
'⏳ Verarbeite...'
|
||||
) : step === 1 ? (
|
||||
'Weiter'
|
||||
) : (
|
||||
'Setup abschließen'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Setup;
|
||||
125
frontend/src/pages/ShiftPlans/ShiftPlanCreate.module.css
Normal file
125
frontend/src/pages/ShiftPlans/ShiftPlanCreate.module.css
Normal file
@@ -0,0 +1,125 @@
|
||||
.container {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
padding: 8px 16px;
|
||||
background-color: #f1f1f1;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.backButton:hover {
|
||||
background-color: #e4e4e4;
|
||||
}
|
||||
|
||||
.form {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.formGroup {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.formGroup label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #34495e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.formGroup input,
|
||||
.formGroup select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.formGroup select.empty {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.dateGroup {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #fde8e8;
|
||||
color: #c53030;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.noTemplates {
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.linkButton {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #3498db;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.linkButton:hover {
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.createButton {
|
||||
padding: 10px 20px;
|
||||
background-color: #2ecc71;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.createButton:hover {
|
||||
background-color: #27ae60;
|
||||
}
|
||||
|
||||
.createButton:disabled {
|
||||
background-color: #95a5a6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -1,56 +1,169 @@
|
||||
// frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { shiftTemplateService } from '../../services/shiftTemplateService';
|
||||
import { shiftPlanService } from '../../services/shiftPlanService';
|
||||
import { ShiftTemplate } from '../../types/shiftTemplate';
|
||||
import styles from './ShiftPlanCreate.module.css';
|
||||
|
||||
const ShiftPlanCreate: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [planName, setPlanName] = useState('');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [selectedTemplate, setSelectedTemplate] = useState('');
|
||||
const [templates, setTemplates] = useState<ShiftTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleCreate = async () => {
|
||||
// API Call zum Erstellen
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Template aus URL-Parameter setzen, falls vorhanden
|
||||
const templateId = searchParams.get('template');
|
||||
if (templateId) {
|
||||
setSelectedTemplate(templateId);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
const data = await shiftTemplateService.getTemplates();
|
||||
setTemplates(data);
|
||||
|
||||
// Wenn keine Template-ID in der URL ist, setze die Standard-Vorlage
|
||||
if (!searchParams.get('template')) {
|
||||
const defaultTemplate = data.find(t => t.isDefault);
|
||||
if (defaultTemplate) {
|
||||
setSelectedTemplate(defaultTemplate.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Vorlagen:', error);
|
||||
setError('Vorlagen konnten nicht geladen werden');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
if (!planName.trim()) {
|
||||
setError('Bitte geben Sie einen Namen für den Schichtplan ein');
|
||||
return;
|
||||
}
|
||||
if (!startDate) {
|
||||
setError('Bitte wählen Sie ein Startdatum');
|
||||
return;
|
||||
}
|
||||
if (!endDate) {
|
||||
setError('Bitte wählen Sie ein Enddatum');
|
||||
return;
|
||||
}
|
||||
if (new Date(endDate) < new Date(startDate)) {
|
||||
setError('Das Enddatum muss nach dem Startdatum liegen');
|
||||
return;
|
||||
}
|
||||
|
||||
await shiftPlanService.createShiftPlan({
|
||||
name: planName,
|
||||
startDate,
|
||||
endDate,
|
||||
templateId: selectedTemplate || undefined
|
||||
});
|
||||
|
||||
// Nach erfolgreicher Erstellung zur Liste der Schichtpläne navigieren
|
||||
navigate('/shift-plans');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Schichtplans:', error);
|
||||
setError('Der Schichtplan konnte nicht erstellt werden. Bitte versuchen Sie es später erneut.');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div>Lade Vorlagen...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Neuen Schichtplan erstellen</h1>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h1>Neuen Schichtplan erstellen</h1>
|
||||
<button onClick={() => navigate(-1)} className={styles.backButton}>
|
||||
Zurück
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className={styles.error}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label>Plan Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={planName}
|
||||
onChange={(e) => setPlanName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Plan Name:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={planName}
|
||||
onChange={(e) => setPlanName(e.target.value)}
|
||||
placeholder="z.B. KW 42 2025"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Von:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.dateGroup}>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Von:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Bis:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Bis:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Vorlage verwenden:</label>
|
||||
<select value={selectedTemplate} onChange={(e) => setSelectedTemplate(e.target.value)}>
|
||||
<option value="">Keine Vorlage</option>
|
||||
{/* Vorlagen laden */}
|
||||
</select>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Vorlage verwenden:</label>
|
||||
<select
|
||||
value={selectedTemplate}
|
||||
onChange={(e) => setSelectedTemplate(e.target.value)}
|
||||
className={templates.length === 0 ? styles.empty : ''}
|
||||
>
|
||||
<option value="">Keine Vorlage</option>
|
||||
{templates.map(template => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.name} {template.isDefault ? '(Standard)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{templates.length === 0 && (
|
||||
<p className={styles.noTemplates}>
|
||||
Keine Vorlagen verfügbar.
|
||||
<button onClick={() => navigate('/shift-templates/new')} className={styles.linkButton}>
|
||||
Neue Vorlage erstellen
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button onClick={handleCreate}>Schichtplan erstellen</button>
|
||||
<div className={styles.actions}>
|
||||
<button onClick={handleCreate} className={styles.createButton} disabled={!selectedTemplate}>
|
||||
Schichtplan erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
133
frontend/src/services/shiftPlanService.ts
Normal file
133
frontend/src/services/shiftPlanService.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// frontend/src/services/shiftPlanService.ts
|
||||
import { authService } from './authService';
|
||||
|
||||
const API_BASE = 'http://localhost:3001/api/shift-plans';
|
||||
|
||||
export interface CreateShiftPlanRequest {
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
templateId?: string;
|
||||
}
|
||||
|
||||
export interface ShiftPlan {
|
||||
id: string;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
templateId?: string;
|
||||
status: 'draft' | 'published';
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
shifts: ShiftPlanShift[];
|
||||
}
|
||||
|
||||
export interface ShiftPlanShift {
|
||||
id: string;
|
||||
shiftPlanId: string;
|
||||
date: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
requiredEmployees: number;
|
||||
assignedEmployees: string[];
|
||||
}
|
||||
|
||||
export const shiftPlanService = {
|
||||
async getShiftPlans(): Promise<ShiftPlan[]> {
|
||||
const response = await fetch(API_BASE, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authService.getAuthHeaders()
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
authService.logout();
|
||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||
}
|
||||
throw new Error('Fehler beim Laden der Schichtpläne');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async getShiftPlan(id: string): Promise<ShiftPlan> {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authService.getAuthHeaders()
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
authService.logout();
|
||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||
}
|
||||
throw new Error('Schichtplan nicht gefunden');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> {
|
||||
const response = await fetch(API_BASE, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authService.getAuthHeaders()
|
||||
},
|
||||
body: JSON.stringify(plan)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
authService.logout();
|
||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||
}
|
||||
throw new Error('Fehler beim Erstellen des Schichtplans');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async updateShiftPlan(id: string, plan: Partial<ShiftPlan>): Promise<ShiftPlan> {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authService.getAuthHeaders()
|
||||
},
|
||||
body: JSON.stringify(plan)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
authService.logout();
|
||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||
}
|
||||
throw new Error('Fehler beim Aktualisieren des Schichtplans');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async deleteShiftPlan(id: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authService.getAuthHeaders()
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
authService.logout();
|
||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||
}
|
||||
throw new Error('Fehler beim Löschen des Schichtplans');
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user