removed phone and departement as user attribute

This commit is contained in:
2025-10-10 18:22:13 +02:00
parent 6a9ddea0c5
commit e1e435a811
21 changed files with 1508 additions and 888 deletions

View File

@@ -1,3 +1,4 @@
// backend/src/controllers/authController.ts
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
@@ -9,8 +10,6 @@ export interface User {
email: string; email: string;
name: string; name: string;
role: string; role: string;
phone?: string;
department?: string;
} }
export interface UserWithPassword extends User { export interface UserWithPassword extends User {
@@ -34,8 +33,8 @@ export interface RegisterRequest {
email: string; email: string;
password: string; password: string;
name: string; name: string;
phone?: string; //employee_type?: string;
department?: string; //is_sufficiently_independent?: string;
role?: string; role?: string;
} }
@@ -52,7 +51,7 @@ export const login = async (req: Request, res: Response) => {
// Get user from database // Get user from database
const user = await db.get<UserWithPassword>( const user = await db.get<UserWithPassword>(
'SELECT id, email, password, name, role, phone, department FROM users WHERE email = ? AND is_active = 1', 'SELECT id, email, password, name, role FROM users WHERE email = ? AND is_active = 1',
[email] [email]
); );
@@ -116,7 +115,7 @@ export const getCurrentUser = async (req: Request, res: Response) => {
} }
const user = await db.get<User>( const user = await db.get<User>(
'SELECT id, email, name, role, phone, department FROM users WHERE id = ? AND is_active = 1', 'SELECT id, email, name, role FROM users WHERE id = ? AND is_active = 1',
[jwtUser.userId] // ← HIER: userId verwenden [jwtUser.userId] // ← HIER: userId verwenden
); );
@@ -162,7 +161,7 @@ export const validateToken = async (req: Request, res: Response) => {
export const register = async (req: Request, res: Response) => { export const register = async (req: Request, res: Response) => {
try { try {
const { email, password, name, phone, department, role = 'user' } = req.body as RegisterRequest; const { email, password, name, role = 'user' } = req.body as RegisterRequest;
// Validate required fields // Validate required fields
if (!email || !password || !name) { if (!email || !password || !name) {
@@ -188,9 +187,9 @@ export const register = async (req: Request, res: Response) => {
// Insert user // Insert user
const result = await db.run( const result = await db.run(
`INSERT INTO users (email, password, name, role, phone, department) `INSERT INTO users (email, password, name, role)
VALUES (?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?)`,
[email, hashedPassword, name, role, phone, department] [email, hashedPassword, name, role]
); );
if (!result.lastID) { if (!result.lastID) {

View File

@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { db } from '../services/databaseService.js'; import { db } from '../services/databaseService.js';
import { AuthRequest } from '../middleware/auth.js'; import { AuthRequest } from '../middleware/auth.js';
import { CreateEmployeeRequest } from '../models/Employee.js';
export const getEmployees = async (req: AuthRequest, res: Response): Promise<void> => { export const getEmployees = async (req: AuthRequest, res: Response): Promise<void> => {
try { try {
@@ -12,7 +13,9 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise<voi
const employees = await db.all<any>(` const employees = await db.all<any>(`
SELECT SELECT
id, email, name, role, is_active as isActive, id, email, name, role, is_active as isActive,
phone, department, created_at as createdAt, employee_type as employeeType,
is_sufficiently_independent as isSufficientlyIndependent,
created_at as createdAt,
last_login as lastLogin last_login as lastLogin
FROM users FROM users
WHERE is_active = 1 WHERE is_active = 1
@@ -36,7 +39,9 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise<void
const employee = await db.get<any>(` const employee = await db.get<any>(`
SELECT SELECT
id, email, name, role, is_active as isActive, id, email, name, role, is_active as isActive,
phone, department, created_at as createdAt, employee_type as employeeType,
is_sufficiently_independent as isSufficientlyIndependent,
created_at as createdAt,
last_login as lastLogin last_login as lastLogin
FROM users FROM users
WHERE id = ? WHERE id = ?
@@ -61,19 +66,22 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
password: '***hidden***' password: '***hidden***'
}); });
const { email, password, name, role, phone, department } = req.body as { const {
email: string; email,
password: string; password,
name: string; name,
role: string; role,
phone?: string; employeeType,
department?: string; isSufficientlyIndependent,
}; notes
} = req.body as CreateEmployeeRequest;
// Validierung // Validierung
if (!email || !password || !name || !role) { if (!email || !password || !name || !role || !employeeType) {
console.log('❌ Validation failed: Missing required fields'); console.log('❌ Validation failed: Missing required fields');
res.status(400).json({ error: 'Email, password, name and role are required' }); res.status(400).json({
error: 'Email, password, name, role und employeeType sind erforderlich'
});
return; return;
} }
@@ -83,13 +91,8 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
return; return;
} }
// First check for ANY user with this email to debug // Check if email already exists
const allUsersWithEmail = await db.all<any>('SELECT id, email, is_active FROM users WHERE email = ?', [email]); const existingActiveUser = await db.get<any>('SELECT id FROM users WHERE email = ? AND is_active = 1', [email]);
console.log('🔍 Found existing users with this email:', allUsersWithEmail);
// Check if email already exists among active users only
const existingActiveUser = await db.get<any>('SELECT id, is_active FROM users WHERE email = ? AND is_active = 1', [email]);
console.log('🔍 Checking active users with this email:', existingActiveUser);
if (existingActiveUser) { if (existingActiveUser) {
console.log('❌ Email exists for active user:', existingActiveUser); console.log('❌ Email exists for active user:', existingActiveUser);
@@ -102,16 +105,30 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
const employeeId = uuidv4(); const employeeId = uuidv4();
await db.run( await db.run(
`INSERT INTO users (id, email, password, name, role, phone, department, is_active) `INSERT INTO users (
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, id, email, password, name, role, employee_type, is_sufficiently_independent,
[employeeId, email, hashedPassword, name, role, phone, department, 1] notes, is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
employeeId,
email,
hashedPassword,
name,
role,
employeeType,
isSufficientlyIndependent ? 1 : 0,
notes || null,
1
]
); );
// Return employee without password // Return created employee
const newEmployee = await db.get<any>(` const newEmployee = await db.get<any>(`
SELECT SELECT
id, email, name, role, is_active as isActive, id, email, name, role, is_active as isActive,
phone, department, created_at as createdAt, employee_type as employeeType,
is_sufficiently_independent as isSufficientlyIndependent,
notes, created_at as createdAt,
last_login as lastLogin last_login as lastLogin
FROM users FROM users
WHERE id = ? WHERE id = ?
@@ -127,7 +144,7 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
export const updateEmployee = async (req: AuthRequest, res: Response): Promise<void> => { export const updateEmployee = async (req: AuthRequest, res: Response): Promise<void> => {
try { try {
const { id } = req.params; const { id } = req.params;
const { name, role, isActive, phone, department } = req.body; const { name, role, isActive, employeeType, isSufficientlyIndependent } = req.body;
// Check if employee exists // Check if employee exists
const existingEmployee = await db.get('SELECT * FROM users WHERE id = ?', [id]); const existingEmployee = await db.get('SELECT * FROM users WHERE id = ?', [id]);
@@ -142,17 +159,19 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
SET name = COALESCE(?, name), SET name = COALESCE(?, name),
role = COALESCE(?, role), role = COALESCE(?, role),
is_active = COALESCE(?, is_active), is_active = COALESCE(?, is_active),
phone = COALESCE(?, phone), employee_type = COALESCE(?, employee_type),
department = COALESCE(?, department) is_sufficiently_independent = COALESCE(?, is_sufficiently_independent)
WHERE id = ?`, WHERE id = ?`,
[name, role, isActive, phone, department, id] [name, role, isActive, employeeType, isSufficientlyIndependent, id]
); );
// Return updated employee // Return updated employee
const updatedEmployee = await db.get<any>(` const updatedEmployee = await db.get<any>(`
SELECT SELECT
id, email, name, role, is_active as isActive, id, email, name, role, is_active as isActive,
phone, department, created_at as createdAt, employee_type as employeeType,
is_sufficiently_independent as isSufficientlyIndependent,
created_at as createdAt,
last_login as lastLogin last_login as lastLogin
FROM users FROM users
WHERE id = ? WHERE id = ?

View File

@@ -44,10 +44,10 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
return; return;
} }
const { password, name, phone, department } = req.body; const { password, name } = req.body;
const email = 'admin@instandhaltung.de'; // Fixed admin email const email = 'admin@instandhaltung.de'; // Fixed admin email
console.log('👤 Creating admin with data:', { name, email, phone, department }); console.log('👤 Creating admin with data:', { name, email });
// Validation // Validation
if (!password || !name) { if (!password || !name) {
@@ -63,16 +63,16 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
// Hash password // Hash password
const hashedPassword = await bcrypt.hash(password, 10); const hashedPassword = await bcrypt.hash(password, 10);
const adminId = uuidv4(); const adminId = randomUUID();
console.log('📝 Inserting admin user with ID:', adminId); console.log('📝 Inserting admin user with ID:', adminId);
try { try {
// Create admin user // Create admin user
await db.run( await db.run(
`INSERT INTO users (id, email, password, name, role, phone, department, is_active) `INSERT INTO users (id, email, password, name, role, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?)`,
[adminId, email, hashedPassword, name, 'admin', phone || null, department || null, 1] [adminId, email, hashedPassword, name, 'admin', 1]
); );
console.log('✅ Admin user created successfully'); console.log('✅ Admin user created successfully');

View File

@@ -0,0 +1,3 @@
-- Add employee fields
ALTER TABLE users ADD COLUMN employee_type TEXT CHECK(employee_type IN ('chef', 'neuling', 'erfahren'));
ALTER TABLE users ADD COLUMN is_sufficiently_independent BOOLEAN DEFAULT FALSE;

View File

@@ -5,8 +5,8 @@ CREATE TABLE IF NOT EXISTS users (
password TEXT NOT NULL, password TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
role TEXT CHECK(role IN ('admin', 'user', 'instandhalter')) NOT NULL, role TEXT CHECK(role IN ('admin', 'user', 'instandhalter')) NOT NULL,
phone TEXT, employee_type TEXT CHECK(employee_type IN ('chef', 'neuling', 'erfahren')),
department TEXT, is_sufficiently_independent BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE, is_active BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP
); );

View File

@@ -2,38 +2,35 @@
export interface Employee { export interface Employee {
id: string; id: string;
email: string; email: string;
password: string;
name: string; name: string;
role: 'admin' | 'instandhalter' | 'user'; role: 'admin' | 'instandhalter' | 'user';
employeeType: 'chef' | 'neuling' | 'erfahren';
isSufficientlyIndependent: boolean;
isActive: boolean; isActive: boolean;
phone?: string; notes?: string;
department?: string;
createdAt: string; createdAt: string;
lastLogin?: string | null; lastLogin?: string | null;
} }
export interface Availability {
id: string;
employeeId: string;
dayOfWeek: number;
startTime: string;
endTime: string;
isAvailable: boolean;
}
export interface CreateEmployeeRequest { export interface CreateEmployeeRequest {
email: string; email: string;
password: string; password: string;
name: string; name: string;
role: 'admin' | 'instandhalter' | 'user'; role: 'admin' | 'instandhalter' | 'user';
phone?: string; employeeType: 'chef' | 'neuling' | 'erfahren';
department?: string; isSufficientlyIndependent: boolean;
notes?: string;
} }
export interface UpdateEmployeeRequest { export interface UpdateEmployeeRequest {
name?: string; name?: string;
role?: 'admin' | 'instandhalter' | 'user'; role?: 'admin' | 'instandhalter' | 'user';
employeeType?: 'chef' | 'neuling' | 'erfahren';
isSufficientlyIndependent?: boolean;
isActive?: boolean; isActive?: boolean;
phone?: string; notes?: string;
department?: string; }
export interface EmployeeWithPassword extends Employee {
password: string;
} }

View File

@@ -0,0 +1,43 @@
import { db } from '../services/databaseService.js';
import { readFile } from 'fs/promises';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export async function applyMigration() {
try {
console.log('📦 Starting database migration...');
// Read the migration file
const migrationPath = join(__dirname, '../database/migrations/002_add_employee_fields.sql');
const migrationSQL = await readFile(migrationPath, 'utf-8');
// Split into individual statements
const statements = migrationSQL
.split(';')
.map(s => s.trim())
.filter(s => s.length > 0);
// Execute each statement
for (const statement of statements) {
try {
await db.exec(statement);
console.log('✅ Executed:', statement.slice(0, 50) + '...');
} catch (error) {
const err = error as { code: string; message: string };
if (err.code === 'SQLITE_ERROR' && err.message.includes('duplicate column name')) {
console.log(' Column already exists, skipping...');
continue;
}
throw error;
}
}
console.log('✅ Migration completed successfully');
} catch (error) {
console.error('❌ Migration failed:', error);
throw error;
}
}

View File

@@ -1,92 +0,0 @@
// backend/src/scripts/seedData.ts - Erweitert
import { db } from '../services/databaseService.js';
import { v4 as uuidv4 } from 'uuid';
import bcrypt from 'bcryptjs';
export const seedData = async () => {
try {
console.log('Starting database seeding...');
// Admin User erstellen
const adminId = uuidv4();
const hashedPassword = await bcrypt.hash('admin123', 10);
await db.run(
`INSERT OR IGNORE INTO users (id, email, password, name, role, phone, department, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[adminId, 'admin@schichtplan.de', hashedPassword, 'System Administrator', 'admin', '+49 123 456789', 'IT', 1]
);
// Test-Mitarbeiter erstellen
const testUsers = [
{
id: uuidv4(),
email: 'instandhalter@schichtplan.de',
password: await bcrypt.hash('instandhalter123', 10),
name: 'Max Instandhalter',
role: 'instandhalter',
phone: '+49 123 456790',
department: 'Produktion'
},
{
id: uuidv4(),
email: 'mitarbeiter1@schichtplan.de',
password: await bcrypt.hash('user123', 10),
name: 'Anna Müller',
role: 'user',
phone: '+49 123 456791',
department: 'Logistik'
},
{
id: uuidv4(),
email: 'mitarbeiter2@schichtplan.de',
password: await bcrypt.hash('user123', 10),
name: 'Tom Schmidt',
role: 'user',
phone: '+49 123 456792',
department: 'Produktion'
}
];
for (const user of testUsers) {
await db.run(
`INSERT OR IGNORE INTO users (id, email, password, name, role, phone, department, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[user.id, user.email, user.password, user.name, user.role, user.phone, user.department, 1]
);
}
// Standard Vorlage erstellen
const templateId = uuidv4();
await db.run(
`INSERT OR IGNORE INTO shift_templates (id, name, description, is_default, created_by)
VALUES (?, ?, ?, ?, ?)`,
[templateId, 'Standard Woche', 'Standard Schichtplan für Montag bis Freitag', 1, adminId]
);
// Standard Schichten
const shifts = [
{ day: 1, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 },
{ day: 1, name: 'Nachmittag', start: '11:30', end: '15:30', employees: 2 },
{ day: 2, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 },
{ day: 2, name: 'Nachmittag', start: '11:30', end: '15:30', employees: 2 },
{ day: 3, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 },
{ day: 3, name: 'Nachmittag', start: '11:30', end: '15:30', employees: 2 },
{ day: 4, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 },
{ day: 4, name: 'Nachmittag', start: '11:30', end: '15:30', employees: 2 },
{ day: 5, name: 'Vormittag', start: '08:00', end: '12:00', employees: 2 }
];
for (const shift of shifts) {
await db.run(
`INSERT OR IGNORE INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, shift.day, shift.name, shift.start, shift.end, shift.employees]
);
}
console.log('✅ Test data seeded successfully');
} catch (error) {
console.error('❌ Error seeding test data:', error);
}
};

View File

@@ -59,9 +59,16 @@ app.get('/api/initial-setup', async (req: any, res: any) => {
// Initialize the application // Initialize the application
const initializeApp = async () => { const initializeApp = async () => {
try { try {
// Initialize database with base schema
await initializeDatabase(); await initializeDatabase();
console.log('✅ Database initialized successfully'); console.log('✅ Database initialized successfully');
// Apply any pending migrations
const { applyMigration } = await import('./scripts/applyMigration.js');
await applyMigration();
console.log('✅ Database migrations applied');
// Setup default template
await setupDefaultTemplate(); await setupDefaultTemplate();
console.log('✅ Default template checked/created'); console.log('✅ Default template checked/created');

View File

@@ -1,4 +1,4 @@
// frontend/src/App.tsx - KORRIGIERTE VERSION // frontend/src/App.tsx - KORRIGIERT MIT LAYOUT
import React from 'react'; import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider, useAuth } from './contexts/AuthContext'; import { AuthProvider, useAuth } from './contexts/AuthContext';
@@ -47,28 +47,13 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }>
return <Layout>{children}</Layout>; return <Layout>{children}</Layout>;
}; };
// SetupWrapper Component
const SetupWrapper: React.FC = () => {
return (
<Router>
<Setup />
</Router>
);
};
// LoginWrapper Component
const LoginWrapper: React.FC = () => {
return (
<Router>
<Login />
</Router>
);
};
// Main App Content // Main App Content
const AppContent: React.FC = () => { const AppContent: React.FC = () => {
const { loading, needsSetup, user } = useAuth(); const { loading, needsSetup, user } = useAuth();
console.log('🏠 AppContent rendering - loading:', loading, 'needsSetup:', needsSetup, 'user:', user);
// Während des Ladens
if (loading) { if (loading) {
return ( return (
<div style={{ textAlign: 'center', padding: '40px' }}> <div style={{ textAlign: 'center', padding: '40px' }}>
@@ -77,22 +62,21 @@ const AppContent: React.FC = () => {
); );
} }
console.log('AppContent - needsSetup:', needsSetup, 'user:', user); // Setup benötigt
// Wenn Setup benötigt wird → Setup zeigen (mit Router)
if (needsSetup) { if (needsSetup) {
return <SetupWrapper />; console.log('🔧 Showing setup page');
return <Setup />;
} }
// Wenn kein User eingeloggt ist → Login zeigen (mit Router) // Kein User eingeloggt
if (!user) { if (!user) {
return <LoginWrapper />; console.log('🔐 Showing login page');
return <Login />;
} }
// Wenn User eingeloggt ist → Geschützte Routen zeigen // User eingeloggt - Geschützte Routen
console.log('✅ Showing protected routes for user:', user.email);
return ( return (
<Router>
<NotificationContainer />
<Routes> <Routes>
<Route path="/" element={ <Route path="/" element={
<ProtectedRoute> <ProtectedRoute>
@@ -125,8 +109,12 @@ const AppContent: React.FC = () => {
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="*" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
</Routes> </Routes>
</Router>
); );
}; };
@@ -134,7 +122,10 @@ function App() {
return ( return (
<NotificationProvider> <NotificationProvider>
<AuthProvider> <AuthProvider>
<Router>
<NotificationContainer />
<AppContent /> <AppContent />
</Router>
</AuthProvider> </AuthProvider>
</NotificationProvider> </NotificationProvider>
); );

View File

@@ -1,95 +1,186 @@
// frontend/src/components/Layout/Footer.tsx // frontend/src/components/Layout/Footer.tsx
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
const Footer: React.FC = () => { const Footer: React.FC = () => {
return ( const styles = {
<footer style={{ footer: {
backgroundColor: '#34495e', background: '#2c3e50',
color: 'white', color: 'white',
padding: '30px 20px', marginTop: 'auto',
marginTop: 'auto' },
}}> footerContent: {
<div style={{
maxWidth: '1200px', maxWidth: '1200px',
margin: '0 auto', margin: '0 auto',
padding: '2rem 20px',
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '30px' gap: '2rem',
}}> },
{/* App Info */} footerSection: {
<div> display: 'flex',
<h4 style={{ marginBottom: '15px' }}>🗓 SchichtPlaner</h4> flexDirection: 'column' as const,
<p style={{ fontSize: '14px', lineHeight: '1.5' }}> },
Einfache Schichtplanung für Ihr Team. footerSectionH3: {
Optimierte Arbeitszeiten, transparente Planung. marginBottom: '1rem',
color: '#ecf0f1',
fontSize: '1.2rem',
},
footerSectionH4: {
marginBottom: '1rem',
color: '#ecf0f1',
fontSize: '1.1rem',
},
footerLink: {
color: '#bdc3c7',
textDecoration: 'none',
marginBottom: '0.5rem',
transition: 'color 0.3s ease',
},
footerBottom: {
borderTop: '1px solid #34495e',
padding: '1rem 20px',
textAlign: 'center' as const,
color: '#95a5a6',
},
};
return (
<footer style={styles.footer}>
<div style={styles.footerContent}>
<div style={styles.footerSection}>
<h3 style={styles.footerSectionH3}>Schichtenplaner</h3>
<p style={{color: '#bdc3c7', margin: 0}}>
Professionelle Schichtplanung für Ihr Team.
Effiziente Personalplanung für optimale Abläufe.
</p> </p>
</div> </div>
{/* Quick Links */} <div style={styles.footerSection}>
<div> <h4 style={styles.footerSectionH4}>Support & Hilfe</h4>
<h4 style={{ marginBottom: '15px' }}>Schnellzugriff</h4> <a
<ul style={{ listStyle: 'none', padding: 0, fontSize: '14px' }}> href="/help"
<li style={{ marginBottom: '8px' }}> style={styles.footerLink}
<Link to="/help" style={{ color: '#bdc3c7', textDecoration: 'none' }}> onMouseEnter={(e) => {
📖 Anleitung e.currentTarget.style.color = '#3498db';
</Link> }}
</li> onMouseLeave={(e) => {
<li style={{ marginBottom: '8px' }}> e.currentTarget.style.color = '#bdc3c7';
<Link to="/help/faq" style={{ color: '#bdc3c7', textDecoration: 'none' }}> }}
Häufige Fragen >
</Link> Hilfe & Anleitungen
</li> </a>
<li style={{ marginBottom: '8px' }}> <a
<Link to="/help/support" style={{ color: '#bdc3c7', textDecoration: 'none' }}> href="/contact"
💬 Support style={styles.footerLink}
</Link> onMouseEnter={(e) => {
</li> e.currentTarget.style.color = '#3498db';
</ul> }}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
}}
>
Kontakt & Support
</a>
<a
href="/faq"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
}}
>
Häufige Fragen
</a>
</div> </div>
{/* Legal Links */} <div style={styles.footerSection}>
<div> <h4 style={styles.footerSectionH4}>Rechtliches</h4>
<h4 style={{ marginBottom: '15px' }}>Rechtliches</h4> <a
<ul style={{ listStyle: 'none', padding: 0, fontSize: '14px' }}> href="/privacy"
<li style={{ marginBottom: '8px' }}> style={styles.footerLink}
<Link to="/impressum" style={{ color: '#bdc3c7', textDecoration: 'none' }}> onMouseEnter={(e) => {
📄 Impressum e.currentTarget.style.color = '#3498db';
</Link> }}
</li> onMouseLeave={(e) => {
<li style={{ marginBottom: '8px' }}> e.currentTarget.style.color = '#bdc3c7';
<Link to="/datenschutz" style={{ color: '#bdc3c7', textDecoration: 'none' }}> }}
🔒 Datenschutz >
</Link> Datenschutzerklärung
</li> </a>
<li style={{ marginBottom: '8px' }}> <a
<Link to="/agb" style={{ color: '#bdc3c7', textDecoration: 'none' }}> href="/imprint"
📝 AGB style={styles.footerLink}
</Link> onMouseEnter={(e) => {
</li> e.currentTarget.style.color = '#3498db';
</ul> }}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
}}
>
Impressum
</a>
<a
href="/terms"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
}}
>
Allgemeine Geschäftsbedingungen
</a>
</div> </div>
{/* Contact */} <div style={styles.footerSection}>
<div> <h4 style={styles.footerSectionH4}>Unternehmen</h4>
<h4 style={{ marginBottom: '15px' }}>Kontakt</h4> <a
<div style={{ fontSize: '14px', color: '#bdc3c7' }}> href="/about"
<p>📧 support@schichtplaner.de</p> style={styles.footerLink}
<p>📞 +49 123 456 789</p> onMouseEnter={(e) => {
<p>🕘 Mo-Fr: 9:00-17:00</p> e.currentTarget.style.color = '#3498db';
</div> }}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
}}
>
Über uns
</a>
<a
href="/features"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
}}
>
Funktionen
</a>
<a
href="/pricing"
style={styles.footerLink}
onMouseEnter={(e) => {
e.currentTarget.style.color = '#3498db';
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = '#bdc3c7';
}}
>
Preise
</a>
</div> </div>
</div> </div>
<div style={{ <div style={styles.footerBottom}>
borderTop: '1px solid #2c3e50', <p style={{margin: 0}}>
marginTop: '30px', &copy; 2025 Schichtenplaner. Alle Rechte vorbehalten. |
paddingTop: '20px', Made with for efficient team management
textAlign: 'center', </p>
fontSize: '12px',
color: '#95a5a6'
}}>
<p>© 2024 SchichtPlaner. Alle Rechte vorbehalten.</p>
</div> </div>
</footer> </footer>
); );

View File

@@ -0,0 +1,220 @@
/* Layout.css - Professionelles Design */
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 1000;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
height: 70px;
}
.logo h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
}
/* Desktop Navigation */
.desktop-nav {
display: flex;
gap: 2rem;
align-items: center;
}
.nav-link {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 6px;
transition: all 0.3s ease;
font-weight: 500;
}
.nav-link:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-1px);
}
/* User Menu */
.user-menu {
display: flex;
align-items: center;
gap: 1rem;
}
.user-info {
font-weight: 500;
}
.logout-btn {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Mobile Menu Button */
.mobile-menu-btn {
display: none;
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
}
/* Mobile Navigation */
.mobile-nav {
display: none;
flex-direction: column;
background: white;
padding: 1rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.mobile-nav-link {
color: #333;
text-decoration: none;
padding: 1rem;
border-bottom: 1px solid #eee;
transition: background-color 0.3s ease;
}
.mobile-nav-link:hover {
background-color: #f5f5f5;
}
.mobile-user-info {
padding: 1rem;
border-top: 1px solid #eee;
margin-top: 1rem;
}
.mobile-logout-btn {
background: #667eea;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
margin-top: 0.5rem;
width: 100%;
}
/* Main Content */
.main-content {
flex: 1;
background-color: #f8f9fa;
min-height: calc(100vh - 140px);
}
.content-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 20px;
}
/* Footer */
.footer {
background: #2c3e50;
color: white;
margin-top: auto;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
}
.footer-section h3,
.footer-section h4 {
margin-bottom: 1rem;
color: #ecf0f1;
}
.footer-section a {
color: #bdc3c7;
text-decoration: none;
display: block;
margin-bottom: 0.5rem;
transition: color 0.3s ease;
}
.footer-section a:hover {
color: #3498db;
}
.footer-bottom {
border-top: 1px solid #34495e;
padding: 1rem 20px;
text-align: center;
color: #95a5a6;
}
/* Responsive Design */
@media (max-width: 768px) {
.desktop-nav,
.user-menu {
display: none;
}
.mobile-menu-btn {
display: block;
}
.mobile-nav {
display: flex;
}
.header-content {
padding: 0 15px;
}
.content-container {
padding: 1rem 15px;
}
.footer-content {
grid-template-columns: 1fr;
text-align: center;
}
}
@media (max-width: 480px) {
.logo h1 {
font-size: 1.2rem;
}
.content-container {
padding: 1rem 10px;
}
}

View File

@@ -1,4 +1,4 @@
// frontend/src/components/Layout/Layout.tsx // frontend/src/components/Layout/Layout.tsx - KORRIGIERT
import React from 'react'; import React from 'react';
import Navigation from './Navigation'; import Navigation from './Navigation';
import Footer from './Footer'; import Footer from './Footer';
@@ -8,22 +8,32 @@ interface LayoutProps {
} }
const Layout: React.FC<LayoutProps> = ({ children }) => { const Layout: React.FC<LayoutProps> = ({ children }) => {
return ( const styles = {
<div style={{ layout: {
minHeight: '100vh', minHeight: '100vh',
display: 'flex', display: 'flex',
flexDirection: 'column' flexDirection: 'column' as const,
}}> },
<Navigation /> mainContent: {
<main style={{
flex: 1, flex: 1,
padding: '20px', backgroundColor: '#f8f9fa',
minHeight: 'calc(100vh - 140px)',
},
contentContainer: {
maxWidth: '1200px', maxWidth: '1200px',
margin: '0 auto', margin: '0 auto',
width: '100%' padding: '2rem 20px',
}}> },
};
return (
<div style={styles.layout}>
<Navigation />
<main style={styles.mainContent}>
<div style={styles.contentContainer}>
{children} {children}
</div>
</main> </main>
<Footer /> <Footer />

View File

@@ -1,119 +1,175 @@
// frontend/src/components/Layout/Navigation.tsx // frontend/src/components/Layout/Navigation.tsx
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const Navigation: React.FC = () => { const Navigation: React.FC = () => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { user, logout, hasRole } = useAuth(); const { user, logout, hasRole } = useAuth();
const location = useLocation(); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const isActive = (path: string) => location.pathname === path; const handleLogout = () => {
logout();
setIsMobileMenuOpen(false);
};
const navigationItems = [ const toggleMobileMenu = () => {
{ path: '/', label: 'Dashboard', icon: '🏠', roles: ['admin', 'instandhalter', 'user'] }, setIsMobileMenuOpen(!isMobileMenuOpen);
{ path: '/shift-plans', label: 'Schichtpläne', icon: '📅', roles: ['admin', 'instandhalter', 'user'] }, };
{ path: '/employees', label: 'Mitarbeiter', icon: '👥', roles: ['admin', 'instandhalter'] },
{ path: '/settings', label: 'Einstellungen', icon: '⚙️', roles: ['admin'] }, const navigationItems = [
{ path: '/help', label: 'Hilfe', icon: '❓', roles: ['admin', 'instandhalter', 'user'] }, { path: '/', label: '📊 Dashboard', roles: ['admin', 'instandhalter', 'user'] },
]; { path: '/shift-plans', label: '📅 Schichtpläne', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/employees', label: '👥 Mitarbeiter', roles: ['admin', 'instandhalter'] },
{ path: '/help', label: '❓ Hilfe & Support', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/settings', label: '⚙️ Einstellungen', roles: ['admin'] },
];
const filteredNavigation = navigationItems.filter(item => const filteredNavigation = navigationItems.filter(item =>
hasRole(item.roles) hasRole(item.roles)
); );
return ( const styles = {
<> header: {
{/* Desktop Navigation */} background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
<nav style={{ color: 'white',
backgroundColor: '#2c3e50', boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
position: 'sticky' as const,
top: 0,
zIndex: 1000,
},
headerContent: {
maxWidth: '1200px',
margin: '0 auto',
padding: '0 20px', padding: '0 20px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<div style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
maxWidth: '1200px', height: '70px',
margin: '0 auto' },
}}> logo: {
{/* Logo/Brand */} flex: 1,
<div style={{ display: 'flex', alignItems: 'center' }}> },
<Link logoH1: {
to="/" margin: 0,
style={{ fontSize: '1.5rem',
fontWeight: 700,
},
desktopNav: {
display: 'flex',
gap: '2rem',
alignItems: 'center',
},
navLink: {
color: 'white', color: 'white',
textDecoration: 'none', textDecoration: 'none',
fontSize: '20px', padding: '0.5rem 1rem',
fontWeight: 'bold', borderRadius: '6px',
padding: '15px 0' transition: 'all 0.3s ease',
}} fontWeight: 500,
> },
🗓 SchichtPlaner userMenu: {
</Link> display: 'flex',
alignItems: 'center',
gap: '1rem',
marginLeft: '2rem',
},
userInfo: {
fontWeight: 500,
},
logoutBtn: {
background: 'rgba(255, 255, 255, 0.1)',
color: 'white',
border: '1px solid rgba(255, 255, 255, 0.3)',
padding: '0.5rem 1rem',
borderRadius: '6px',
cursor: 'pointer',
transition: 'all 0.3s ease',
},
mobileMenuBtn: {
display: 'none',
background: 'none',
border: 'none',
color: 'white',
fontSize: '1.5rem',
cursor: 'pointer',
padding: '0.5rem',
},
mobileNav: {
display: isMobileMenuOpen ? 'flex' : 'none',
flexDirection: 'column' as const,
background: 'white',
padding: '1rem',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
},
mobileNavLink: {
color: '#333',
textDecoration: 'none',
padding: '1rem',
borderBottom: '1px solid #eee',
transition: 'background-color 0.3s ease',
},
mobileUserInfo: {
padding: '1rem',
borderTop: '1px solid #eee',
marginTop: '1rem',
color: '#333',
},
mobileLogoutBtn: {
background: '#667eea',
color: 'white',
border: 'none',
padding: '0.5rem 1rem',
borderRadius: '6px',
cursor: 'pointer',
marginTop: '0.5rem',
width: '100%',
},
};
return (
<header style={styles.header}>
<div style={styles.headerContent}>
<div style={styles.logo}>
<h1 style={styles.logoH1}>🔄 Schichtenplaner</h1>
</div> </div>
{/* Desktop Menu */} {/* Desktop Navigation */}
<div style={{ <nav style={styles.desktopNav}>
display: 'flex', {filteredNavigation.map((item) => (
alignItems: 'center', <a
gap: '10px'
}}>
{filteredNavigation.map(item => (
<Link
key={item.path} key={item.path}
to={item.path} href={item.path}
style={{ style={styles.navLink}
color: 'white',
textDecoration: 'none',
padding: '15px 20px',
borderRadius: '4px',
backgroundColor: isActive(item.path) ? '#3498db' : 'transparent',
transition: 'background-color 0.2s',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!isActive(item.path)) { e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
e.currentTarget.style.backgroundColor = '#34495e'; e.currentTarget.style.transform = 'translateY(-1px)';
}
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (!isActive(item.path)) { e.currentTarget.style.background = 'none';
e.currentTarget.style.backgroundColor = 'transparent'; e.currentTarget.style.transform = 'translateY(0)';
} }}
onClick={(e) => {
e.preventDefault();
window.location.href = item.path;
}} }}
> >
<span>{item.icon}</span>
{item.label} {item.label}
</Link> </a>
))} ))}
</div> </nav>
{/* User Menu */} {/* User Menu */}
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}> <div style={styles.userMenu}>
<span style={{ color: 'white', fontSize: '14px' }}> <span style={styles.userInfo}>
{user?.name} ({user?.role}) {user?.name} ({user?.role})
</span> </span>
<button <button
onClick={logout} onClick={handleLogout}
style={{ style={styles.logoutBtn}
background: 'none',
border: '1px solid #e74c3c',
color: '#e74c3c',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#e74c3c'; e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
e.currentTarget.style.color = 'white';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'; e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
e.currentTarget.style.color = '#e74c3c';
}} }}
> >
Abmelden Abmelden
@@ -122,67 +178,48 @@ const navigationItems = [
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<button <button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} style={styles.mobileMenuBtn}
style={{ onClick={toggleMobileMenu}
display: 'block',
background: 'none',
border: 'none',
color: 'white',
fontSize: '24px',
cursor: 'pointer'
}}
> >
</button> </button>
</div> </div>
{/* Mobile Menu */} {/* Mobile Navigation */}
{mobileMenuOpen && ( {isMobileMenuOpen && (
<div style={{ <nav style={styles.mobileNav}>
display: 'block', {filteredNavigation.map((item) => (
backgroundColor: '#34495e', <a
padding: '10px 0'
}}>
{filteredNavigation.map(item => (
<Link
key={item.path} key={item.path}
to={item.path} href={item.path}
onClick={() => setMobileMenuOpen(false)} style={styles.mobileNavLink}
style={{ onMouseEnter={(e) => {
display: 'block', e.currentTarget.style.backgroundColor = '#f5f5f5';
color: 'white', }}
textDecoration: 'none', onMouseLeave={(e) => {
padding: '12px 20px', e.currentTarget.style.backgroundColor = 'transparent';
borderBottom: '1px solid #2c3e50' }}
onClick={(e) => {
e.preventDefault();
window.location.href = item.path;
setIsMobileMenuOpen(false);
}} }}
> >
<span style={{ marginRight: '10px' }}>{item.icon}</span>
{item.label} {item.label}
</Link> </a>
))} ))}
<div style={styles.mobileUserInfo}>
<span>{user?.name} ({user?.role})</span>
<button
onClick={handleLogout}
style={styles.mobileLogoutBtn}
>
Abmelden
</button>
</div> </div>
)}
</nav> </nav>
)}
{/* Breadcrumbs */} </header>
<div style={{
backgroundColor: '#ecf0f1',
padding: '10px 20px',
borderBottom: '1px solid #bdc3c7',
fontSize: '14px'
}}>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
{/* Breadcrumb wird dynamisch basierend auf der Route gefüllt */}
<span style={{ color: '#7f8c8d' }}>
🏠 Dashboard {location.pathname !== '/' && '>'}
{location.pathname === '/shift-plans' && ' 📅 Schichtpläne'}
{location.pathname === '/employees' && ' 👥 Mitarbeiter'}
{location.pathname === '/settings' && ' ⚙️ Einstellungen'}
{location.pathname === '/help' && ' ❓ Hilfe'}
</span>
</div>
</div>
</>
); );
}; };

View File

@@ -1,4 +1,3 @@
// frontend/src/contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Employee } from '../types/employee'; import { Employee } from '../types/employee';
@@ -27,7 +26,7 @@ interface AuthProviderProps {
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<Employee | null>(null); const [user, setUser] = useState<Employee | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [needsSetup, setNeedsSetup] = useState(false); const [needsSetup, setNeedsSetup] = useState<boolean | null>(null); // ← Start mit null
// Token aus localStorage laden // Token aus localStorage laden
const getStoredToken = (): string | null => { const getStoredToken = (): string | null => {
@@ -46,16 +45,17 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const checkSetupStatus = async (): Promise<void> => { const checkSetupStatus = async (): Promise<void> => {
try { try {
console.log('🔍 Checking setup status...');
const response = await fetch('http://localhost:3002/api/setup/status'); const response = await fetch('http://localhost:3002/api/setup/status');
if (!response.ok) { if (!response.ok) {
throw new Error('Setup status check failed'); throw new Error('Setup status check failed');
} }
const data = await response.json(); const data = await response.json();
console.log('Setup status response:', data); console.log('Setup status response:', data);
setNeedsSetup(data.needsSetup === true); setNeedsSetup(data.needsSetup === true);
} catch (error) { } catch (error) {
console.error('Error checking setup status:', error); console.error('Error checking setup status:', error);
setNeedsSetup(false); setNeedsSetup(true); // Bei Fehler Setup annehmen
} }
}; };
@@ -65,7 +65,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
console.log('🔄 Refreshing user, token exists:', !!token); console.log('🔄 Refreshing user, token exists:', !!token);
if (!token) { if (!token) {
setLoading(false); console.log(' No token found, user not logged in');
setUser(null);
return; return;
} }
@@ -85,11 +86,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
setUser(null); setUser(null);
} }
} catch (error) { } catch (error) {
console.error('Error refreshing user:', error); console.error('Error refreshing user:', error);
removeStoredToken(); removeStoredToken();
setUser(null); setUser(null);
} finally {
setLoading(false);
} }
}; };
@@ -117,7 +116,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
setStoredToken(data.token); setStoredToken(data.token);
setUser(data.user); setUser(data.user);
} catch (error) { } catch (error) {
console.error('Login error:', error); console.error('Login error:', error);
throw error; throw error;
} }
}; };
@@ -129,24 +128,22 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
}; };
const hasRole = (roles: string[]): boolean => { const hasRole = (roles: string[]): boolean => {
console.log('🔐 Checking roles - User:', user, 'Required roles:', roles); if (!user) return false;
return roles.includes(user.role);
if (!user) {
console.log('❌ No user found');
return false;
}
const hasRequiredRole = roles.includes(user.role);
console.log('✅ User role:', user.role, 'Has required role:', hasRequiredRole);
return hasRequiredRole;
}; };
useEffect(() => { useEffect(() => {
const initializeAuth = async () => { const initializeAuth = async () => {
console.log('🚀 Initializing authentication...'); console.log('🚀 Initializing authentication...');
try {
await checkSetupStatus(); await checkSetupStatus();
await refreshUser(); await refreshUser();
} catch (error) {
console.error('❌ Error during auth initialization:', error);
} finally {
setLoading(false);
console.log('✅ Auth initialization complete - needsSetup:', needsSetup, 'user:', user);
}
}; };
initializeAuth(); initializeAuth();
@@ -159,19 +156,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
hasRole, hasRole,
loading, loading,
refreshUser, refreshUser,
needsSetup, needsSetup: needsSetup === null ? true : needsSetup, // Falls null, true annehmen
checkSetupStatus, checkSetupStatus,
}; };
useEffect(() => {
console.log('🔄 Auth state changed - user:', user, 'loading:', loading);
}, [user, loading]);
useEffect(() => {
const token = getStoredToken();
console.log('💾 Stored token on mount:', token ? 'Exists' : 'None');
}, []);
return ( return (
<AuthContext.Provider value={value}> <AuthContext.Provider value={value}>
{children} {children}

View File

@@ -1,83 +1,120 @@
// frontend/src/pages/Auth/Login.tsx - KORRIGIERT // frontend/src/pages/Auth/Login.tsx - KORRIGIERT
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import { useNotification } from '../../contexts/NotificationContext';
const Login: React.FC = () => { const Login: React.FC = () => {
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { login, user } = useAuth();
const { showNotification } = useNotification();
const navigate = useNavigate();
const { login, user } = useAuth(); // user hinzugefügt für Debugging // 🔥 NEU: Redirect wenn bereits eingeloggt
useEffect(() => {
console.log('🔍 Login Component - Current user:', user); if (user) {
console.log('✅ User already logged in, redirecting to dashboard');
navigate('/');
}
}, [user, navigate]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError('');
setLoading(true); setLoading(true);
try { try {
console.log('🚀 Starting login process...'); console.log('🔐 Attempting login for:', email);
await login({ email, password }); await login({ email, password });
console.log('✅ Login process completed');
// Navigation passiert automatisch durch AuthContext // 🔥 WICHTIG: Erfolgsmeldung und Redirect
console.log('✅ Login successful, redirecting to dashboard');
showNotification({
type: 'success',
title: 'Erfolgreich angemeldet',
message: `Willkommen zurück!`
});
} catch (err: any) { // Navigiere zur Startseite
console.error('❌ Login error:', err); navigate('/');
setError(err.message || 'Login fehlgeschlagen');
} catch (error: any) {
console.error('❌ Login error:', error);
showNotification({
type: 'error',
title: 'Anmeldung fehlgeschlagen',
message: error.message || 'Bitte überprüfen Sie Ihre Anmeldedaten'
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
// Wenn bereits eingeloggt, zeige Ladeanzeige
if (user) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div> Weiterleiten...</div>
</div>
);
}
return ( return (
<div style={{ <div style={{
maxWidth: '400px', display: 'flex',
margin: '100px auto', justifyContent: 'center',
padding: '20px', alignItems: 'center',
border: '1px solid #ddd', minHeight: '100vh',
borderRadius: '8px' backgroundColor: '#f5f5f5'
}}> }}>
<h2>Anmelden</h2> <form onSubmit={handleSubmit} style={{
backgroundColor: 'white',
{error && ( padding: '40px',
<div style={{ borderRadius: '8px',
color: 'red', boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
backgroundColor: '#ffe6e6', width: '100%',
padding: '10px', maxWidth: '400px'
borderRadius: '4px',
marginBottom: '15px'
}}> }}>
<strong>Fehler:</strong> {error} <h2 style={{ textAlign: 'center', marginBottom: '30px' }}>Anmeldung</h2>
</div>
)}
<form onSubmit={handleSubmit}> <div style={{ marginBottom: '20px' }}>
<div style={{ marginBottom: '15px' }}> <label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
<label style={{ display: 'block', marginBottom: '5px' }}> E-Mail
E-Mail:
</label> </label>
<input <input
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }} style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="ihre-email@example.com"
/> />
</div> </div>
<div style={{ marginBottom: '15px' }}> <div style={{ marginBottom: '30px' }}>
<label style={{ display: 'block', marginBottom: '5px' }}> <label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
Passwort: Passwort
</label> </label>
<input <input
type="password" type="password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
style={{ width: '100%', padding: '8px', border: '1px solid #ccc', borderRadius: '4px' }} style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Ihr Passwort"
/> />
</div> </div>
@@ -86,24 +123,18 @@ const Login: React.FC = () => {
disabled={loading} disabled={loading}
style={{ style={{
width: '100%', width: '100%',
padding: '10px', padding: '12px',
backgroundColor: loading ? '#ccc' : '#007bff', backgroundColor: loading ? '#ccc' : '#007bff',
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '4px', borderRadius: '4px',
fontSize: '16px',
cursor: loading ? 'not-allowed' : 'pointer' cursor: loading ? 'not-allowed' : 'pointer'
}} }}
> >
{loading ? 'Anmeldung...' : 'Anmelden'} {loading ? '⏳ Wird angemeldet...' : 'Anmelden'}
</button> </button>
</form> </form>
<div style={{ marginTop: '15px', textAlign: 'center' }}>
<p><strong>Test Accounts:</strong></p>
<p>admin@schichtplan.de / admin123</p>
<p>instandhalter@schichtplan.de / instandhalter123</p>
<p>user@schichtplan.de / user123</p>
</div>
</div> </div>
); );
}; };

View File

@@ -1,4 +1,4 @@
// frontend/src/pages/Employees/components/EmployeeForm.tsx // frontend/src/pages/Employees/components/EmployeeForm.tsx - VEREINFACHT
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../types/employee'; import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../types/employee';
import { employeeService } from '../../../services/employeeService'; import { employeeService } from '../../../services/employeeService';
@@ -18,6 +18,28 @@ const ROLE_OPTIONS = [
{ value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen' } { value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen' }
] as const; ] as const;
// Mitarbeiter Typen Definition
const EMPLOYEE_TYPE_OPTIONS = [
{
value: 'chef',
label: '👨‍💼 Chef/Administrator',
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung',
color: '#e74c3c'
},
{
value: 'erfahren',
label: '👴 Erfahren',
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen',
color: '#3498db'
},
{
value: 'neuling',
label: '👶 Neuling',
description: 'Benötigt Einarbeitung und Unterstützung',
color: '#27ae60'
}
] as const;
const EmployeeForm: React.FC<EmployeeFormProps> = ({ const EmployeeForm: React.FC<EmployeeFormProps> = ({
mode, mode,
employee, employee,
@@ -29,8 +51,9 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
email: '', email: '',
password: '', password: '',
role: 'user' as 'admin' | 'instandhalter' | 'user', role: 'user' as 'admin' | 'instandhalter' | 'user',
phone: '', employeeType: 'neuling' as 'chef' | 'neuling' | 'erfahren',
department: '', isSufficientlyIndependent: false,
notes: '',
isActive: true isActive: true
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -42,16 +65,17 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
setFormData({ setFormData({
name: employee.name, name: employee.name,
email: employee.email, email: employee.email,
password: '', // Passwort wird beim Editieren nicht angezeigt password: '',
role: employee.role, role: employee.role,
phone: employee.phone || '', employeeType: employee.employeeType,
department: employee.department || '', isSufficientlyIndependent: employee.isSufficientlyIndependent,
notes: employee.notes || '',
isActive: employee.isActive isActive: employee.isActive
}); });
} }
}, [mode, employee]); }, [mode, employee]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target; const { name, value, type } = e.target;
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
@@ -59,7 +83,6 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
})); }));
}; };
// NEU: Checkbox für Rollen
const handleRoleChange = (roleValue: 'admin' | 'instandhalter' | 'user') => { const handleRoleChange = (roleValue: 'admin' | 'instandhalter' | 'user') => {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
@@ -67,6 +90,16 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
})); }));
}; };
const handleEmployeeTypeChange = (employeeType: 'chef' | 'neuling' | 'erfahren') => {
setFormData(prev => ({
...prev,
employeeType,
// Automatische Werte basierend auf Typ
isSufficientlyIndependent: employeeType === 'chef' ? true :
employeeType === 'erfahren' ? true : false
}));
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
@@ -79,17 +112,19 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
email: formData.email, email: formData.email,
password: formData.password, password: formData.password,
role: formData.role, role: formData.role,
phone: formData.phone || undefined, employeeType: formData.employeeType,
department: formData.department || undefined isSufficientlyIndependent: formData.isSufficientlyIndependent,
notes: formData.notes || undefined
}; };
await employeeService.createEmployee(createData); await employeeService.createEmployee(createData);
} else if (employee) { } else if (employee) {
const updateData: UpdateEmployeeRequest = { const updateData: UpdateEmployeeRequest = {
name: formData.name, name: formData.name,
role: formData.role, role: formData.role,
employeeType: formData.employeeType,
isSufficientlyIndependent: formData.isSufficientlyIndependent,
isActive: formData.isActive, isActive: formData.isActive,
phone: formData.phone || undefined, notes: formData.notes || undefined
department: formData.department || undefined
}; };
await employeeService.updateEmployee(employee.id, updateData); await employeeService.updateEmployee(employee.id, updateData);
} }
@@ -111,22 +146,21 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
return formData.name.trim() && formData.email.trim(); return formData.name.trim() && formData.email.trim();
}; };
// Bestimme welche Rollen der aktuelle Benutzer vergeben darf
const getAvailableRoles = () => { const getAvailableRoles = () => {
if (hasRole(['admin'])) { if (hasRole(['admin'])) {
return ROLE_OPTIONS; // Admins können alle Rollen vergeben return ROLE_OPTIONS;
} }
if (hasRole(['instandhalter'])) { if (hasRole(['instandhalter'])) {
return ROLE_OPTIONS.filter(role => role.value !== 'admin'); // Instandhalter können keine Admins erstellen return ROLE_OPTIONS.filter(role => role.value !== 'admin');
} }
return ROLE_OPTIONS.filter(role => role.value === 'user'); // Normale User können gar nichts (sollte nicht vorkommen) return ROLE_OPTIONS.filter(role => role.value === 'user');
}; };
const availableRoles = getAvailableRoles(); const availableRoles = getAvailableRoles();
return ( return (
<div style={{ <div style={{
maxWidth: '600px', maxWidth: '700px',
margin: '0 auto', margin: '0 auto',
backgroundColor: 'white', backgroundColor: 'white',
padding: '30px', padding: '30px',
@@ -158,14 +192,19 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div style={{ display: 'grid', gap: '20px' }}> <div style={{ display: 'grid', gap: '20px' }}>
{/* Name */}
<div> {/* Grundinformationen */}
<label style={{ <div style={{
display: 'block', padding: '20px',
marginBottom: '8px', backgroundColor: '#f8f9fa',
fontWeight: 'bold', borderRadius: '8px',
color: '#2c3e50' border: '1px solid #e9ecef'
}}> }}>
<h3 style={{ margin: '0 0 15px 0', color: '#495057' }}>📋 Grundinformationen</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Vollständiger Name * Vollständiger Name *
</label> </label>
<input <input
@@ -185,14 +224,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
/> />
</div> </div>
{/* E-Mail */}
<div> <div>
<label style={{ <label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
E-Mail Adresse * E-Mail Adresse *
</label> </label>
<input <input
@@ -211,16 +244,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
placeholder="max.mustermann@example.com" placeholder="max.mustermann@example.com"
/> />
</div> </div>
</div>
{/* Passwort (nur bei Erstellung) */}
{mode === 'create' && ( {mode === 'create' && (
<div> <div style={{ marginTop: '15px' }}>
<label style={{ <label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
Passwort * Passwort *
</label> </label>
<input <input
@@ -244,93 +272,39 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div> </div>
</div> </div>
)} )}
{/* Telefon */}
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
Telefonnummer
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="+49 123 456789"
/>
</div> </div>
{/* Abteilung */} {/* Mitarbeiter Kategorie */}
<div> <div style={{
<label style={{ padding: '20px',
display: 'block', backgroundColor: '#f8f9fa',
marginBottom: '8px', borderRadius: '8px',
fontWeight: 'bold', border: '1px solid #e9ecef'
color: '#2c3e50'
}}> }}>
Abteilung <h3 style={{ margin: '0 0 15px 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3>
</label>
<input
type="text"
name="department"
value={formData.department}
onChange={handleChange}
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="z.B. Produktion, Logistik, Verwaltung"
/>
</div>
{/* NEU: Rollen als Checkboxes */}
<div>
<label style={{
display: 'block',
marginBottom: '12px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
Rolle *
</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{availableRoles.map(role => ( {EMPLOYEE_TYPE_OPTIONS.map(type => (
<div <div
key={role.value} key={type.value}
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'flex-start',
padding: '5px', padding: '15px',
border: `2px solid ${formData.role === role.value ? '#3498db' : '#e0e0e0'}`, border: `2px solid ${formData.employeeType === type.value ? type.color : '#e0e0e0'}`,
borderRadius: '8px', borderRadius: '8px',
backgroundColor: formData.role === role.value ? '#f8fafc' : 'white', backgroundColor: formData.employeeType === type.value ? '#f8fafc' : 'white',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s' transition: 'all 0.2s'
}} }}
onClick={() => handleRoleChange(role.value)} onClick={() => handleEmployeeTypeChange(type.value)}
> >
<input <input
type="radio" type="radio"
name="role" name="employeeType"
value={role.value} value={type.value}
checked={formData.role === role.value} checked={formData.employeeType === type.value}
onChange={() => handleRoleChange(role.value)} onChange={() => handleEmployeeTypeChange(type.value)}
style={{ style={{
marginRight: '12px', marginRight: '12px',
marginTop: '2px', marginTop: '2px',
@@ -342,52 +316,179 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
<div style={{ <div style={{
fontWeight: 'bold', fontWeight: 'bold',
color: '#2c3e50', color: '#2c3e50',
marginBottom: '4px' marginBottom: '4px',
fontSize: '16px'
}}> }}>
{role.label} {type.label}
</div> </div>
<div style={{ <div style={{
fontSize: '14px', fontSize: '14px',
color: '#7f8c8d', color: '#7f8c8d',
lineHeight: '1.4' lineHeight: '1.4'
}}> }}>
{role.description} {type.description}
</div> </div>
</div> </div>
{/* Role Badge */}
<div style={{ <div style={{
padding: '4px 8px', padding: '6px 12px',
backgroundColor: backgroundColor: type.color,
role.value === 'admin' ? '#e74c3c' :
role.value === 'instandhalter' ? '#3498db' : '#27ae60',
color: 'white', color: 'white',
borderRadius: '12px', borderRadius: '15px',
fontSize: '12px', fontSize: '12px',
fontWeight: 'bold' fontWeight: 'bold'
}}> }}>
{role.value.toUpperCase()} {type.value.toUpperCase()}
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div>
{/* Info über Berechtigungen */} {/* Eigenständigkeit */}
<div style={{ <div style={{
marginTop: '10px', padding: '20px',
padding: '10px', backgroundColor: '#f8f9fa',
backgroundColor: '#e8f4fd', borderRadius: '8px',
border: '1px solid #b6d7e8', border: '1px solid #e9ecef'
borderRadius: '4px',
fontSize: '12px',
color: '#2c3e50'
}}> }}>
<strong>Info:</strong> { <h3 style={{ margin: '0 0 15px 0', color: '#495057' }}>🎯 Eigenständigkeit</h3>
formData.role === 'admin' ? 'Administratoren haben vollen Zugriff auf alle Funktionen.' :
formData.role === 'instandhalter' ? 'Instandhalter können Schichtpläne erstellen und Mitarbeiter verwalten.' : <div style={{
'Mitarbeiter können ihre eigenen Schichten und Verfügbarkeiten einsehen.' display: 'flex',
alignItems: 'center',
gap: '15px',
padding: '15px',
border: '1px solid #e0e0e0',
borderRadius: '6px',
backgroundColor: '#fff'
}}>
<input
type="checkbox"
name="isSufficientlyIndependent"
id="isSufficientlyIndependent"
checked={formData.isSufficientlyIndependent}
onChange={handleChange}
disabled={formData.employeeType === 'chef'}
style={{
width: '20px',
height: '20px',
opacity: formData.employeeType === 'chef' ? 0.5 : 1
}}
/>
<div style={{ flex: 1 }}>
<label htmlFor="isSufficientlyIndependent" style={{
fontWeight: 'bold',
color: '#2c3e50',
display: 'block',
opacity: formData.employeeType === 'chef' ? 0.5 : 1
}}>
Als ausreichend eigenständig markieren
{formData.employeeType === 'chef' && ' (Automatisch für Chefs)'}
</label>
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
{formData.employeeType === 'chef'
? 'Chefs sind automatisch als eigenständig markiert.'
: 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.'
} }
</div> </div>
</div> </div>
<div style={{
padding: '6px 12px',
backgroundColor: formData.isSufficientlyIndependent ? '#27ae60' : '#e74c3c',
color: 'white',
borderRadius: '15px',
fontSize: '12px',
fontWeight: 'bold',
opacity: formData.employeeType === 'chef' ? 0.7 : 1
}}>
{formData.isSufficientlyIndependent ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
</div>
</div>
</div>
{/* Bemerkungen */}
<div style={{
padding: '20px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '1px solid #e9ecef'
}}>
<h3 style={{ margin: '0 0 15px 0', color: '#495057' }}> Bemerkungen</h3>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Notizen & Hinweise
</label>
<textarea
name="notes"
value={formData.notes}
onChange={handleChange}
rows={3}
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px',
resize: 'vertical'
}}
placeholder="Besondere Fähigkeiten, Einschränkungen, Schulungen, wichtige Hinweise..."
/>
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}>
Optionale Notizen für interne Zwecke
</div>
</div>
</div>
{/* Systemrolle (nur für Admins) */}
{hasRole(['admin']) && (
<div style={{
padding: '20px',
backgroundColor: '#fff3cd',
borderRadius: '8px',
border: '1px solid #ffeaa7'
}}>
<h3 style={{ margin: '0 0 15px 0', color: '#856404' }}> Systemrolle</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{availableRoles.map(role => (
<div
key={role.value}
style={{
display: 'flex',
alignItems: 'flex-start',
padding: '12px',
border: `2px solid ${formData.role === role.value ? '#f39c12' : '#e0e0e0'}`,
borderRadius: '6px',
backgroundColor: formData.role === role.value ? '#fef9e7' : 'white',
cursor: 'pointer'
}}
onClick={() => handleRoleChange(role.value)}
>
<input
type="radio"
name="role"
value={role.value}
checked={formData.role === role.value}
onChange={() => handleRoleChange(role.value)}
style={{
marginRight: '10px',
marginTop: '2px'
}}
/>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 'bold', color: '#2c3e50' }}>
{role.label}
</div>
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
{role.description}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Aktiv Status (nur beim Bearbeiten) */} {/* Aktiv Status (nur beim Bearbeiten) */}
{mode === 'edit' && ( {mode === 'edit' && (

View File

@@ -6,12 +6,11 @@ import { useAuth } from '../../../contexts/AuthContext';
interface EmployeeListProps { interface EmployeeListProps {
employees: Employee[]; employees: Employee[];
onEdit: (employee: Employee) => void; onEdit: (employee: Employee) => void;
onDelete: (employee: Employee) => void; // Jetzt mit Employee-Objekt onDelete: (employee: Employee) => void;
onManageAvailability: (employee: Employee) => void; onManageAvailability: (employee: Employee) => void;
currentUserRole: 'admin' | 'instandhalter'; currentUserRole: 'admin' | 'instandhalter';
} }
const EmployeeList: React.FC<EmployeeListProps> = ({ const EmployeeList: React.FC<EmployeeListProps> = ({
employees, employees,
onEdit, onEdit,
@@ -34,7 +33,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
return ( return (
employee.name.toLowerCase().includes(term) || employee.name.toLowerCase().includes(term) ||
employee.email.toLowerCase().includes(term) || employee.email.toLowerCase().includes(term) ||
employee.department?.toLowerCase().includes(term) || employee.employeeType.toLowerCase().includes(term) ||
employee.role.toLowerCase().includes(term) employee.role.toLowerCase().includes(term)
); );
} }
@@ -51,13 +50,28 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
} }
}; };
const getEmployeeTypeBadge = (type: string) => {
switch (type) {
case 'chef': return { text: '👨‍💼 CHEF', color: '#e74c3c', bgColor: '#fadbd8' };
case 'erfahren': return { text: '👴 ERFAHREN', color: '#3498db', bgColor: '#d6eaf8' };
case 'neuling': return { text: '👶 NEULING', color: '#27ae60', bgColor: '#d5f4e6' };
default: return { text: 'UNBEKANNT', color: '#95a5a6', bgColor: '#ecf0f1' };
}
};
const getIndependenceBadge = (isIndependent: boolean) => {
return isIndependent
? { text: '✅ Eigenständig', color: '#27ae60', bgColor: '#d5f4e6' }
: { text: '❌ Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
};
const getStatusBadge = (isActive: boolean) => { const getStatusBadge = (isActive: boolean) => {
return isActive return isActive
? { text: 'Aktiv', color: '#27ae60', bgColor: '#d5f4e6' } ? { text: 'Aktiv', color: '#27ae60', bgColor: '#d5f4e6' }
: { text: 'Inaktiv', color: '#e74c3c', bgColor: '#fadbd8' }; : { text: 'Inaktiv', color: '#e74c3c', bgColor: '#fadbd8' };
}; };
// NEU: Kann Benutzer löschen? // Kann Benutzer löschen?
const canDeleteEmployee = (employee: Employee): boolean => { const canDeleteEmployee = (employee: Employee): boolean => {
// Nur Admins können löschen // Nur Admins können löschen
if (currentUserRole !== 'admin') return false; if (currentUserRole !== 'admin') return false;
@@ -71,7 +85,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
return true; return true;
}; };
// NEU: Kann Benutzer bearbeiten? // Kann Benutzer bearbeiten?
const canEditEmployee = (employee: Employee): boolean => { const canEditEmployee = (employee: Employee): boolean => {
// Admins können alle bearbeiten // Admins können alle bearbeiten
if (currentUserRole === 'admin') return true; if (currentUserRole === 'admin') return true;
@@ -134,7 +148,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
<label style={{ fontWeight: 'bold', color: '#2c3e50' }}>Suchen:</label> <label style={{ fontWeight: 'bold', color: '#2c3e50' }}>Suchen:</label>
<input <input
type="text" type="text"
placeholder="Nach Name, E-Mail oder Abteilung suchen..." placeholder="Nach Name, E-Mail oder Typ suchen..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
style={{ style={{
@@ -152,7 +166,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</div> </div>
</div> </div>
{/* Mitarbeiter Tabelle - SYMMETRISCH KORRIGIERT */} {/* Mitarbeiter Tabelle */}
<div style={{ <div style={{
backgroundColor: 'white', backgroundColor: 'white',
borderRadius: '8px', borderRadius: '8px',
@@ -160,10 +174,10 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
overflow: 'hidden', overflow: 'hidden',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)' boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}> }}>
{/* Tabellen-Header - SYMMETRISCH */} {/* Tabellen-Header */}
<div style={{ <div style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr 120px', // Feste Breite für Aktionen gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr 1fr 120px',
gap: '15px', gap: '15px',
padding: '15px 20px', padding: '15px 20px',
backgroundColor: '#f8f9fa', backgroundColor: '#f8f9fa',
@@ -173,7 +187,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
alignItems: 'center' alignItems: 'center'
}}> }}>
<div>Name & E-Mail</div> <div>Name & E-Mail</div>
<div>Abteilung</div> <div>Typ</div>
<div style={{ textAlign: 'center' }}>Eigenständigkeit</div>
<div style={{ textAlign: 'center' }}>Rolle</div> <div style={{ textAlign: 'center' }}>Rolle</div>
<div style={{ textAlign: 'center' }}>Status</div> <div style={{ textAlign: 'center' }}>Status</div>
<div style={{ textAlign: 'center' }}>Letzter Login</div> <div style={{ textAlign: 'center' }}>Letzter Login</div>
@@ -181,7 +196,10 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</div> </div>
{filteredEmployees.map(employee => { {filteredEmployees.map(employee => {
const status = getStatusBadge(employee.isActive ?? true); const employeeType = getEmployeeTypeBadge(employee.employeeType);
const independence = getIndependenceBadge(employee.isSufficientlyIndependent);
const roleColor = getRoleBadgeColor(employee.role);
const status = getStatusBadge(employee.isActive);
const canEdit = canEditEmployee(employee); const canEdit = canEditEmployee(employee);
const canDelete = canDeleteEmployee(employee); const canDelete = canDeleteEmployee(employee);
@@ -190,7 +208,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
key={employee.id} key={employee.id}
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr 120px', // Gleiche Spalten wie Header gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr 1fr 120px',
gap: '15px', gap: '15px',
padding: '15px 20px', padding: '15px 20px',
borderBottom: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0',
@@ -217,18 +235,45 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</div> </div>
</div> </div>
{/* Abteilung */} {/* Mitarbeiter Typ */}
<div style={{ color: '#666' }}>
{employee.department || (
<span style={{ color: '#999', fontStyle: 'italic' }}>Nicht zugewiesen</span>
)}
</div>
{/* Rolle - ZENTRIERT */}
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<span <span
style={{ style={{
backgroundColor: getRoleBadgeColor(employee.role), backgroundColor: employeeType.bgColor,
color: employeeType.color,
padding: '6px 12px',
borderRadius: '15px',
fontSize: '12px',
fontWeight: 'bold',
display: 'inline-block'
}}
>
{employeeType.text}
</span>
</div>
{/* Eigenständigkeit */}
<div style={{ textAlign: 'center' }}>
<span
style={{
backgroundColor: independence.bgColor,
color: independence.color,
padding: '6px 12px',
borderRadius: '15px',
fontSize: '12px',
fontWeight: 'bold',
display: 'inline-block'
}}
>
{independence.text}
</span>
</div>
{/* Rolle */}
<div style={{ textAlign: 'center' }}>
<span
style={{
backgroundColor: roleColor,
color: 'white', color: 'white',
padding: '6px 12px', padding: '6px 12px',
borderRadius: '15px', borderRadius: '15px',
@@ -243,7 +288,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</span> </span>
</div> </div>
{/* Status - ZENTRIERT */} {/* Status */}
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<span <span
style={{ style={{
@@ -261,7 +306,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</span> </span>
</div> </div>
{/* Letzter Login - ZENTRIERT */} {/* Letzter Login */}
<div style={{ textAlign: 'center', fontSize: '14px', color: '#666' }}> <div style={{ textAlign: 'center', fontSize: '14px', color: '#666' }}>
{employee.lastLogin {employee.lastLogin
? new Date(employee.lastLogin).toLocaleDateString('de-DE') ? new Date(employee.lastLogin).toLocaleDateString('de-DE')
@@ -269,14 +314,14 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
} }
</div> </div>
{/* Aktionen - ZENTRIERT und SYMMETRISCH */} {/* Aktionen */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
gap: '8px', gap: '8px',
justifyContent: 'center', justifyContent: 'center',
flexWrap: 'wrap' flexWrap: 'wrap'
}}> }}>
{/* Verfügbarkeit Button - immer sichtbar für berechtigte */} {/* Verfügbarkeit Button */}
{(currentUserRole === 'admin' || currentUserRole === 'instandhalter') && ( {(currentUserRole === 'admin' || currentUserRole === 'instandhalter') && (
<button <button
onClick={() => onManageAvailability(employee)} onClick={() => onManageAvailability(employee)}
@@ -318,7 +363,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</button> </button>
)} )}
{/* Löschen Button - NUR FÜR ADMINS */} {/* Löschen Button */}
{canDelete && ( {canDelete && (
<button <button
onClick={() => onDelete(employee)} onClick={() => onDelete(employee)}
@@ -333,13 +378,13 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
minWidth: '32px', minWidth: '32px',
height: '32px' height: '32px'
}} }}
title="Mitarbeiter deaktivieren" title="Mitarbeiter löschen"
> >
🗑 🗑
</button> </button>
)} )}
{/* Platzhalter für Symmetrie wenn keine Aktionen */} {/* Platzhalter für Symmetrie */}
{!canEdit && !canDelete && (currentUserRole !== 'admin' && currentUserRole !== 'instandhalter') && ( {!canEdit && !canDelete && (currentUserRole !== 'admin' && currentUserRole !== 'instandhalter') && (
<div style={{ width: '32px', height: '32px' }}></div> <div style={{ width: '32px', height: '32px' }}></div>
)} )}
@@ -349,7 +394,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
})} })}
</div> </div>
{/* NEU: Info-Box über Berechtigungen */} {/* Info-Box über Berechtigungen */}
<div style={{ <div style={{
marginTop: '20px', marginTop: '20px',
padding: '15px', padding: '15px',
@@ -367,6 +412,53 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
<li>Benutzer können sich <strong>nicht selbst löschen</strong></li> <li>Benutzer können sich <strong>nicht selbst löschen</strong></li>
</ul> </ul>
</div> </div>
{/* Legende für Mitarbeiter Typen */}
<div style={{
marginTop: '20px',
padding: '15px',
backgroundColor: '#f8f9fa',
border: '1px solid #e9ecef',
borderRadius: '6px',
fontSize: '14px'
}}>
<strong>🎯 Legende Mitarbeiter Typen:</strong>
<div style={{ display: 'flex', gap: '15px', marginTop: '10px', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{
backgroundColor: '#fadbd8',
color: '#e74c3c',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: 'bold'
}}>👨💼 CHEF</span>
<span style={{ fontSize: '12px', color: '#666' }}>Vollzugriff</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{
backgroundColor: '#d6eaf8',
color: '#3498db',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: 'bold'
}}>👴 ERFAHREN</span>
<span style={{ fontSize: '12px', color: '#666' }}>Langjährige Erfahrung</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<span style={{
backgroundColor: '#d5f4e6',
color: '#27ae60',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: 'bold'
}}>👶 NEULING</span>
<span style={{ fontSize: '12px', color: '#666' }}>Benötigt Einarbeitung</span>
</div>
</div>
</div>
</div> </div>
); );
}; };

View File

@@ -1,6 +1,5 @@
// frontend/src/pages/Setup/Setup.tsx // frontend/src/pages/Setup/Setup.tsx - KORRIGIERT
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
const Setup: React.FC = () => { const Setup: React.FC = () => {
@@ -8,14 +7,11 @@ const Setup: React.FC = () => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
password: '', password: '',
confirmPassword: '', confirmPassword: '',
name: '', name: ''
phone: '',
department: ''
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const navigate = useNavigate(); const { checkSetupStatus } = useAuth();
const { login, checkSetupStatus } = useAuth();
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
@@ -66,12 +62,10 @@ const Setup: React.FC = () => {
const payload = { const payload = {
password: formData.password, password: formData.password,
name: formData.name, name: formData.name
...(formData.phone ? { phone: formData.phone } : {}),
...(formData.department ? { department: formData.department } : {})
}; };
console.log('🚀 Sending setup request with payload:', payload); console.log('🚀 Sending setup request...');
const response = await fetch('http://localhost:3002/api/setup/admin', { const response = await fetch('http://localhost:3002/api/setup/admin', {
method: 'POST', method: 'POST',
@@ -81,66 +75,110 @@ const Setup: React.FC = () => {
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
const responseText = await response.text();
console.log('📨 Setup response:', responseText);
let result;
try {
result = JSON.parse(responseText);
} catch (parseError) {
console.error('❌ Failed to parse response as JSON:', responseText);
throw new Error('Invalid server response');
}
if (!response.ok) { if (!response.ok) {
throw new Error(result.error || 'Setup fehlgeschlagen'); const data = await response.json();
throw new Error(data.error || 'Setup fehlgeschlagen');
} }
const result = await response.json();
console.log('✅ Setup successful:', result); console.log('✅ Setup successful:', result);
// WICHTIG: Setup Status neu prüfen und dann zu Login navigieren // Setup Status neu prüfen
await checkSetupStatus(); await checkSetupStatus();
// Kurze Verzögerung damit der State aktualisiert werden kann
setTimeout(() => {
navigate('/login');
}, 100);
} catch (err: any) { } catch (err: any) {
console.error('❌ Setup error:', err); console.error('❌ Setup error:', err);
setError(typeof err === 'string' ? err : err.message || 'Ein unerwarteter Fehler ist aufgetreten'); setError(err.message || 'Ein unerwarteter Fehler ist aufgetreten');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
return ( return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center"> <div style={{
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8"> minHeight: '100vh',
<div className="text-center mb-8"> backgroundColor: '#f8f9fa',
<h1 className="text-3xl font-bold mb-2">Erstkonfiguration</h1> display: 'flex',
<p className="text-gray-600"> alignItems: 'center',
Konfigurieren Sie den Administrator-Account justifyContent: 'center',
padding: '2rem'
}}>
<div style={{
backgroundColor: 'white',
padding: '3rem',
borderRadius: '12px',
boxShadow: '0 10px 30px rgba(0,0,0,0.1)',
width: '100%',
maxWidth: '500px',
border: '1px solid #e9ecef'
}}>
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<h1 style={{
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '0.5rem',
color: '#2c3e50'
}}>
🚀 Erstkonfiguration
</h1>
<p style={{
color: '#6c757d',
fontSize: '1.1rem'
}}>
Richten Sie Ihren Administrator-Account ein
</p> </p>
</div> </div>
{error && ( {error && (
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded"> <div style={{
backgroundColor: '#f8d7da',
border: '1px solid #f5c6cb',
color: '#721c24',
padding: '1rem',
borderRadius: '6px',
marginBottom: '1.5rem'
}}>
{error} {error}
</div> </div>
)} )}
{step === 1 && ( {step === 1 && (
<div className="space-y-4"> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label style={{
Admin E-Mail display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Administrator E-Mail
</label> </label>
<div className="p-2 bg-gray-100 border rounded"> <div style={{
padding: '0.75rem',
backgroundColor: '#e9ecef',
border: '1px solid #ced4da',
borderRadius: '6px',
color: '#495057',
fontWeight: '500'
}}>
admin@instandhaltung.de admin@instandhaltung.de
</div> </div>
<div style={{
fontSize: '0.875rem',
color: '#6c757d',
marginTop: '0.25rem'
}}>
Diese E-Mail wird für den Administrator-Account verwendet
</div> </div>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort Passwort
</label> </label>
<input <input
@@ -148,13 +186,26 @@ const Setup: React.FC = () => {
name="password" name="password"
value={formData.password} value={formData.password}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Mindestens 6 Zeichen" placeholder="Mindestens 6 Zeichen"
required required
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort bestätigen Passwort bestätigen
</label> </label>
<input <input
@@ -162,7 +213,14 @@ const Setup: React.FC = () => {
name="confirmPassword" name="confirmPassword"
value={formData.confirmPassword} value={formData.confirmPassword}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Passwort wiederholen" placeholder="Passwort wiederholen"
required required
/> />
@@ -171,76 +229,95 @@ const Setup: React.FC = () => {
)} )}
{step === 2 && ( {step === 2 && (
<div className="space-y-4"> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label style={{
Name display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Vollständiger Name
</label> </label>
<input <input
type="text" type="text"
name="name" name="name"
value={formData.name} value={formData.name}
onChange={handleInputChange} onChange={handleInputChange}
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500" style={{
placeholder="Vollständiger Name" width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Max Mustermann"
required required
/> />
</div> </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>
)} )}
<div className="mt-6 flex justify-between"> <div style={{
marginTop: '2rem',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
{step === 2 && ( {step === 2 && (
<button <button
onClick={handleBack} onClick={handleBack}
className="px-4 py-2 text-gray-600 hover:text-gray-800" style={{
padding: '0.75rem 1.5rem',
color: '#6c757d',
border: '1px solid #6c757d',
background: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: '500'
}}
disabled={loading} disabled={loading}
> >
Zurück Zurück
</button> </button>
)} )}
<button <button
onClick={handleNext} onClick={handleNext}
disabled={loading} disabled={loading}
className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 ${ style={{
step === 1 ? 'ml-auto' : '' padding: '0.75rem 2rem',
}`} backgroundColor: loading ? '#6c757d' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: loading ? 'not-allowed' : 'pointer',
fontWeight: '600',
fontSize: '1rem',
marginLeft: step === 1 ? 'auto' : '0',
transition: 'background-color 0.3s ease'
}}
> >
{loading ? ( {loading ? '⏳ Wird verarbeitet...' :
'⏳ Verarbeite...' step === 1 ? 'Weiter →' : 'Setup abschließen'}
) : step === 1 ? (
'Weiter'
) : (
'Setup abschließen'
)}
</button> </button>
</div> </div>
{step === 2 && (
<div style={{
marginTop: '1.5rem',
textAlign: 'center',
color: '#6c757d',
fontSize: '0.9rem',
padding: '1rem',
backgroundColor: '#e7f3ff',
borderRadius: '6px',
border: '1px solid #b6d7e8'
}}>
💡 Nach dem erfolgreichen Setup werden Sie zur Anmeldeseite weitergeleitet,
wo Sie sich mit Ihren Zugangsdaten anmelden können.
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -4,20 +4,12 @@ export interface Employee {
email: string; email: string;
name: string; name: string;
role: 'admin' | 'instandhalter' | 'user'; role: 'admin' | 'instandhalter' | 'user';
employeeType: 'chef' | 'neuling' | 'erfahren';
isSufficientlyIndependent: boolean;
isActive: boolean; isActive: boolean;
createdAt: string; createdAt: string;
lastLogin?: string | null; lastLogin?: string | null;
phone?: string; notes?: string;
department?: string;
}
export interface Availability {
id: string;
employeeId: string;
dayOfWeek: number; // 0-6 (Sonntag-Samstag)
startTime: string; // "08:00"
endTime: string; // "16:00"
isAvailable: boolean;
} }
export interface CreateEmployeeRequest { export interface CreateEmployeeRequest {
@@ -25,14 +17,25 @@ export interface CreateEmployeeRequest {
password: string; password: string;
name: string; name: string;
role: 'admin' | 'instandhalter' | 'user'; role: 'admin' | 'instandhalter' | 'user';
phone?: string; employeeType: 'chef' | 'neuling' | 'erfahren';
department?: string; isSufficientlyIndependent: boolean;
notes?: string;
} }
export interface UpdateEmployeeRequest { export interface UpdateEmployeeRequest {
name?: string; name?: string;
role?: 'admin' | 'instandhalter' | 'user'; role?: 'admin' | 'instandhalter' | 'user';
employeeType?: 'chef' | 'neuling' | 'erfahren';
isSufficientlyIndependent?: boolean;
isActive?: boolean; isActive?: boolean;
phone?: string; notes?: string;
department?: string; }
export interface Availability {
id: string;
employeeId: string;
dayOfWeek: number;
startTime: string;
endTime: string;
isAvailable: boolean;
} }

View File

@@ -4,9 +4,12 @@ export interface User {
email: string; email: string;
name: string; name: string;
role: 'admin' | 'instandhalter' | 'user'; role: 'admin' | 'instandhalter' | 'user';
phone?: string; employeeType: 'chef' | 'neuling' | 'erfahren';
department?: string; isSufficientlyIndependent: boolean;
lastLogin?: string; isActive: boolean;
createdAt: string;
lastLogin?: string | null;
notes?: string;
} }
export interface LoginRequest { export interface LoginRequest {