settings works for every user

This commit is contained in:
2025-10-13 14:34:45 +02:00
parent dec92daf7c
commit b6fd57dfc7
13 changed files with 505 additions and 456 deletions

View File

@@ -372,6 +372,15 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise<v
const { id } = req.params; const { id } = req.params;
const { currentPassword, newPassword } = req.body; const { currentPassword, newPassword } = req.body;
// Get the current user from the auth middleware
const currentUser = (req as AuthRequest).user;
// Check if user is changing their own password or is an admin
if (currentUser?.userId !== id && currentUser?.role !== 'admin') {
res.status(403).json({ error: 'You can only change your own password' });
return;
}
// Check if employee exists and get password // Check if employee exists and get password
const employee = await db.get<{ password: string }>('SELECT password FROM employees WHERE id = ?', [id]); const employee = await db.get<{ password: string }>('SELECT password FROM employees WHERE id = ?', [id]);
if (!employee) { if (!employee) {
@@ -379,12 +388,20 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise<v
return; return;
} }
// Verify current password // For non-admin users, verify current password
if (currentUser?.role !== 'admin') {
const isValidPassword = await bcrypt.compare(currentPassword, employee.password); const isValidPassword = await bcrypt.compare(currentPassword, employee.password);
if (!isValidPassword) { if (!isValidPassword) {
res.status(400).json({ error: 'Current password is incorrect' }); res.status(400).json({ error: 'Current password is incorrect' });
return; return;
} }
}
// Validate new password
if (!newPassword || newPassword.length < 6) {
res.status(400).json({ error: 'New password must be at least 6 characters long' });
return;
}
// Hash new password // Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 10); const hashedPassword = await bcrypt.hash(newPassword, 10);

View File

@@ -681,7 +681,7 @@ async function generateScheduledShifts(planId: string, startDate: string, endDat
} }
} }
export const getTemplates = async (req: Request, res: Response): Promise<void> => { /*export const getTemplates = async (req: Request, res: Response): Promise<void> => {
try { try {
console.log('🔍 Lade Vorlagen...'); console.log('🔍 Lade Vorlagen...');
@@ -707,7 +707,7 @@ export const getTemplates = async (req: Request, res: Response): Promise<void> =
console.error('Error fetching templates:', error); console.error('Error fetching templates:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}; };*/
// Neue Funktion: Create from Template // Neue Funktion: Create from Template
/*export const createFromTemplate = async (req: Request, res: Response): Promise<void> => { /*export const createFromTemplate = async (req: Request, res: Response): Promise<void> => {

View File

@@ -23,10 +23,10 @@ router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
router.post('/', requireRole(['admin']), createEmployee); router.post('/', requireRole(['admin']), createEmployee);
router.put('/:id', requireRole(['admin']), updateEmployee); router.put('/:id', requireRole(['admin']), updateEmployee);
router.delete('/:id', requireRole(['admin']), deleteEmployee); router.delete('/:id', requireRole(['admin']), deleteEmployee);
router.put('/:id/password', requireRole(['admin']), changePassword); router.put('/:id/password', authMiddleware, changePassword);
// Availability Routes // Availability Routes
router.get('/:employeeId/availabilities', requireRole(['admin', 'instandhalter']), getAvailabilities); router.get('/:employeeId/availabilities', authMiddleware, getAvailabilities);
router.put('/:employeeId/availabilities', requireRole(['admin', 'instandhalter']), updateAvailabilities); router.put('/:employeeId/availabilities', authMiddleware, updateAvailabilities);
export default router; export default router;

View File

@@ -7,7 +7,7 @@ import {
createShiftPlan, createShiftPlan,
updateShiftPlan, updateShiftPlan,
deleteShiftPlan, deleteShiftPlan,
getTemplates, //getTemplates,
//createFromTemplate, //createFromTemplate,
createFromPreset createFromPreset
} from '../controllers/shiftPlanController.js'; } from '../controllers/shiftPlanController.js';
@@ -22,7 +22,7 @@ router.use(authMiddleware);
router.get('/', getShiftPlans); router.get('/', getShiftPlans);
// GET templates only // GET templates only
router.get('/templates', getTemplates); //router.get('/templates', getTemplates);
// GET specific shift plan or template // GET specific shift plan or template
router.get('/:id', getShiftPlan); router.get('/:id', getShiftPlan);

View File

@@ -3,7 +3,6 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { db } from '../services/databaseService.js'; import { db } from '../services/databaseService.js';
import { setupDefaultTemplate } from './setupDefaultTemplate.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);

View File

@@ -1,127 +0,0 @@
// backend/src/scripts/setupDefaultTemplate.ts
import { v4 as uuidv4 } from 'uuid';
import { db } from '../services/databaseService.js';
import { DEFAULT_ZEBRA_TIME_SLOTS } from '../models/defaults/shiftPlanDefaults.js';
interface AdminUser {
id: string;
}
/**
* Sets up the default shift template if it doesn't exist
* @returns {Promise<void>}
*/
export async function setupDefaultTemplate(): Promise<void> {
try {
// Prüfen ob bereits eine Standard-Vorlage existiert - KORREKTUR: shift_plans verwenden
const existingDefault = await db.get(
'SELECT * FROM shift_plans WHERE is_template = 1 AND name = ?',
['Standardwoche']
);
if (existingDefault) {
console.log('Standard-Vorlage existiert bereits');
return;
}
// Admin-Benutzer für die Standard-Vorlage finden - KORREKTUR: employees verwenden
const adminUser = await db.get<AdminUser>(
'SELECT id FROM employees WHERE role = ?',
['admin']
);
if (!adminUser) {
console.log('Kein Admin-Benutzer gefunden. Standard-Vorlage kann nicht erstellt werden.');
return;
}
const templateId = uuidv4();
console.log('🔄 Erstelle Standard-Vorlage mit ID:', templateId);
// Transaktion starten
await db.run('BEGIN TRANSACTION');
try {
// Standard-Vorlage erstellen - KORREKTUR: shift_plans verwenden
await db.run(
`INSERT INTO shift_plans (id, name, description, is_template, status, created_by)
VALUES (?, ?, ?, ?, ?, ?)`,
[
templateId,
'Standardwoche',
'Mo-Do: Vormittags- und Nachmittagsschicht, Fr: nur Vormittagsschicht',
1, // is_template = true
'template', // status = 'template'
adminUser.id
]
);
console.log('Standard-Vorlage erstellt:', templateId);
// Zeit-Slots erstellen - KORREKTUR: time_slots verwenden
const timeSlots = DEFAULT_ZEBRA_TIME_SLOTS.map(slot => ({
...slot,
id: uuidv4()
}));
for (const slot of timeSlots) {
await db.run(
`INSERT INTO time_slots (id, plan_id, name, start_time, end_time, description)
VALUES (?, ?, ?, ?, ?, ?)`,
[slot.id, templateId, slot.name, slot.startTime, slot.endTime, slot.description]
);
}
console.log('✅ Zeit-Slots erstellt');
// Schichten für Mo-Do - KORREKTUR: shifts verwenden
for (let day = 1; day <= 4; day++) {
// Vormittagsschicht
await db.run(
`INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, timeSlots[0].id, 2, '#3498db']
);
// Nachmittagsschicht
await db.run(
`INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, day, timeSlots[1].id, 2, '#e74c3c']
);
}
// Freitag nur Vormittagsschicht
await db.run(
`INSERT INTO shifts (id, plan_id, day_of_week, time_slot_id, required_employees, color)
VALUES (?, ?, ?, ?, ?, ?)`,
[uuidv4(), templateId, 5, timeSlots[0].id, 2, '#3498db']
);
console.log('✅ Schichten erstellt');
// In der problematischen Stelle: KORREKTUR: shift_plans verwenden
const createdTemplate = await db.get(
'SELECT * FROM shift_plans WHERE id = ?',
[templateId]
) as { name: string } | undefined;
console.log('📋 Erstellte Vorlage:', createdTemplate?.name);
const shiftCount = await db.get(
'SELECT COUNT(*) as count FROM shifts WHERE plan_id = ?',
[templateId]
) as { count: number } | undefined;
console.log(`📊 Anzahl Schichten: ${shiftCount?.count}`);
await db.run('COMMIT');
console.log('🎉 Standard-Vorlage erfolgreich initialisiert');
} catch (error) {
await db.run('ROLLBACK');
console.error('❌ Fehler beim Erstellen der Vorlage:', error);
throw error;
}
} catch (error) {
console.error('❌ Fehler in setupDefaultTemplate:', error);
}
}

View File

@@ -1,7 +1,6 @@
// backend/src/server.ts // backend/src/server.ts
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
import { setupDefaultTemplate } from './scripts/setupDefaultTemplate.js';
import { initializeDatabase } from './scripts/initializeDatabase.js'; import { initializeDatabase } from './scripts/initializeDatabase.js';
// Route imports // Route imports
@@ -63,10 +62,6 @@ const initializeApp = async () => {
await applyMigration(); await applyMigration();
//console.log('✅ Database migrations applied'); //console.log('✅ Database migrations applied');
// Setup default template
await setupDefaultTemplate();
//console.log('✅ Default template checked/created');
// Start server only after successful initialization // Start server only after successful initialization
app.listen(PORT, () => { app.listen(PORT, () => {
console.log('🎉 BACKEND STARTED SUCCESSFULLY!'); console.log('🎉 BACKEND STARTED SUCCESSFULLY!');

View File

@@ -111,7 +111,7 @@ const AppContent: React.FC = () => {
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/settings" element={ <Route path="/settings" element={
<ProtectedRoute roles={['admin']}> <ProtectedRoute>
<Settings /> <Settings />
</ProtectedRoute> </ProtectedRoute>
} /> } />

View File

@@ -20,7 +20,7 @@ const Navigation: React.FC = () => {
{ path: '/shift-plans', label: '📅 Schichtpläne', roles: ['admin', 'instandhalter', 'user'] }, { path: '/shift-plans', label: '📅 Schichtpläne', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/employees', label: '👥 Mitarbeiter', roles: ['admin', 'instandhalter'] }, { path: '/employees', label: '👥 Mitarbeiter', roles: ['admin', 'instandhalter'] },
{ path: '/help', label: '❓ Hilfe & Support', roles: ['admin', 'instandhalter', 'user'] }, { path: '/help', label: '❓ Hilfe & Support', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/settings', label: '⚙️ Einstellungen', roles: ['admin'] }, { path: '/settings', label: '⚙️ Einstellungen', roles: ['admin', 'instandhalter', 'user'] },
]; ];
const filteredNavigation = navigationItems.filter(item => const filteredNavigation = navigationItems.filter(item =>

View File

@@ -31,10 +31,8 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
}) => { }) => {
const [availabilities, setAvailabilities] = useState<Availability[]>([]); const [availabilities, setAvailabilities] = useState<Availability[]>([]);
const [shiftPlans, setShiftPlans] = useState<ShiftPlan[]>([]); const [shiftPlans, setShiftPlans] = useState<ShiftPlan[]>([]);
const [usedDays, setUsedDays] = useState<{id: number, name: string}[]>([]);
const [selectedPlanId, setSelectedPlanId] = useState<string>(''); const [selectedPlanId, setSelectedPlanId] = useState<string>('');
const [selectedPlan, setSelectedPlan] = useState<ShiftPlan | null>(null); const [selectedPlan, setSelectedPlan] = useState<ShiftPlan | null>(null);
const [timeSlots, setTimeSlots] = useState<ExtendedTimeSlot[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -62,118 +60,54 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
useEffect(() => { useEffect(() => {
if (selectedPlanId) { if (selectedPlanId) {
loadSelectedPlan(); loadSelectedPlan();
} else {
setTimeSlots([]);
} }
}, [selectedPlanId]); }, [selectedPlanId]);
const getUsedDaysFromPlan = (plan: ShiftPlan | null) => { const formatTime = (time: string): string => {
if (!plan || !plan.shifts) return []; if (!time) return '--:--';
return time.substring(0, 5);
const usedDays = new Set<number>();
plan.shifts.forEach(shift => {
usedDays.add(shift.dayOfWeek);
});
const daysArray = Array.from(usedDays).sort();
console.log('📅 VERWENDETE TAGE IM PLAN:', daysArray);
return daysArray.map(dayId => {
return daysOfWeek.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
});
}; };
const getUsedTimeSlotsFromPlan = (plan: ShiftPlan | null): ExtendedTimeSlot[] => { // Create a data structure that maps days to their actual time slots
if (!plan || !plan.shifts || !plan.timeSlots) return []; const getTimetableData = () => {
if (!selectedPlan || !selectedPlan.shifts || !selectedPlan.timeSlots) {
return { days: [], timeSlotsByDay: {} };
}
const usedTimeSlotIds = new Set<string>(); // Group shifts by day
plan.shifts.forEach(shift => { const shiftsByDay = selectedPlan.shifts.reduce((acc, shift) => {
usedTimeSlotIds.add(shift.timeSlotId); if (!acc[shift.dayOfWeek]) {
acc[shift.dayOfWeek] = [];
}
acc[shift.dayOfWeek].push(shift);
return acc;
}, {} as Record<number, typeof selectedPlan.shifts>);
// Get unique days that have shifts
const days = Array.from(new Set(selectedPlan.shifts.map(shift => shift.dayOfWeek)))
.sort()
.map(dayId => {
return daysOfWeek.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
}); });
const usedTimeSlots = plan.timeSlots // For each day, get the time slots that actually have shifts
.filter(timeSlot => usedTimeSlotIds.has(timeSlot.id)) const timeSlotsByDay: Record<number, ExtendedTimeSlot[]> = {};
days.forEach(day => {
const shiftsForDay = shiftsByDay[day.id] || [];
const timeSlotIdsForDay = new Set(shiftsForDay.map(shift => shift.timeSlotId));
timeSlotsByDay[day.id] = selectedPlan.timeSlots
.filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id))
.map(timeSlot => ({ .map(timeSlot => ({
...timeSlot, ...timeSlot,
displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})`, displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})`,
source: `Plan: ${plan.name}` source: `Plan: ${selectedPlan.name}`
})) }))
.sort((a, b) => a.startTime.localeCompare(b.startTime)); .sort((a, b) => a.startTime.localeCompare(b.startTime));
console.log('⏰ VERWENDETE ZEIT-SLOTS IM PLAN:', usedTimeSlots);
return usedTimeSlots;
};
// Load time slots from shift plans - CORRECTED VERSION
const extractTimeSlotsFromPlans = (plans: ShiftPlan[]): ExtendedTimeSlot[] => {
console.log('🔄 EXTRAHIERE ZEIT-SLOTS AUS SCHICHTPLÄNEN:', plans);
const allTimeSlots = new Map<string, ExtendedTimeSlot>();
plans.forEach(plan => {
console.log(`📋 ANALYSIERE PLAN: ${plan.name}`, {
id: plan.id,
timeSlots: plan.timeSlots,
shifts: plan.shifts
}); });
// Use timeSlots from plan if available return { days, timeSlotsByDay };
if (plan.timeSlots && Array.isArray(plan.timeSlots) && plan.timeSlots.length > 0) {
plan.timeSlots.forEach(timeSlot => {
console.log(` 🔍 ZEIT-SLOT:`, timeSlot);
const key = timeSlot.id; // Use ID as key to avoid duplicates
if (!allTimeSlots.has(key)) {
allTimeSlots.set(key, {
...timeSlot,
displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})`,
source: `Plan: ${plan.name}`
});
}
});
} else {
console.warn(`⚠️ PLAN ${plan.name} HAT KEINE TIME_SLOTS:`, plan.timeSlots);
}
// Alternative: Extract from shifts if timeSlots array exists but is empty
if (plan.shifts && Array.isArray(plan.shifts) && plan.shifts.length > 0) {
console.log(`🔍 VERSUCHE TIME_SLOTS AUS SHIFTS ZU EXTRAHIEREN:`, plan.shifts.length);
// Create a set of unique timeSlotIds from shifts
const uniqueTimeSlotIds = new Set(plan.shifts.map(shift => shift.timeSlotId));
uniqueTimeSlotIds.forEach(timeSlotId => {
// Try to find time slot in plan's timeSlots first
const existingTimeSlot = plan.timeSlots?.find(ts => ts.id === timeSlotId);
if (existingTimeSlot) {
const key = existingTimeSlot.id;
if (!allTimeSlots.has(key)) {
allTimeSlots.set(key, {
...existingTimeSlot,
displayName: `${existingTimeSlot.name} (${formatTime(existingTimeSlot.startTime)}-${formatTime(existingTimeSlot.endTime)})`,
source: `Plan: ${plan.name} (from shift)`
});
}
} else {
// If time slot not found in plan.timeSlots, create a basic one from the ID
console.warn(`⚠️ TIME_SLOT MIT ID ${timeSlotId} NICHT IN PLAN.TIME_SLOTS GEFUNDEN`);
}
});
}
});
const result = Array.from(allTimeSlots.values()).sort((a, b) =>
a.startTime.localeCompare(b.startTime)
);
console.log('✅ ZEIT-SLOTS AUS PLÄNEN GEFUNDEN:', result.length, result);
return result;
};
const formatTime = (time: string): string => {
if (!time) return '--:--';
return time.substring(0, 5); // Ensure HH:MM format
}; };
const loadData = async () => { const loadData = async () => {
@@ -207,16 +141,13 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
const planWithShifts = plans.find(plan => const planWithShifts = plans.find(plan =>
plan.shifts && plan.shifts.length > 0 && plan.shifts && plan.shifts.length > 0 &&
plan.timeSlots && plan.timeSlots.length > 0 plan.timeSlots && plan.timeSlots.length > 0
) || plans[0]; // Fallback to first plan ) || plans[0];
setSelectedPlanId(planWithShifts.id); setSelectedPlanId(planWithShifts.id);
console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name); console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name);
// Load the selected plan to get its actual used time slots and days // Load the selected plan to get its actual used time slots and days
await loadSelectedPlan(); await loadSelectedPlan();
} else {
setTimeSlots([]);
setUsedDays([]);
} }
// 4. Set existing availabilities // 4. Set existing availabilities
@@ -242,19 +173,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort(), usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort(),
usedTimeSlots: Array.from(new Set(plan.shifts?.map(s => s.timeSlotId) || [])).length usedTimeSlots: Array.from(new Set(plan.shifts?.map(s => s.timeSlotId) || [])).length
}); });
// Only show time slots and days that are actually used in the plan
const usedTimeSlots = getUsedTimeSlotsFromPlan(plan);
const usedDays = getUsedDaysFromPlan(plan);
console.log('✅ VERWENDETE DATEN:', {
timeSlots: usedTimeSlots.length,
days: usedDays.length,
dayIds: usedDays.map(d => d.id)
});
setTimeSlots(usedTimeSlots);
setUsedDays(usedDays); // We'll add this state variable
} catch (err: any) { } catch (err: any) {
console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err); console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err);
setError('Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler')); setError('Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler'));
@@ -270,8 +188,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
avail.timeSlotId === timeSlotId avail.timeSlotId === timeSlotId
); );
console.log(`🔍 EXISTIERENDE VERFÜGBARKEIT GEFUNDEN AN INDEX:`, existingIndex);
if (existingIndex >= 0) { if (existingIndex >= 0) {
// Update existing availability // Update existing availability
const updated = [...prev]; const updated = [...prev];
@@ -280,7 +196,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
preferenceLevel: level, preferenceLevel: level,
isAvailable: level !== 3 isAvailable: level !== 3
}; };
console.log('✅ VERFÜGBARKEIT AKTUALISIERT:', updated[existingIndex]);
return updated; return updated;
} else { } else {
// Create new availability // Create new availability
@@ -293,7 +208,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
preferenceLevel: level, preferenceLevel: level,
isAvailable: level !== 3 isAvailable: level !== 3
}; };
console.log('🆕 NEUE VERFÜGBARKEIT ERSTELLT:', newAvailability);
return [...prev, newAvailability]; return [...prev, newAvailability];
} }
}); });
@@ -306,11 +220,186 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
); );
const result = availability?.preferenceLevel || 3; const result = availability?.preferenceLevel || 3;
console.log(`🔍 ABFRAGE VERFÜGBARKEIT: Tag ${dayId}, Slot ${timeSlotId} = Level ${result}`);
return result; return result;
}; };
// Update the timetable rendering to use the new data structure
const renderTimetable = () => {
const { days, timeSlotsByDay } = getTimetableData();
if (days.length === 0 || Object.keys(timeSlotsByDay).length === 0) {
return (
<div style={{
padding: '40px',
textAlign: 'center',
backgroundColor: '#f8f9fa',
color: '#6c757d',
borderRadius: '8px',
border: '1px solid #e9ecef'
}}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}>📅</div>
<h4>Keine Shifts im ausgewählten Plan</h4>
<p>Der ausgewählte Schichtplan hat keine Shifts definiert.</p>
</div>
);
}
// Get all unique time slots across all days for row headers
const allTimeSlotIds = new Set<string>();
days.forEach(day => {
timeSlotsByDay[day.id]?.forEach(timeSlot => {
allTimeSlotIds.add(timeSlot.id);
});
});
const allTimeSlots = Array.from(allTimeSlotIds)
.map(id => selectedPlan?.timeSlots?.find(ts => ts.id === id))
.filter(Boolean)
.map(timeSlot => ({
...timeSlot!,
displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`,
source: `Plan: ${selectedPlan!.name}`
}))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
return (
<div style={{
marginBottom: '30px',
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden'
}}>
<div style={{
backgroundColor: '#2c3e50',
color: 'white',
padding: '15px 20px',
fontWeight: 'bold'
}}>
Verfügbarkeit definieren
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
{allTimeSlots.length} Schichttypen {days.length} Tage Nur tatsächlich im Plan verwendete Schichten
</div>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{
width: '100%',
borderCollapse: 'collapse',
backgroundColor: 'white'
}}>
<thead>
<tr style={{ backgroundColor: '#f8f9fa' }}>
<th style={{
padding: '12px 16px',
textAlign: 'left',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '100px'
}}>
Schicht (Zeit)
</th>
{days.map(weekday => (
<th key={weekday.id} style={{
padding: '12px 16px',
textAlign: 'center',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '90px'
}}>
{weekday.name}
</th>
))}
</tr>
</thead>
<tbody>
{allTimeSlots.map((timeSlot, timeIndex) => (
<tr key={timeSlot.id} style={{
backgroundColor: timeIndex % 2 === 0 ? 'white' : '#f8f9fa'
}}>
<td style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
fontWeight: '500',
backgroundColor: '#f8f9fa'
}}>
{timeSlot.displayName}
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
{timeSlot.source}
</div>
</td>
{days.map(weekday => {
// Check if this time slot exists for this day
const timeSlotForDay = timeSlotsByDay[weekday.id]?.find(ts => ts.id === timeSlot.id);
if (!timeSlotForDay) {
return (
<td key={weekday.id} style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
textAlign: 'center',
backgroundColor: '#f8f9fa',
color: '#ccc',
fontStyle: 'italic'
}}>
-
</td>
);
}
const currentLevel = getAvailabilityForDayAndSlot(weekday.id, timeSlot.id);
const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
return (
<td key={weekday.id} style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
textAlign: 'center',
backgroundColor: levelConfig?.bgColor
}}>
<select
value={currentLevel}
onChange={(e) => {
const newLevel = parseInt(e.target.value) as AvailabilityLevel;
handleAvailabilityLevelChange(weekday.id, timeSlot.id, newLevel);
}}
style={{
padding: '8px 12px',
border: `2px solid ${levelConfig?.color || '#ddd'}`,
borderRadius: '6px',
backgroundColor: levelConfig?.bgColor || 'white',
color: levelConfig?.color || '#333',
fontWeight: 'bold',
minWidth: '140px',
cursor: 'pointer',
textAlign: 'center'
}}
>
{availabilityLevels.map(level => (
<option
key={level.level}
value={level.level}
style={{
backgroundColor: level.bgColor,
color: level.color,
fontWeight: 'bold'
}}
>
{level.level}: {level.label}
</option>
))}
</select>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
const handleSave = async () => { const handleSave = async () => {
try { try {
setSaving(true); setSaving(true);
@@ -321,10 +410,13 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
return; return;
} }
// Filter availabilities to only include those with actual time slots const { days, timeSlotsByDay } = getTimetableData();
const validAvailabilities = availabilities.filter(avail =>
timeSlots.some(slot => slot.id === avail.timeSlotId) // Filter availabilities to only include those with actual shifts
); const validAvailabilities = availabilities.filter(avail => {
const timeSlotsForDay = timeSlotsByDay[avail.dayOfWeek] || [];
return timeSlotsForDay.some(slot => slot.id === avail.timeSlotId);
});
if (validAvailabilities.length === 0) { if (validAvailabilities.length === 0) {
setError('Keine gültigen Verfügbarkeiten zum Speichern gefunden'); setError('Keine gültigen Verfügbarkeiten zum Speichern gefunden');
@@ -363,9 +455,18 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
); );
} }
const { days, timeSlotsByDay } = getTimetableData();
const allTimeSlotIds = new Set<string>();
days.forEach(day => {
timeSlotsByDay[day.id]?.forEach(timeSlot => {
allTimeSlotIds.add(timeSlot.id);
});
});
const timeSlotsCount = allTimeSlotIds.size;
return ( return (
<div style={{ <div style={{
maxWidth: '1400px', maxWidth: '1900px',
margin: '0 auto', margin: '0 auto',
backgroundColor: 'white', backgroundColor: 'white',
padding: '30px', padding: '30px',
@@ -384,22 +485,22 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{/* Debug-Info */} {/* Debug-Info */}
<div style={{ <div style={{
backgroundColor: timeSlots.length === 0 ? '#f8d7da' : '#d1ecf1', backgroundColor: timeSlotsCount === 0 ? '#f8d7da' : '#d1ecf1',
border: `1px solid ${timeSlots.length === 0 ? '#f5c6cb' : '#bee5eb'}`, border: `1px solid ${timeSlotsCount === 0 ? '#f5c6cb' : '#bee5eb'}`,
borderRadius: '6px', borderRadius: '6px',
padding: '15px', padding: '15px',
marginBottom: '20px' marginBottom: '20px'
}}> }}>
<h4 style={{ <h4 style={{
margin: '0 0 10px 0', margin: '0 0 10px 0',
color: timeSlots.length === 0 ? '#721c24' : '#0c5460' color: timeSlotsCount === 0 ? '#721c24' : '#0c5460'
}}> }}>
{timeSlots.length === 0 ? '❌ PROBLEM: Keine Zeit-Slots gefunden' : '✅ Plan-Daten geladen'} {timeSlotsCount === 0 ? '❌ PROBLEM: Keine Zeit-Slots gefunden' : '✅ Plan-Daten geladen'}
</h4> </h4>
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}> <div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
<div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div> <div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div>
<div><strong>Verwendete Zeit-Slots:</strong> {timeSlots.length}</div> <div><strong>Verwendete Zeit-Slots:</strong> {timeSlotsCount}</div>
<div><strong>Verwendete Tage:</strong> {usedDays.length} ({usedDays.map(d => d.name).join(', ')})</div> <div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
<div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan?.shifts?.length || 0}</div> <div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan?.shifts?.length || 0}</div>
</div> </div>
@@ -415,18 +516,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
)} )}
</div> </div>
<div style={{
backgroundColor: '#2c3e50',
color: 'white',
padding: '15px 20px',
fontWeight: 'bold'
}}>
Verfügbarkeit für: {selectedPlan?.name || 'Kein Plan ausgewählt'}
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
{timeSlots.length} Schichttypen {usedDays.length} Tage Nur tatsächlich im Plan verwendete Schichten und Tage
</div>
</div>
{/* Employee Info */} {/* Employee Info */}
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px' }}>
<h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}> <h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}>
@@ -534,144 +623,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</div> </div>
{/* Availability Timetable */} {/* Availability Timetable */}
{timeSlots.length > 0 ? ( {renderTimetable()}
<div style={{
marginBottom: '30px',
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden'
}}>
<div style={{
backgroundColor: '#2c3e50',
color: 'white',
padding: '15px 20px',
fontWeight: 'bold'
}}>
Verfügbarkeit definieren
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
{timeSlots.length} Schichttypen verfügbar Wählen Sie für jeden Tag und jede Schicht die Verfügbarkeit
</div>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{
width: '100%',
borderCollapse: 'collapse',
backgroundColor: 'white'
}}>
<thead>
<tr style={{ backgroundColor: '#f8f9fa' }}>
<th style={{
padding: '12px 16px',
textAlign: 'left',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '200px'
}}>
Schicht (Zeit)
</th>
{usedDays.map(weekday => (
<th key={weekday.id} style={{
padding: '12px 16px',
textAlign: 'center',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '150px'
}}>
{weekday.name}
</th>
))}
</tr>
</thead>
<tbody>
{timeSlots.map((timeSlot, timeIndex) => (
<tr key={timeSlot.id} style={{
backgroundColor: timeIndex % 2 === 0 ? 'white' : '#f8f9fa'
}}>
<td style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
fontWeight: '500',
backgroundColor: '#f8f9fa'
}}>
{timeSlot.displayName}
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
{timeSlot.source}
</div>
</td>
{usedDays.map(weekday => {
const currentLevel = getAvailabilityForDayAndSlot(weekday.id, timeSlot.id);
const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
return (
<td key={weekday.id} style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
textAlign: 'center',
backgroundColor: levelConfig?.bgColor
}}>
<select
value={currentLevel}
onChange={(e) => {
const newLevel = parseInt(e.target.value) as AvailabilityLevel;
handleAvailabilityLevelChange(weekday.id, timeSlot.id, newLevel);
}}
style={{
padding: '8px 12px',
border: `2px solid ${levelConfig?.color || '#ddd'}`,
borderRadius: '6px',
backgroundColor: levelConfig?.bgColor || 'white',
color: levelConfig?.color || '#333',
fontWeight: 'bold',
minWidth: '140px',
cursor: 'pointer',
textAlign: 'center'
}}
>
{availabilityLevels.map(level => (
<option
key={level.level}
value={level.level}
style={{
backgroundColor: level.bgColor,
color: level.color,
fontWeight: 'bold'
}}
>
{level.level}: {level.label}
</option>
))}
</select>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div style={{
padding: '40px',
textAlign: 'center',
backgroundColor: '#f8f9fa',
color: '#6c757d',
borderRadius: '8px',
border: '1px solid #e9ecef'
}}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}></div>
<h4>Keine Schichttypen konfiguriert</h4>
<p>Es wurden keine Zeit-Slots in den Schichtplänen gefunden.</p>
<p style={{ fontSize: '14px', marginTop: '10px' }}>
Bitte erstellen Sie zuerst Schichtpläne mit Zeit-Slots oder wählen Sie einen anderen Schichtplan aus.
</p>
<div style={{ marginTop: '20px', fontSize: '12px', color: '#999' }}>
Gefundene Schichtpläne: {shiftPlans.length}<br />
Schichtpläne mit TimeSlots: {shiftPlans.filter(p => p.timeSlots && p.timeSlots.length > 0).length}
</div>
</div>
)}
{/* Buttons */} {/* Buttons */}
<div style={{ <div style={{
@@ -697,14 +649,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving || timeSlots.length === 0 || !selectedPlanId} disabled={saving || timeSlotsCount === 0 || !selectedPlanId}
style={{ style={{
padding: '12px 24px', padding: '12px 24px',
backgroundColor: saving ? '#bdc3c7' : (timeSlots.length === 0 || !selectedPlanId ? '#95a5a6' : '#3498db'), backgroundColor: saving ? '#bdc3c7' : (timeSlotsCount === 0 || !selectedPlanId ? '#95a5a6' : '#3498db'),
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '6px', borderRadius: '6px',
cursor: (saving || timeSlots.length === 0 || !selectedPlanId) ? 'not-allowed' : 'pointer', cursor: (saving || timeSlotsCount === 0 || !selectedPlanId) ? 'not-allowed' : 'pointer',
fontWeight: 'bold' fontWeight: 'bold'
}} }}
> >

View File

@@ -1,4 +1,4 @@
// frontend/src/pages/Employees/components/EmployeeForm.tsx - KORRIGIERT // frontend/src/pages/Employees/components/EmployeeForm.tsx
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../models/Employee'; import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../models/Employee';
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults'; import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
@@ -12,7 +12,6 @@ interface EmployeeFormProps {
onCancel: () => void; onCancel: () => void;
} }
const EmployeeForm: React.FC<EmployeeFormProps> = ({ const EmployeeForm: React.FC<EmployeeFormProps> = ({
mode, mode,
employee, employee,
@@ -25,9 +24,15 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
password: '', password: '',
role: 'user' as 'admin' | 'maintenance' | 'user', role: 'user' as 'admin' | 'maintenance' | 'user',
employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced', employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced',
contractType: 'small' as 'small' | 'large',
canWorkAlone: false, canWorkAlone: false,
isActive: true isActive: true
}); });
const [passwordForm, setPasswordForm] = useState({
newPassword: '',
confirmPassword: ''
});
const [showPasswordSection, setShowPasswordSection] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const { hasRole } = useAuth(); const { hasRole } = useAuth();
@@ -40,6 +45,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
password: '', // Passwort wird beim Bearbeiten nicht angezeigt password: '', // Passwort wird beim Bearbeiten nicht angezeigt
role: employee.role, role: employee.role,
employeeType: employee.employeeType, employeeType: employee.employeeType,
contractType: employee.contractType,
canWorkAlone: employee.canWorkAlone, canWorkAlone: employee.canWorkAlone,
isActive: employee.isActive isActive: employee.isActive
}); });
@@ -55,6 +61,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
})); }));
}; };
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setPasswordForm(prev => ({
...prev,
[name]: value
}));
};
const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => { const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => {
// Manager and experienced can work alone, trainee cannot // Manager and experienced can work alone, trainee cannot
const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced'; const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced';
@@ -66,6 +80,13 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
})); }));
}; };
const handleContractTypeChange = (contractType: 'small' | 'large') => {
setFormData(prev => ({
...prev,
contractType
}));
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setLoading(true); setLoading(true);
@@ -79,7 +100,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
password: formData.password, password: formData.password,
role: formData.role, role: formData.role,
employeeType: formData.employeeType, employeeType: formData.employeeType,
contractType: 'small', // Default value contractType: formData.contractType,
canWorkAlone: formData.canWorkAlone canWorkAlone: formData.canWorkAlone
}; };
await employeeService.createEmployee(createData); await employeeService.createEmployee(createData);
@@ -88,11 +109,27 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
name: formData.name.trim(), name: formData.name.trim(),
role: formData.role, role: formData.role,
employeeType: formData.employeeType, employeeType: formData.employeeType,
contractType: employee.contractType, // Keep the existing contract type contractType: formData.contractType,
canWorkAlone: formData.canWorkAlone, canWorkAlone: formData.canWorkAlone,
isActive: formData.isActive, isActive: formData.isActive,
}; };
await employeeService.updateEmployee(employee.id, updateData); await employeeService.updateEmployee(employee.id, updateData);
// If password change is requested and user is admin
if (showPasswordSection && passwordForm.newPassword && hasRole(['admin'])) {
if (passwordForm.newPassword.length < 6) {
throw new Error('Das neue Passwort muss mindestens 6 Zeichen lang sein');
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
throw new Error('Die Passwörter stimmen nicht überein');
}
// Use the password change endpoint
await employeeService.changePassword(employee.id, {
currentPassword: '', // Empty for admin reset - backend should handle this
newPassword: passwordForm.newPassword
});
}
} }
onSuccess(); onSuccess();
@@ -111,6 +148,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
? ROLE_CONFIG ? ROLE_CONFIG
: ROLE_CONFIG.filter(role => role.value !== 'admin'); : ROLE_CONFIG.filter(role => role.value !== 'admin');
const contractTypeOptions = [
{ value: 'small' as const, label: 'Kleiner Vertrag', description: '1 Schicht pro Woche' },
{ value: 'large' as const, label: 'Großer Vertrag', description: '2 Schichten pro Woche' }
];
return ( return (
<div style={{ <div style={{
maxWidth: '700px', maxWidth: '700px',
@@ -187,12 +229,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
value={formData.email} value={formData.email}
onChange={handleChange} onChange={handleChange}
required required
disabled={mode === 'edit'} // Email cannot be changed in edit mode
style={{ style={{
width: '100%', width: '100%',
padding: '10px', padding: '10px',
border: '1px solid #ddd', border: '1px solid #ddd',
borderRadius: '4px', borderRadius: '4px',
fontSize: '16px' fontSize: '16px',
backgroundColor: mode === 'edit' ? '#f8f9fa' : 'white'
}} }}
placeholder="max.mustermann@example.com" placeholder="max.mustermann@example.com"
/> />
@@ -227,6 +271,78 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
)} )}
</div> </div>
{/* Vertragstyp (nur für Admins) */}
{hasRole(['admin']) && (
<div style={{
padding: '20px',
backgroundColor: '#e8f4fd',
borderRadius: '8px',
border: '1px solid #b6d7e8'
}}>
<h3 style={{ margin: '0 0 15px 0', color: '#0c5460' }}>📝 Vertragstyp</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{contractTypeOptions.map(contract => (
<div
key={contract.value}
style={{
display: 'flex',
alignItems: 'flex-start',
padding: '15px',
border: `2px solid ${formData.contractType === contract.value ? '#3498db' : '#e0e0e0'}`,
borderRadius: '8px',
backgroundColor: formData.contractType === contract.value ? '#f0f8ff' : 'white',
cursor: 'pointer',
transition: 'all 0.2s'
}}
onClick={() => handleContractTypeChange(contract.value)}
>
<input
type="radio"
name="contractType"
value={contract.value}
checked={formData.contractType === contract.value}
onChange={() => handleContractTypeChange(contract.value)}
style={{
marginRight: '12px',
marginTop: '2px',
width: '18px',
height: '18px'
}}
/>
<div style={{ flex: 1 }}>
<div style={{
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: '4px',
fontSize: '16px'
}}>
{contract.label}
</div>
<div style={{
fontSize: '14px',
color: '#7f8c8d',
lineHeight: '1.4'
}}>
{contract.description}
</div>
</div>
<div style={{
padding: '6px 12px',
backgroundColor: formData.contractType === contract.value ? '#3498db' : '#95a5a6',
color: 'white',
borderRadius: '15px',
fontSize: '12px',
fontWeight: 'bold'
}}>
{contract.value.toUpperCase()}
</div>
</div>
))}
</div>
</div>
)}
{/* Mitarbeiter Kategorie */} {/* Mitarbeiter Kategorie */}
<div style={{ <div style={{
padding: '20px', padding: '20px',
@@ -359,6 +475,104 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div> </div>
</div> </div>
{/* Passwort ändern (nur für Admins im Edit-Modus) */}
{mode === 'edit' && hasRole(['admin']) && (
<div style={{
padding: '20px',
backgroundColor: '#fff3cd',
borderRadius: '8px',
border: '1px solid #ffeaa7'
}}>
<h3 style={{ margin: '0 0 15px 0', color: '#856404' }}>🔒 Passwort zurücksetzen</h3>
{!showPasswordSection ? (
<button
type="button"
onClick={() => setShowPasswordSection(true)}
style={{
padding: '10px 16px',
backgroundColor: '#f39c12',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
🔑 Passwort zurücksetzen
</button>
) : (
<div style={{ display: 'grid', gap: '15px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Neues Passwort *
</label>
<input
type="password"
name="newPassword"
value={passwordForm.newPassword}
onChange={handlePasswordChange}
required
minLength={6}
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Mindestens 6 Zeichen"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Passwort bestätigen *
</label>
<input
type="password"
name="confirmPassword"
value={passwordForm.confirmPassword}
onChange={handlePasswordChange}
required
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Passwort wiederholen"
/>
</div>
<div style={{ fontSize: '12px', color: '#7f8c8d' }}>
<strong>Hinweis:</strong> Als Administrator können Sie das Passwort des Benutzers ohne Kenntnis des aktuellen Passworts zurücksetzen.
</div>
<button
type="button"
onClick={() => {
setShowPasswordSection(false);
setPasswordForm({ newPassword: '', confirmPassword: '' });
}}
style={{
padding: '8px 16px',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
alignSelf: 'flex-start'
}}
>
Abbrechen
</button>
</div>
)}
</div>
)}
{/* Systemrolle (nur für Admins) */} {/* Systemrolle (nur für Admins) */}
{hasRole(['admin']) && ( {hasRole(['admin']) && (
<div style={{ <div style={{
@@ -383,7 +597,6 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
cursor: 'pointer' cursor: 'pointer'
}} }}
onClick={() => { onClick={() => {
// Use a direct setter instead of the function form
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
role: role.value as 'admin' | 'maintenance' | 'user' role: role.value as 'admin' | 'maintenance' | 'user'
@@ -479,7 +692,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
<button <button
type="submit" type="submit"
disabled={loading || !isFormValid} disabled={loading || !isFormValid || (showPasswordSection && (!passwordForm.newPassword || !passwordForm.confirmPassword))}
style={{ style={{
padding: '12px 24px', padding: '12px 24px',
backgroundColor: loading ? '#bdc3c7' : (isFormValid ? '#27ae60' : '#95a5a6'), backgroundColor: loading ? '#bdc3c7' : (isFormValid ? '#27ae60' : '#95a5a6'),

View File

@@ -246,7 +246,7 @@ const Settings: React.FC = () => {
value={currentUser.email} value={currentUser.email}
disabled disabled
style={{ style={{
width: '100%', width: '95%',
padding: '10px', padding: '10px',
border: '1px solid #ddd', border: '1px solid #ddd',
borderRadius: '4px', borderRadius: '4px',
@@ -264,7 +264,7 @@ const Settings: React.FC = () => {
value={currentUser.role} value={currentUser.role}
disabled disabled
style={{ style={{
width: '100%', width: '95%',
padding: '10px', padding: '10px',
border: '1px solid #ddd', border: '1px solid #ddd',
borderRadius: '4px', borderRadius: '4px',
@@ -284,7 +284,7 @@ const Settings: React.FC = () => {
value={currentUser.employeeType} value={currentUser.employeeType}
disabled disabled
style={{ style={{
width: '100%', width: '95%',
padding: '10px', padding: '10px',
border: '1px solid #ddd', border: '1px solid #ddd',
borderRadius: '4px', borderRadius: '4px',
@@ -302,7 +302,7 @@ const Settings: React.FC = () => {
value={currentUser.contractType} value={currentUser.contractType}
disabled disabled
style={{ style={{
width: '100%', width: '95%',
padding: '10px', padding: '10px',
border: '1px solid #ddd', border: '1px solid #ddd',
borderRadius: '4px', borderRadius: '4px',
@@ -326,7 +326,7 @@ const Settings: React.FC = () => {
onChange={handleProfileChange} onChange={handleProfileChange}
required required
style={{ style={{
width: '100%', width: '97%',
padding: '10px', padding: '10px',
border: '1px solid #ddd', border: '1px solid #ddd',
borderRadius: '4px', borderRadius: '4px',

View File

@@ -122,12 +122,12 @@ export const shiftPlanService = {
} }
}, },
getTemplates: async (): Promise<ShiftPlan[]> => { /*getTemplates: async (): Promise<ShiftPlan[]> => {
const response = await fetch(`${API_BASE}/templates`, { const response = await fetch(`${API_BASE}/templates`, {
headers: getAuthHeaders() headers: getAuthHeaders()
}); });
return handleResponse(response); return handleResponse(response);
}, },*/
// Get specific template or plan // Get specific template or plan
getTemplate: async (id: string): Promise<ShiftPlan> => { getTemplate: async (id: string): Promise<ShiftPlan> => {