mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
settings works for every user
This commit is contained in:
@@ -372,6 +372,15 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise<v
|
||||
const { id } = req.params;
|
||||
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
|
||||
const employee = await db.get<{ password: string }>('SELECT password FROM employees WHERE id = ?', [id]);
|
||||
if (!employee) {
|
||||
@@ -379,10 +388,18 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise<v
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isValidPassword = await bcrypt.compare(currentPassword, employee.password);
|
||||
if (!isValidPassword) {
|
||||
res.status(400).json({ error: 'Current password is incorrect' });
|
||||
// For non-admin users, verify current password
|
||||
if (currentUser?.role !== 'admin') {
|
||||
const isValidPassword = await bcrypt.compare(currentPassword, employee.password);
|
||||
if (!isValidPassword) {
|
||||
res.status(400).json({ error: 'Current password is incorrect' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate new password
|
||||
if (!newPassword || newPassword.length < 6) {
|
||||
res.status(400).json({ error: 'New password must be at least 6 characters long' });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
console.log('🔍 Lade Vorlagen...');
|
||||
|
||||
@@ -707,7 +707,7 @@ export const getTemplates = async (req: Request, res: Response): Promise<void> =
|
||||
console.error('Error fetching templates:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
};*/
|
||||
|
||||
// Neue Funktion: Create from Template
|
||||
/*export const createFromTemplate = async (req: Request, res: Response): Promise<void> => {
|
||||
|
||||
@@ -23,10 +23,10 @@ router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
|
||||
router.post('/', requireRole(['admin']), createEmployee);
|
||||
router.put('/:id', requireRole(['admin']), updateEmployee);
|
||||
router.delete('/:id', requireRole(['admin']), deleteEmployee);
|
||||
router.put('/:id/password', requireRole(['admin']), changePassword);
|
||||
router.put('/:id/password', authMiddleware, changePassword);
|
||||
|
||||
// Availability Routes
|
||||
router.get('/:employeeId/availabilities', requireRole(['admin', 'instandhalter']), getAvailabilities);
|
||||
router.put('/:employeeId/availabilities', requireRole(['admin', 'instandhalter']), updateAvailabilities);
|
||||
router.get('/:employeeId/availabilities', authMiddleware, getAvailabilities);
|
||||
router.put('/:employeeId/availabilities', authMiddleware, updateAvailabilities);
|
||||
|
||||
export default router;
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
createShiftPlan,
|
||||
updateShiftPlan,
|
||||
deleteShiftPlan,
|
||||
getTemplates,
|
||||
//getTemplates,
|
||||
//createFromTemplate,
|
||||
createFromPreset
|
||||
} from '../controllers/shiftPlanController.js';
|
||||
@@ -22,7 +22,7 @@ router.use(authMiddleware);
|
||||
router.get('/', getShiftPlans);
|
||||
|
||||
// GET templates only
|
||||
router.get('/templates', getTemplates);
|
||||
//router.get('/templates', getTemplates);
|
||||
|
||||
// GET specific shift plan or template
|
||||
router.get('/:id', getShiftPlan);
|
||||
|
||||
@@ -3,7 +3,6 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { db } from '../services/databaseService.js';
|
||||
import { setupDefaultTemplate } from './setupDefaultTemplate.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// backend/src/server.ts
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { setupDefaultTemplate } from './scripts/setupDefaultTemplate.js';
|
||||
import { initializeDatabase } from './scripts/initializeDatabase.js';
|
||||
|
||||
// Route imports
|
||||
@@ -62,10 +61,6 @@ const initializeApp = async () => {
|
||||
const { applyMigration } = await import('./scripts/applyMigration.js');
|
||||
await applyMigration();
|
||||
//console.log('✅ Database migrations applied');
|
||||
|
||||
// Setup default template
|
||||
await setupDefaultTemplate();
|
||||
//console.log('✅ Default template checked/created');
|
||||
|
||||
// Start server only after successful initialization
|
||||
app.listen(PORT, () => {
|
||||
|
||||
@@ -111,7 +111,7 @@ const AppContent: React.FC = () => {
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/settings" element={
|
||||
<ProtectedRoute roles={['admin']}>
|
||||
<ProtectedRoute>
|
||||
<Settings />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
@@ -20,7 +20,7 @@ const Navigation: React.FC = () => {
|
||||
{ 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'] },
|
||||
{ path: '/settings', label: '⚙️ Einstellungen', roles: ['admin', 'instandhalter', 'user'] },
|
||||
];
|
||||
|
||||
const filteredNavigation = navigationItems.filter(item =>
|
||||
|
||||
@@ -31,10 +31,8 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
}) => {
|
||||
const [availabilities, setAvailabilities] = useState<Availability[]>([]);
|
||||
const [shiftPlans, setShiftPlans] = useState<ShiftPlan[]>([]);
|
||||
const [usedDays, setUsedDays] = useState<{id: number, name: string}[]>([]);
|
||||
const [selectedPlanId, setSelectedPlanId] = useState<string>('');
|
||||
const [selectedPlan, setSelectedPlan] = useState<ShiftPlan | null>(null);
|
||||
const [timeSlots, setTimeSlots] = useState<ExtendedTimeSlot[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -62,118 +60,54 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
useEffect(() => {
|
||||
if (selectedPlanId) {
|
||||
loadSelectedPlan();
|
||||
} else {
|
||||
setTimeSlots([]);
|
||||
}
|
||||
}, [selectedPlanId]);
|
||||
|
||||
const getUsedDaysFromPlan = (plan: ShiftPlan | null) => {
|
||||
if (!plan || !plan.shifts) return [];
|
||||
|
||||
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[] => {
|
||||
if (!plan || !plan.shifts || !plan.timeSlots) return [];
|
||||
|
||||
const usedTimeSlotIds = new Set<string>();
|
||||
plan.shifts.forEach(shift => {
|
||||
usedTimeSlotIds.add(shift.timeSlotId);
|
||||
});
|
||||
|
||||
const usedTimeSlots = plan.timeSlots
|
||||
.filter(timeSlot => usedTimeSlotIds.has(timeSlot.id))
|
||||
.map(timeSlot => ({
|
||||
...timeSlot,
|
||||
displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})`,
|
||||
source: `Plan: ${plan.name}`
|
||||
}))
|
||||
.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
|
||||
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
|
||||
return time.substring(0, 5);
|
||||
};
|
||||
|
||||
// Create a data structure that maps days to their actual time slots
|
||||
const getTimetableData = () => {
|
||||
if (!selectedPlan || !selectedPlan.shifts || !selectedPlan.timeSlots) {
|
||||
return { days: [], timeSlotsByDay: {} };
|
||||
}
|
||||
|
||||
// Group shifts by day
|
||||
const shiftsByDay = selectedPlan.shifts.reduce((acc, shift) => {
|
||||
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}` };
|
||||
});
|
||||
|
||||
// For each day, get the time slots that actually have shifts
|
||||
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 => ({
|
||||
...timeSlot,
|
||||
displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})`,
|
||||
source: `Plan: ${selectedPlan.name}`
|
||||
}))
|
||||
.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
||||
});
|
||||
|
||||
return { days, timeSlotsByDay };
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
@@ -207,16 +141,13 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
const planWithShifts = plans.find(plan =>
|
||||
plan.shifts && plan.shifts.length > 0 &&
|
||||
plan.timeSlots && plan.timeSlots.length > 0
|
||||
) || plans[0]; // Fallback to first plan
|
||||
) || plans[0];
|
||||
|
||||
setSelectedPlanId(planWithShifts.id);
|
||||
console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name);
|
||||
|
||||
// Load the selected plan to get its actual used time slots and days
|
||||
await loadSelectedPlan();
|
||||
} else {
|
||||
setTimeSlots([]);
|
||||
setUsedDays([]);
|
||||
}
|
||||
|
||||
// 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(),
|
||||
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) {
|
||||
console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err);
|
||||
setError('Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler'));
|
||||
@@ -270,8 +188,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
avail.timeSlotId === timeSlotId
|
||||
);
|
||||
|
||||
console.log(`🔍 EXISTIERENDE VERFÜGBARKEIT GEFUNDEN AN INDEX:`, existingIndex);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing availability
|
||||
const updated = [...prev];
|
||||
@@ -280,7 +196,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
preferenceLevel: level,
|
||||
isAvailable: level !== 3
|
||||
};
|
||||
console.log('✅ VERFÜGBARKEIT AKTUALISIERT:', updated[existingIndex]);
|
||||
return updated;
|
||||
} else {
|
||||
// Create new availability
|
||||
@@ -293,7 +208,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
preferenceLevel: level,
|
||||
isAvailable: level !== 3
|
||||
};
|
||||
console.log('🆕 NEUE VERFÜGBARKEIT ERSTELLT:', newAvailability);
|
||||
return [...prev, newAvailability];
|
||||
}
|
||||
});
|
||||
@@ -306,11 +220,186 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
);
|
||||
|
||||
const result = availability?.preferenceLevel || 3;
|
||||
console.log(`🔍 ABFRAGE VERFÜGBARKEIT: Tag ${dayId}, Slot ${timeSlotId} = Level ${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 () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
@@ -321,10 +410,13 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter availabilities to only include those with actual time slots
|
||||
const validAvailabilities = availabilities.filter(avail =>
|
||||
timeSlots.some(slot => slot.id === avail.timeSlotId)
|
||||
);
|
||||
const { days, timeSlotsByDay } = getTimetableData();
|
||||
|
||||
// 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) {
|
||||
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 (
|
||||
<div style={{
|
||||
maxWidth: '1400px',
|
||||
maxWidth: '1900px',
|
||||
margin: '0 auto',
|
||||
backgroundColor: 'white',
|
||||
padding: '30px',
|
||||
@@ -384,22 +485,22 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
|
||||
{/* Debug-Info */}
|
||||
<div style={{
|
||||
backgroundColor: timeSlots.length === 0 ? '#f8d7da' : '#d1ecf1',
|
||||
border: `1px solid ${timeSlots.length === 0 ? '#f5c6cb' : '#bee5eb'}`,
|
||||
backgroundColor: timeSlotsCount === 0 ? '#f8d7da' : '#d1ecf1',
|
||||
border: `1px solid ${timeSlotsCount === 0 ? '#f5c6cb' : '#bee5eb'}`,
|
||||
borderRadius: '6px',
|
||||
padding: '15px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<h4 style={{
|
||||
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>
|
||||
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
|
||||
<div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div>
|
||||
<div><strong>Verwendete Zeit-Slots:</strong> {timeSlots.length}</div>
|
||||
<div><strong>Verwendete Tage:</strong> {usedDays.length} ({usedDays.map(d => d.name).join(', ')})</div>
|
||||
<div><strong>Verwendete Zeit-Slots:</strong> {timeSlotsCount}</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>
|
||||
|
||||
@@ -415,18 +516,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
)}
|
||||
</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 */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}>
|
||||
@@ -534,144 +623,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Availability Timetable */}
|
||||
{timeSlots.length > 0 ? (
|
||||
<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>
|
||||
)}
|
||||
{renderTimetable()}
|
||||
|
||||
{/* Buttons */}
|
||||
<div style={{
|
||||
@@ -697,14 +649,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving || timeSlots.length === 0 || !selectedPlanId}
|
||||
disabled={saving || timeSlotsCount === 0 || !selectedPlanId}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: saving ? '#bdc3c7' : (timeSlots.length === 0 || !selectedPlanId ? '#95a5a6' : '#3498db'),
|
||||
backgroundColor: saving ? '#bdc3c7' : (timeSlotsCount === 0 || !selectedPlanId ? '#95a5a6' : '#3498db'),
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: (saving || timeSlots.length === 0 || !selectedPlanId) ? 'not-allowed' : 'pointer',
|
||||
cursor: (saving || timeSlotsCount === 0 || !selectedPlanId) ? 'not-allowed' : 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../models/Employee';
|
||||
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
|
||||
@@ -12,7 +12,6 @@ interface EmployeeFormProps {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
|
||||
const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
mode,
|
||||
employee,
|
||||
@@ -25,9 +24,15 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
password: '',
|
||||
role: 'user' as 'admin' | 'maintenance' | 'user',
|
||||
employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced',
|
||||
contractType: 'small' as 'small' | 'large',
|
||||
canWorkAlone: false,
|
||||
isActive: true
|
||||
});
|
||||
const [passwordForm, setPasswordForm] = useState({
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
const [showPasswordSection, setShowPasswordSection] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const { hasRole } = useAuth();
|
||||
@@ -40,6 +45,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
password: '', // Passwort wird beim Bearbeiten nicht angezeigt
|
||||
role: employee.role,
|
||||
employeeType: employee.employeeType,
|
||||
contractType: employee.contractType,
|
||||
canWorkAlone: employee.canWorkAlone,
|
||||
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') => {
|
||||
// Manager and experienced can work alone, trainee cannot
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
@@ -79,7 +100,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
password: formData.password,
|
||||
role: formData.role,
|
||||
employeeType: formData.employeeType,
|
||||
contractType: 'small', // Default value
|
||||
contractType: formData.contractType,
|
||||
canWorkAlone: formData.canWorkAlone
|
||||
};
|
||||
await employeeService.createEmployee(createData);
|
||||
@@ -88,11 +109,27 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
name: formData.name.trim(),
|
||||
role: formData.role,
|
||||
employeeType: formData.employeeType,
|
||||
contractType: employee.contractType, // Keep the existing contract type
|
||||
contractType: formData.contractType,
|
||||
canWorkAlone: formData.canWorkAlone,
|
||||
isActive: formData.isActive,
|
||||
};
|
||||
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();
|
||||
@@ -111,6 +148,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
? ROLE_CONFIG
|
||||
: 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 (
|
||||
<div style={{
|
||||
maxWidth: '700px',
|
||||
@@ -187,12 +229,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
disabled={mode === 'edit'} // Email cannot be changed in edit mode
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px'
|
||||
fontSize: '16px',
|
||||
backgroundColor: mode === 'edit' ? '#f8f9fa' : 'white'
|
||||
}}
|
||||
placeholder="max.mustermann@example.com"
|
||||
/>
|
||||
@@ -227,6 +271,78 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
)}
|
||||
</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 */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
@@ -359,6 +475,104 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
</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) */}
|
||||
{hasRole(['admin']) && (
|
||||
<div style={{
|
||||
@@ -383,7 +597,6 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
// Use a direct setter instead of the function form
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
role: role.value as 'admin' | 'maintenance' | 'user'
|
||||
@@ -479,7 +692,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !isFormValid}
|
||||
disabled={loading || !isFormValid || (showPasswordSection && (!passwordForm.newPassword || !passwordForm.confirmPassword))}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
backgroundColor: loading ? '#bdc3c7' : (isFormValid ? '#27ae60' : '#95a5a6'),
|
||||
|
||||
@@ -246,7 +246,7 @@ const Settings: React.FC = () => {
|
||||
value={currentUser.email}
|
||||
disabled
|
||||
style={{
|
||||
width: '100%',
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
@@ -264,7 +264,7 @@ const Settings: React.FC = () => {
|
||||
value={currentUser.role}
|
||||
disabled
|
||||
style={{
|
||||
width: '100%',
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
@@ -284,7 +284,7 @@ const Settings: React.FC = () => {
|
||||
value={currentUser.employeeType}
|
||||
disabled
|
||||
style={{
|
||||
width: '100%',
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
@@ -302,7 +302,7 @@ const Settings: React.FC = () => {
|
||||
value={currentUser.contractType}
|
||||
disabled
|
||||
style={{
|
||||
width: '100%',
|
||||
width: '95%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
@@ -326,7 +326,7 @@ const Settings: React.FC = () => {
|
||||
onChange={handleProfileChange}
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
width: '97%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
|
||||
@@ -122,12 +122,12 @@ export const shiftPlanService = {
|
||||
}
|
||||
},
|
||||
|
||||
getTemplates: async (): Promise<ShiftPlan[]> => {
|
||||
/*getTemplates: async (): Promise<ShiftPlan[]> => {
|
||||
const response = await fetch(`${API_BASE}/templates`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
return handleResponse(response);
|
||||
},
|
||||
},*/
|
||||
|
||||
// Get specific template or plan
|
||||
getTemplate: async (id: string): Promise<ShiftPlan> => {
|
||||
|
||||
Reference in New Issue
Block a user