diff --git a/backend/src/controllers/employeeController.ts b/backend/src/controllers/employeeController.ts index b411e3b..e41587c 100644 --- a/backend/src/controllers/employeeController.ts +++ b/backend/src/controllers/employeeController.ts @@ -7,9 +7,20 @@ import { AuthRequest } from '../middleware/auth.js'; import { CreateEmployeeRequest } from '../models/Employee.js'; function generateEmail(firstname: string, lastname: string): string { - // Remove special characters and convert to lowercase - const cleanFirstname = firstname.toLowerCase().replace(/[^a-z0-9]/g, ''); - const cleanLastname = lastname.toLowerCase().replace(/[^a-z0-9]/g, ''); + // Convert German umlauts to their expanded forms + const convertUmlauts = (str: string): string => { + return str + .toLowerCase() + .replace(/ü/g, 'ue') + .replace(/ö/g, 'oe') + .replace(/ä/g, 'ae') + .replace(/ß/g, 'ss'); + }; + + // Remove any remaining special characters and convert to lowercase + const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, ''); + const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, ''); + return `${cleanFirstname}.${cleanLastname}@sp.de`; } diff --git a/backend/src/database/schema.sql b/backend/src/database/schema.sql index 8160ec3..835ad72 100644 --- a/backend/src/database/schema.sql +++ b/backend/src/database/schema.sql @@ -104,11 +104,14 @@ CREATE TABLE IF NOT EXISTS employee_availability ( UNIQUE(employee_id, plan_id, shift_id) ); --- Performance indexes -CREATE INDEX IF NOT EXISTS idx_employees_role_active ON employees(role, is_active); +-- Performance indexes (UPDATED - removed role index from employees) CREATE INDEX IF NOT EXISTS idx_employees_email_active ON employees(email, is_active); CREATE INDEX IF NOT EXISTS idx_employees_type_active ON employees(employee_type, is_active); +-- Index for employee_roles table (NEW) +CREATE INDEX IF NOT EXISTS idx_employee_roles_employee ON employee_roles(employee_id); +CREATE INDEX IF NOT EXISTS idx_employee_roles_role ON employee_roles(role); + CREATE INDEX IF NOT EXISTS idx_shift_plans_status_date ON shift_plans(status, start_date, end_date); CREATE INDEX IF NOT EXISTS idx_shift_plans_created_by ON shift_plans(created_by); CREATE INDEX IF NOT EXISTS idx_shift_plans_template ON shift_plans(is_template, status); diff --git a/backend/src/models/defaults/employeeDefaults.ts b/backend/src/models/defaults/employeeDefaults.ts index 9058f2e..7a61cae 100644 --- a/backend/src/models/defaults/employeeDefaults.ts +++ b/backend/src/models/defaults/employeeDefaults.ts @@ -14,7 +14,7 @@ export const EMPLOYEE_DEFAULTS = { export const MANAGER_DEFAULTS = { role: 'admin' as const, employeeType: 'manager' as const, - contractType: 'large' as const, // Not really used but required by DB + contractType: 'large' as const, canWorkAlone: true, isActive: true }; @@ -64,26 +64,25 @@ export const AVAILABILITY_PREFERENCES = { } as const; // Default availability for new employees (all shifts unavailable as level 3) -export function createDefaultAvailabilities(employeeId: string, planId: string, timeSlotIds: string[]): Omit[] { +// UPDATED: Now uses shiftId instead of timeSlotId + dayOfWeek +export function createDefaultAvailabilities(employeeId: string, planId: string, shiftIds: string[]): Omit[] { const availabilities: Omit[] = []; - // Monday to Friday (1-5) - for (let day = 1; day <= 5; day++) { - for (const timeSlotId of timeSlotIds) { - availabilities.push({ - employeeId, - planId, - dayOfWeek: day, - timeSlotId, - preferenceLevel: 3 // Default to "unavailable" - employees must explicitly set availability - }); - } + // Create one availability entry per shift + for (const shiftId of shiftIds) { + availabilities.push({ + employeeId, + planId, + shiftId, + preferenceLevel: 3 // Default to "unavailable" - employees must explicitly set availability + }); } return availabilities; } // Create complete manager availability for all days (default: only Mon-Tue available) +// NOTE: This function might need revision based on new schema requirements export function createManagerDefaultSchedule(managerId: string, planId: string, timeSlotIds: string[]): Omit[] { const assignments: Omit[] = []; diff --git a/backend/src/models/helpers/employeeHelpers.ts b/backend/src/models/helpers/employeeHelpers.ts index 9d1b1f7..d00ecab 100644 --- a/backend/src/models/helpers/employeeHelpers.ts +++ b/backend/src/models/helpers/employeeHelpers.ts @@ -1,25 +1,51 @@ // backend/src/models/helpers/employeeHelpers.ts import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js'; -// Simplified validation - use schema validation instead +// Email generation function (same as in controllers) +function generateEmail(firstname: string, lastname: string): string { + // Convert German umlauts to their expanded forms + const convertUmlauts = (str: string): string => { + return str + .toLowerCase() + .replace(/ü/g, 'ue') + .replace(/ö/g, 'oe') + .replace(/ä/g, 'ae') + .replace(/ß/g, 'ss'); + }; + + // Remove any remaining special characters and convert to lowercase + const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, ''); + const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, ''); + + return `${cleanFirstname}.${cleanLastname}@sp.de`; +} + +// UPDATED: Validation for new employee model export function validateEmployeeData(employee: CreateEmployeeRequest): string[] { const errors: string[] = []; - if (!employee.email?.includes('@')) { - errors.push('Valid email is required'); - } - + // Email is now auto-generated, so no email validation needed + if (employee.password?.length < 6) { errors.push('Password must be at least 6 characters long'); } - if (!employee.name?.trim() || employee.name.trim().length < 2) { - errors.push('Name is required and must be at least 2 characters long'); + if (!employee.firstname?.trim() || employee.firstname.trim().length < 2) { + errors.push('First name is required and must be at least 2 characters long'); + } + + if (!employee.lastname?.trim() || employee.lastname.trim().length < 2) { + errors.push('Last name is required and must be at least 2 characters long'); } return errors; } +// Generate email for employee (new helper function) +export function generateEmployeeEmail(firstname: string, lastname: string): string { + return generateEmail(firstname, lastname); +} + // Simplified business logic helpers export const isManager = (employee: Employee): boolean => employee.employeeType === 'manager'; @@ -38,3 +64,26 @@ export const canEmployeeWorkAlone = (employee: Employee): boolean => export const getEmployeeWorkHours = (employee: Employee): number => isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2); + +// New helper for full name display +export const getFullName = (employee: { firstname: string; lastname: string }): string => + `${employee.firstname} ${employee.lastname}`; + +// Helper for availability validation +export function validateAvailabilityData(availability: Omit): string[] { + const errors: string[] = []; + + if (!availability.planId) { + errors.push('Plan ID is required'); + } + + if (!availability.shiftId) { + errors.push('Shift ID is required'); + } + + if (![1, 2, 3].includes(availability.preferenceLevel)) { + errors.push('Preference level must be 1, 2, or 3'); + } + + return errors; +} \ No newline at end of file diff --git a/backend/src/models/helpers/shiftPlanHelpers.ts b/backend/src/models/helpers/shiftPlanHelpers.ts index 2b562c4..d28ac03 100644 --- a/backend/src/models/helpers/shiftPlanHelpers.ts +++ b/backend/src/models/helpers/shiftPlanHelpers.ts @@ -78,7 +78,8 @@ export function calculateTotalRequiredEmployees(plan: ShiftPlan): number { return plan.shifts.reduce((total, shift) => total + shift.requiredEmployees, 0); } -/*export function getScheduledShiftByDateAndTime( +// UPDATED: Get scheduled shift by date and time slot +export function getScheduledShiftByDateAndTime( plan: ShiftPlan, date: string, timeSlotId: string @@ -86,7 +87,7 @@ export function calculateTotalRequiredEmployees(plan: ShiftPlan): number { return plan.scheduledShifts?.find(shift => shift.date === date && shift.timeSlotId === timeSlotId ); -}*/ +} export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: string[] } { const errors: string[] = []; @@ -115,4 +116,14 @@ export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: canPublish: errors.length === 0, errors }; +} + +// NEW: Helper for shift generation +export function generateShiftId(): string { + return `shift_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +// NEW: Helper for time slot generation +export function generateTimeSlotId(): string { + return `timeslot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } \ No newline at end of file diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index ba56851..4ac3608 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -16,7 +16,7 @@ interface AuthContextType { refreshUser: () => void; needsSetup: boolean; checkSetupStatus: () => Promise; - updateUser: (userData: Employee) => void; // Add this line + updateUser: (userData: Employee) => void; } const AuthContext = createContext(undefined); diff --git a/frontend/src/models/defaults/employeeDefaults.ts b/frontend/src/models/defaults/employeeDefaults.ts index e961826..7a61cae 100644 --- a/frontend/src/models/defaults/employeeDefaults.ts +++ b/frontend/src/models/defaults/employeeDefaults.ts @@ -14,39 +14,39 @@ export const EMPLOYEE_DEFAULTS = { export const MANAGER_DEFAULTS = { role: 'admin' as const, employeeType: 'manager' as const, - contractType: 'large' as const, // Not really used but required by DB + contractType: 'large' as const, canWorkAlone: true, isActive: true }; -export const EMPLOYEE_TYPE_CONFIG = [ - { +export const EMPLOYEE_TYPE_CONFIG = { + manager: { value: 'manager' as const, label: 'Chef/Administrator', color: '#e74c3c', independent: true, description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung' }, - { + experienced: { value: 'experienced' as const, label: 'Erfahren', color: '#3498db', independent: true, description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen' }, - { + trainee: { value: 'trainee' as const, label: 'Neuling', color: '#27ae60', independent: false, description: 'Benötigt Einarbeitung und Unterstützung' } - ] as const; +} as const; export const ROLE_CONFIG = [ - { value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen', color: '#27ae60' }, - { value: 'maintenance', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten', color: '#3498db' }, - { value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen', color: '#e74c3c' } + { value: 'user' as const, label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen', color: '#27ae60' }, + { value: 'maintenance' as const, label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten', color: '#3498db' }, + { value: 'admin' as const, label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen', color: '#e74c3c' } ] as const; // Contract type descriptions @@ -64,26 +64,25 @@ export const AVAILABILITY_PREFERENCES = { } as const; // Default availability for new employees (all shifts unavailable as level 3) -export function createDefaultAvailabilities(employeeId: string, planId: string, timeSlotIds: string[]): Omit[] { +// UPDATED: Now uses shiftId instead of timeSlotId + dayOfWeek +export function createDefaultAvailabilities(employeeId: string, planId: string, shiftIds: string[]): Omit[] { const availabilities: Omit[] = []; - // Monday to Friday (1-5) - for (let day = 1; day <= 5; day++) { - for (const timeSlotId of timeSlotIds) { - availabilities.push({ - employeeId, - planId, - dayOfWeek: day, - timeSlotId, - preferenceLevel: 3 // Default to "unavailable" - employees must explicitly set availability - }); - } + // Create one availability entry per shift + for (const shiftId of shiftIds) { + availabilities.push({ + employeeId, + planId, + shiftId, + preferenceLevel: 3 // Default to "unavailable" - employees must explicitly set availability + }); } return availabilities; } // Create complete manager availability for all days (default: only Mon-Tue available) +// NOTE: This function might need revision based on new schema requirements export function createManagerDefaultSchedule(managerId: string, planId: string, timeSlotIds: string[]): Omit[] { const assignments: Omit[] = []; diff --git a/frontend/src/models/helpers/employeeHelpers.ts b/frontend/src/models/helpers/employeeHelpers.ts index 9d1b1f7..d00ecab 100644 --- a/frontend/src/models/helpers/employeeHelpers.ts +++ b/frontend/src/models/helpers/employeeHelpers.ts @@ -1,25 +1,51 @@ // backend/src/models/helpers/employeeHelpers.ts import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js'; -// Simplified validation - use schema validation instead +// Email generation function (same as in controllers) +function generateEmail(firstname: string, lastname: string): string { + // Convert German umlauts to their expanded forms + const convertUmlauts = (str: string): string => { + return str + .toLowerCase() + .replace(/ü/g, 'ue') + .replace(/ö/g, 'oe') + .replace(/ä/g, 'ae') + .replace(/ß/g, 'ss'); + }; + + // Remove any remaining special characters and convert to lowercase + const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, ''); + const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, ''); + + return `${cleanFirstname}.${cleanLastname}@sp.de`; +} + +// UPDATED: Validation for new employee model export function validateEmployeeData(employee: CreateEmployeeRequest): string[] { const errors: string[] = []; - if (!employee.email?.includes('@')) { - errors.push('Valid email is required'); - } - + // Email is now auto-generated, so no email validation needed + if (employee.password?.length < 6) { errors.push('Password must be at least 6 characters long'); } - if (!employee.name?.trim() || employee.name.trim().length < 2) { - errors.push('Name is required and must be at least 2 characters long'); + if (!employee.firstname?.trim() || employee.firstname.trim().length < 2) { + errors.push('First name is required and must be at least 2 characters long'); + } + + if (!employee.lastname?.trim() || employee.lastname.trim().length < 2) { + errors.push('Last name is required and must be at least 2 characters long'); } return errors; } +// Generate email for employee (new helper function) +export function generateEmployeeEmail(firstname: string, lastname: string): string { + return generateEmail(firstname, lastname); +} + // Simplified business logic helpers export const isManager = (employee: Employee): boolean => employee.employeeType === 'manager'; @@ -38,3 +64,26 @@ export const canEmployeeWorkAlone = (employee: Employee): boolean => export const getEmployeeWorkHours = (employee: Employee): number => isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2); + +// New helper for full name display +export const getFullName = (employee: { firstname: string; lastname: string }): string => + `${employee.firstname} ${employee.lastname}`; + +// Helper for availability validation +export function validateAvailabilityData(availability: Omit): string[] { + const errors: string[] = []; + + if (!availability.planId) { + errors.push('Plan ID is required'); + } + + if (!availability.shiftId) { + errors.push('Shift ID is required'); + } + + if (![1, 2, 3].includes(availability.preferenceLevel)) { + errors.push('Preference level must be 1, 2, or 3'); + } + + return errors; +} \ No newline at end of file diff --git a/frontend/src/models/helpers/shiftPlanHelpers.ts b/frontend/src/models/helpers/shiftPlanHelpers.ts index ce0eae8..d28ac03 100644 --- a/frontend/src/models/helpers/shiftPlanHelpers.ts +++ b/frontend/src/models/helpers/shiftPlanHelpers.ts @@ -1,5 +1,4 @@ // backend/src/models/helpers/shiftPlanHelpers.ts -import { shiftAssignmentService } from '../../services/shiftAssignmentService.js'; import { ShiftPlan, Shift, ScheduledShift, TimeSlot } from '../ShiftPlan.js'; // Validation helpers @@ -79,16 +78,16 @@ export function calculateTotalRequiredEmployees(plan: ShiftPlan): number { return plan.shifts.reduce((total, shift) => total + shift.requiredEmployees, 0); } -/*export async function getScheduledShiftByDateAndTime( +// UPDATED: Get scheduled shift by date and time slot +export function getScheduledShiftByDateAndTime( plan: ShiftPlan, date: string, timeSlotId: string -): Promise { - const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(plan.id); - return scheduledShifts.find( - shift => shift.date === date && shift.timeSlotId === timeSlotId +): ScheduledShift | undefined { + return plan.scheduledShifts?.find(shift => + shift.date === date && shift.timeSlotId === timeSlotId ); -}*/ +} export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: string[] } { const errors: string[] = []; @@ -117,4 +116,14 @@ export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: canPublish: errors.length === 0, errors }; +} + +// NEW: Helper for shift generation +export function generateShiftId(): string { + return `shift_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +// NEW: Helper for time slot generation +export function generateTimeSlotId(): string { + return `timeslot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } \ No newline at end of file diff --git a/frontend/src/pages/Settings/Settings.tsx b/frontend/src/pages/Settings/Settings.tsx index 81a0b02..7637240 100644 --- a/frontend/src/pages/Settings/Settings.tsx +++ b/frontend/src/pages/Settings/Settings.tsx @@ -14,9 +14,10 @@ const Settings: React.FC = () => { const [loading, setLoading] = useState(false); const [showAvailabilityManager, setShowAvailabilityManager] = useState(false); - // Profile form state + // Profile form state - updated for firstname/lastname const [profileForm, setProfileForm] = useState({ - name: currentUser?.name || '' + firstname: currentUser?.firstname || '', + lastname: currentUser?.lastname || '' }); // Password form state @@ -29,7 +30,8 @@ const Settings: React.FC = () => { useEffect(() => { if (currentUser) { setProfileForm({ - name: currentUser.name + firstname: currentUser.firstname || '', + lastname: currentUser.lastname || '' }); } }, [currentUser]); @@ -54,10 +56,21 @@ const Settings: React.FC = () => { e.preventDefault(); if (!currentUser) return; + // Validation + if (!profileForm.firstname.trim() || !profileForm.lastname.trim()) { + showNotification({ + type: 'error', + title: 'Fehler', + message: 'Vorname und Nachname sind erforderlich' + }); + return; + } + try { setLoading(true); await employeeService.updateEmployee(currentUser.id, { - name: profileForm.name.trim() + firstname: profileForm.firstname.trim(), + lastname: profileForm.lastname.trim() }); // Update the auth context with new user data @@ -167,8 +180,10 @@ const Settings: React.FC = () => { ); } - // Style constants for consistency - + // Get full name for display + const getFullName = () => { + return `${currentUser.firstname || ''} ${currentUser.lastname || ''}`.trim(); + }; return (
@@ -294,6 +309,9 @@ const Settings: React.FC = () => { disabled style={styles.fieldInputDisabled} /> +
+ E-Mail wird automatisch aus Vor- und Nachname generiert +
- {/* Editable name field */} -
- - { - e.target.style.borderColor = '#1a1325'; - e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)'; - }} - onBlur={(e) => { - e.target.style.borderColor = '#e8e8e8'; - e.target.style.boxShadow = 'none'; - }} - /> +

Persönliche Informationen

+ {/* Editable name fields */} +
+
+ + { + e.target.style.borderColor = '#1a1325'; + e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)'; + }} + onBlur={(e) => { + e.target.style.borderColor = '#e8e8e8'; + e.target.style.boxShadow = 'none'; + }} + /> +
+
+ + { + e.target.style.borderColor = '#1a1325'; + e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)'; + }} + onBlur={(e) => { + e.target.style.borderColor = '#e8e8e8'; + e.target.style.boxShadow = 'none'; + }} + /> +
+
+
+
+ Vorschau: {getFullName() || '(Kein Name)'} +
+
+ E-Mail: {currentUser.email} +
@@ -363,21 +411,21 @@ const Settings: React.FC = () => {