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 { 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);
|
||||||
|
|||||||
@@ -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> => {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
// 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!');
|
||||||
|
|||||||
@@ -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>
|
||||||
} />
|
} />
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -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'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user