mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
fixed settings
This commit is contained in:
@@ -7,9 +7,20 @@ import { AuthRequest } from '../middleware/auth.js';
|
|||||||
import { CreateEmployeeRequest } from '../models/Employee.js';
|
import { CreateEmployeeRequest } from '../models/Employee.js';
|
||||||
|
|
||||||
function generateEmail(firstname: string, lastname: string): string {
|
function generateEmail(firstname: string, lastname: string): string {
|
||||||
// Remove special characters and convert to lowercase
|
// Convert German umlauts to their expanded forms
|
||||||
const cleanFirstname = firstname.toLowerCase().replace(/[^a-z0-9]/g, '');
|
const convertUmlauts = (str: string): string => {
|
||||||
const cleanLastname = lastname.toLowerCase().replace(/[^a-z0-9]/g, '');
|
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`;
|
return `${cleanFirstname}.${cleanLastname}@sp.de`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,11 +104,14 @@ CREATE TABLE IF NOT EXISTS employee_availability (
|
|||||||
UNIQUE(employee_id, plan_id, shift_id)
|
UNIQUE(employee_id, plan_id, shift_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Performance indexes
|
-- Performance indexes (UPDATED - removed role index from employees)
|
||||||
CREATE INDEX IF NOT EXISTS idx_employees_role_active ON employees(role, is_active);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_employees_email_active ON employees(email, is_active);
|
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);
|
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_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_created_by ON shift_plans(created_by);
|
||||||
CREATE INDEX IF NOT EXISTS idx_shift_plans_template ON shift_plans(is_template, status);
|
CREATE INDEX IF NOT EXISTS idx_shift_plans_template ON shift_plans(is_template, status);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const EMPLOYEE_DEFAULTS = {
|
|||||||
export const MANAGER_DEFAULTS = {
|
export const MANAGER_DEFAULTS = {
|
||||||
role: 'admin' as const,
|
role: 'admin' as const,
|
||||||
employeeType: 'manager' as const,
|
employeeType: 'manager' as const,
|
||||||
contractType: 'large' as const, // Not really used but required by DB
|
contractType: 'large' as const,
|
||||||
canWorkAlone: true,
|
canWorkAlone: true,
|
||||||
isActive: true
|
isActive: true
|
||||||
};
|
};
|
||||||
@@ -64,26 +64,25 @@ export const AVAILABILITY_PREFERENCES = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Default availability for new employees (all shifts unavailable as level 3)
|
// Default availability for new employees (all shifts unavailable as level 3)
|
||||||
export function createDefaultAvailabilities(employeeId: string, planId: string, timeSlotIds: string[]): Omit<EmployeeAvailability, 'id'>[] {
|
// UPDATED: Now uses shiftId instead of timeSlotId + dayOfWeek
|
||||||
|
export function createDefaultAvailabilities(employeeId: string, planId: string, shiftIds: string[]): Omit<EmployeeAvailability, 'id'>[] {
|
||||||
const availabilities: Omit<EmployeeAvailability, 'id'>[] = [];
|
const availabilities: Omit<EmployeeAvailability, 'id'>[] = [];
|
||||||
|
|
||||||
// Monday to Friday (1-5)
|
// Create one availability entry per shift
|
||||||
for (let day = 1; day <= 5; day++) {
|
for (const shiftId of shiftIds) {
|
||||||
for (const timeSlotId of timeSlotIds) {
|
availabilities.push({
|
||||||
availabilities.push({
|
employeeId,
|
||||||
employeeId,
|
planId,
|
||||||
planId,
|
shiftId,
|
||||||
dayOfWeek: day,
|
preferenceLevel: 3 // Default to "unavailable" - employees must explicitly set availability
|
||||||
timeSlotId,
|
});
|
||||||
preferenceLevel: 3 // Default to "unavailable" - employees must explicitly set availability
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return availabilities;
|
return availabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create complete manager availability for all days (default: only Mon-Tue available)
|
// 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<ManagerAvailability, 'id'>[] {
|
export function createManagerDefaultSchedule(managerId: string, planId: string, timeSlotIds: string[]): Omit<ManagerAvailability, 'id'>[] {
|
||||||
const assignments: Omit<ManagerAvailability, 'id'>[] = [];
|
const assignments: Omit<ManagerAvailability, 'id'>[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,51 @@
|
|||||||
// backend/src/models/helpers/employeeHelpers.ts
|
// backend/src/models/helpers/employeeHelpers.ts
|
||||||
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
|
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[] {
|
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
if (!employee.email?.includes('@')) {
|
// Email is now auto-generated, so no email validation needed
|
||||||
errors.push('Valid email is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (employee.password?.length < 6) {
|
if (employee.password?.length < 6) {
|
||||||
errors.push('Password must be at least 6 characters long');
|
errors.push('Password must be at least 6 characters long');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!employee.name?.trim() || employee.name.trim().length < 2) {
|
if (!employee.firstname?.trim() || employee.firstname.trim().length < 2) {
|
||||||
errors.push('Name is required and must be at least 2 characters long');
|
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;
|
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
|
// Simplified business logic helpers
|
||||||
export const isManager = (employee: Employee): boolean =>
|
export const isManager = (employee: Employee): boolean =>
|
||||||
employee.employeeType === 'manager';
|
employee.employeeType === 'manager';
|
||||||
@@ -38,3 +64,26 @@ export const canEmployeeWorkAlone = (employee: Employee): boolean =>
|
|||||||
|
|
||||||
export const getEmployeeWorkHours = (employee: Employee): number =>
|
export const getEmployeeWorkHours = (employee: Employee): number =>
|
||||||
isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
|
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<EmployeeAvailability, 'id' | 'employeeId'>): 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;
|
||||||
|
}
|
||||||
@@ -78,7 +78,8 @@ export function calculateTotalRequiredEmployees(plan: ShiftPlan): number {
|
|||||||
return plan.shifts.reduce((total, shift) => total + shift.requiredEmployees, 0);
|
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,
|
plan: ShiftPlan,
|
||||||
date: string,
|
date: string,
|
||||||
timeSlotId: string
|
timeSlotId: string
|
||||||
@@ -86,7 +87,7 @@ export function calculateTotalRequiredEmployees(plan: ShiftPlan): number {
|
|||||||
return plan.scheduledShifts?.find(shift =>
|
return plan.scheduledShifts?.find(shift =>
|
||||||
shift.date === date && shift.timeSlotId === timeSlotId
|
shift.date === date && shift.timeSlotId === timeSlotId
|
||||||
);
|
);
|
||||||
}*/
|
}
|
||||||
|
|
||||||
export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: string[] } {
|
export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: string[] } {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
@@ -115,4 +116,14 @@ export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors:
|
|||||||
canPublish: errors.length === 0,
|
canPublish: errors.length === 0,
|
||||||
errors
|
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)}`;
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ interface AuthContextType {
|
|||||||
refreshUser: () => void;
|
refreshUser: () => void;
|
||||||
needsSetup: boolean;
|
needsSetup: boolean;
|
||||||
checkSetupStatus: () => Promise<void>;
|
checkSetupStatus: () => Promise<void>;
|
||||||
updateUser: (userData: Employee) => void; // Add this line
|
updateUser: (userData: Employee) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|||||||
@@ -14,39 +14,39 @@ export const EMPLOYEE_DEFAULTS = {
|
|||||||
export const MANAGER_DEFAULTS = {
|
export const MANAGER_DEFAULTS = {
|
||||||
role: 'admin' as const,
|
role: 'admin' as const,
|
||||||
employeeType: 'manager' as const,
|
employeeType: 'manager' as const,
|
||||||
contractType: 'large' as const, // Not really used but required by DB
|
contractType: 'large' as const,
|
||||||
canWorkAlone: true,
|
canWorkAlone: true,
|
||||||
isActive: true
|
isActive: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EMPLOYEE_TYPE_CONFIG = [
|
export const EMPLOYEE_TYPE_CONFIG = {
|
||||||
{
|
manager: {
|
||||||
value: 'manager' as const,
|
value: 'manager' as const,
|
||||||
label: 'Chef/Administrator',
|
label: 'Chef/Administrator',
|
||||||
color: '#e74c3c',
|
color: '#e74c3c',
|
||||||
independent: true,
|
independent: true,
|
||||||
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
|
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
|
||||||
},
|
},
|
||||||
{
|
experienced: {
|
||||||
value: 'experienced' as const,
|
value: 'experienced' as const,
|
||||||
label: 'Erfahren',
|
label: 'Erfahren',
|
||||||
color: '#3498db',
|
color: '#3498db',
|
||||||
independent: true,
|
independent: true,
|
||||||
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen'
|
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen'
|
||||||
},
|
},
|
||||||
{
|
trainee: {
|
||||||
value: 'trainee' as const,
|
value: 'trainee' as const,
|
||||||
label: 'Neuling',
|
label: 'Neuling',
|
||||||
color: '#27ae60',
|
color: '#27ae60',
|
||||||
independent: false,
|
independent: false,
|
||||||
description: 'Benötigt Einarbeitung und Unterstützung'
|
description: 'Benötigt Einarbeitung und Unterstützung'
|
||||||
}
|
}
|
||||||
] as const;
|
} as const;
|
||||||
|
|
||||||
export const ROLE_CONFIG = [
|
export const ROLE_CONFIG = [
|
||||||
{ value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen', color: '#27ae60' },
|
{ value: 'user' as const, label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen', color: '#27ae60' },
|
||||||
{ value: 'maintenance', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten', color: '#3498db' },
|
{ value: 'maintenance' as const, 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: 'admin' as const, label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen', color: '#e74c3c' }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Contract type descriptions
|
// Contract type descriptions
|
||||||
@@ -64,26 +64,25 @@ export const AVAILABILITY_PREFERENCES = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Default availability for new employees (all shifts unavailable as level 3)
|
// Default availability for new employees (all shifts unavailable as level 3)
|
||||||
export function createDefaultAvailabilities(employeeId: string, planId: string, timeSlotIds: string[]): Omit<EmployeeAvailability, 'id'>[] {
|
// UPDATED: Now uses shiftId instead of timeSlotId + dayOfWeek
|
||||||
|
export function createDefaultAvailabilities(employeeId: string, planId: string, shiftIds: string[]): Omit<EmployeeAvailability, 'id'>[] {
|
||||||
const availabilities: Omit<EmployeeAvailability, 'id'>[] = [];
|
const availabilities: Omit<EmployeeAvailability, 'id'>[] = [];
|
||||||
|
|
||||||
// Monday to Friday (1-5)
|
// Create one availability entry per shift
|
||||||
for (let day = 1; day <= 5; day++) {
|
for (const shiftId of shiftIds) {
|
||||||
for (const timeSlotId of timeSlotIds) {
|
availabilities.push({
|
||||||
availabilities.push({
|
employeeId,
|
||||||
employeeId,
|
planId,
|
||||||
planId,
|
shiftId,
|
||||||
dayOfWeek: day,
|
preferenceLevel: 3 // Default to "unavailable" - employees must explicitly set availability
|
||||||
timeSlotId,
|
});
|
||||||
preferenceLevel: 3 // Default to "unavailable" - employees must explicitly set availability
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return availabilities;
|
return availabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create complete manager availability for all days (default: only Mon-Tue available)
|
// 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<ManagerAvailability, 'id'>[] {
|
export function createManagerDefaultSchedule(managerId: string, planId: string, timeSlotIds: string[]): Omit<ManagerAvailability, 'id'>[] {
|
||||||
const assignments: Omit<ManagerAvailability, 'id'>[] = [];
|
const assignments: Omit<ManagerAvailability, 'id'>[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,51 @@
|
|||||||
// backend/src/models/helpers/employeeHelpers.ts
|
// backend/src/models/helpers/employeeHelpers.ts
|
||||||
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
|
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[] {
|
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
if (!employee.email?.includes('@')) {
|
// Email is now auto-generated, so no email validation needed
|
||||||
errors.push('Valid email is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (employee.password?.length < 6) {
|
if (employee.password?.length < 6) {
|
||||||
errors.push('Password must be at least 6 characters long');
|
errors.push('Password must be at least 6 characters long');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!employee.name?.trim() || employee.name.trim().length < 2) {
|
if (!employee.firstname?.trim() || employee.firstname.trim().length < 2) {
|
||||||
errors.push('Name is required and must be at least 2 characters long');
|
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;
|
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
|
// Simplified business logic helpers
|
||||||
export const isManager = (employee: Employee): boolean =>
|
export const isManager = (employee: Employee): boolean =>
|
||||||
employee.employeeType === 'manager';
|
employee.employeeType === 'manager';
|
||||||
@@ -38,3 +64,26 @@ export const canEmployeeWorkAlone = (employee: Employee): boolean =>
|
|||||||
|
|
||||||
export const getEmployeeWorkHours = (employee: Employee): number =>
|
export const getEmployeeWorkHours = (employee: Employee): number =>
|
||||||
isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
|
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<EmployeeAvailability, 'id' | 'employeeId'>): 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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
// backend/src/models/helpers/shiftPlanHelpers.ts
|
// backend/src/models/helpers/shiftPlanHelpers.ts
|
||||||
import { shiftAssignmentService } from '../../services/shiftAssignmentService.js';
|
|
||||||
import { ShiftPlan, Shift, ScheduledShift, TimeSlot } from '../ShiftPlan.js';
|
import { ShiftPlan, Shift, ScheduledShift, TimeSlot } from '../ShiftPlan.js';
|
||||||
|
|
||||||
// Validation helpers
|
// Validation helpers
|
||||||
@@ -79,16 +78,16 @@ export function calculateTotalRequiredEmployees(plan: ShiftPlan): number {
|
|||||||
return plan.shifts.reduce((total, shift) => total + shift.requiredEmployees, 0);
|
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,
|
plan: ShiftPlan,
|
||||||
date: string,
|
date: string,
|
||||||
timeSlotId: string
|
timeSlotId: string
|
||||||
): Promise<ScheduledShift | undefined> {
|
): ScheduledShift | undefined {
|
||||||
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(plan.id);
|
return plan.scheduledShifts?.find(shift =>
|
||||||
return scheduledShifts.find(
|
shift.date === date && shift.timeSlotId === timeSlotId
|
||||||
shift => shift.date === date && shift.timeSlotId === timeSlotId
|
|
||||||
);
|
);
|
||||||
}*/
|
}
|
||||||
|
|
||||||
export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: string[] } {
|
export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors: string[] } {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
@@ -117,4 +116,14 @@ export function canPublishPlan(plan: ShiftPlan): { canPublish: boolean; errors:
|
|||||||
canPublish: errors.length === 0,
|
canPublish: errors.length === 0,
|
||||||
errors
|
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)}`;
|
||||||
}
|
}
|
||||||
@@ -14,9 +14,10 @@ const Settings: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showAvailabilityManager, setShowAvailabilityManager] = useState(false);
|
const [showAvailabilityManager, setShowAvailabilityManager] = useState(false);
|
||||||
|
|
||||||
// Profile form state
|
// Profile form state - updated for firstname/lastname
|
||||||
const [profileForm, setProfileForm] = useState({
|
const [profileForm, setProfileForm] = useState({
|
||||||
name: currentUser?.name || ''
|
firstname: currentUser?.firstname || '',
|
||||||
|
lastname: currentUser?.lastname || ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// Password form state
|
// Password form state
|
||||||
@@ -29,7 +30,8 @@ const Settings: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
setProfileForm({
|
setProfileForm({
|
||||||
name: currentUser.name
|
firstname: currentUser.firstname || '',
|
||||||
|
lastname: currentUser.lastname || ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
@@ -54,10 +56,21 @@ const Settings: React.FC = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!profileForm.firstname.trim() || !profileForm.lastname.trim()) {
|
||||||
|
showNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Fehler',
|
||||||
|
message: 'Vorname und Nachname sind erforderlich'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await employeeService.updateEmployee(currentUser.id, {
|
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
|
// 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 (
|
return (
|
||||||
<div style={styles.container}>
|
<div style={styles.container}>
|
||||||
@@ -294,6 +309,9 @@ const Settings: React.FC = () => {
|
|||||||
disabled
|
disabled
|
||||||
style={styles.fieldInputDisabled}
|
style={styles.fieldInputDisabled}
|
||||||
/>
|
/>
|
||||||
|
<div style={styles.fieldHint}>
|
||||||
|
E-Mail wird automatisch aus Vor- und Nachname generiert
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.field}>
|
<div style={styles.field}>
|
||||||
<label style={styles.fieldLabel}>
|
<label style={styles.fieldLabel}>
|
||||||
@@ -331,31 +349,61 @@ const Settings: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.infoCard}>
|
<div style={styles.infoCard}>
|
||||||
{/* Editable name field */}
|
<h4 style={styles.infoCardTitle}>Persönliche Informationen</h4>
|
||||||
<div style={{ ...styles.field, marginTop: '1rem' }}>
|
{/* Editable name fields */}
|
||||||
<label style={styles.fieldLabel}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
|
||||||
Vollständiger Name *
|
<div style={styles.field}>
|
||||||
</label>
|
<label style={styles.fieldLabel}>
|
||||||
<input
|
Vorname *
|
||||||
type="text"
|
</label>
|
||||||
name="name"
|
<input
|
||||||
value={profileForm.name}
|
type="text"
|
||||||
onChange={handleProfileChange}
|
name="firstname"
|
||||||
required
|
value={profileForm.firstname}
|
||||||
style={{
|
onChange={handleProfileChange}
|
||||||
...styles.fieldInput,
|
required
|
||||||
width: '95%'
|
style={styles.fieldInput}
|
||||||
}}
|
placeholder="Ihr Vorname"
|
||||||
placeholder="Ihr vollständiger Name"
|
onFocus={(e) => {
|
||||||
onFocus={(e) => {
|
e.target.style.borderColor = '#1a1325';
|
||||||
e.target.style.borderColor = '#1a1325';
|
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
||||||
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
|
}}
|
||||||
}}
|
onBlur={(e) => {
|
||||||
onBlur={(e) => {
|
e.target.style.borderColor = '#e8e8e8';
|
||||||
e.target.style.borderColor = '#e8e8e8';
|
e.target.style.boxShadow = 'none';
|
||||||
e.target.style.boxShadow = 'none';
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
<div style={styles.field}>
|
||||||
|
<label style={styles.fieldLabel}>
|
||||||
|
Nachname *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="lastname"
|
||||||
|
value={profileForm.lastname}
|
||||||
|
onChange={handleProfileChange}
|
||||||
|
required
|
||||||
|
style={styles.fieldInput}
|
||||||
|
placeholder="Ihr Nachname"
|
||||||
|
onFocus={(e) => {
|
||||||
|
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';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: '1rem', padding: '1rem', background: '#f8f9fa', borderRadius: '8px' }}>
|
||||||
|
<div style={{ fontSize: '0.9rem', color: '#666' }}>
|
||||||
|
<strong>Vorschau:</strong> {getFullName() || '(Kein Name)'}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '0.8rem', color: '#888', marginTop: '0.5rem' }}>
|
||||||
|
E-Mail: {currentUser.email}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,21 +411,21 @@ const Settings: React.FC = () => {
|
|||||||
<div style={styles.actions}>
|
<div style={styles.actions}>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || !profileForm.name.trim()}
|
disabled={loading || !profileForm.firstname.trim() || !profileForm.lastname.trim()}
|
||||||
style={{
|
style={{
|
||||||
...styles.button,
|
...styles.button,
|
||||||
...styles.buttonPrimary,
|
...styles.buttonPrimary,
|
||||||
...((loading || !profileForm.name.trim()) ? styles.buttonDisabled : {})
|
...((loading || !profileForm.firstname.trim() || !profileForm.lastname.trim()) ? styles.buttonDisabled : {})
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!loading && profileForm.name.trim()) {
|
if (!loading && profileForm.firstname.trim() && profileForm.lastname.trim()) {
|
||||||
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
|
e.currentTarget.style.background = styles.buttonPrimaryHover.background;
|
||||||
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
|
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
|
||||||
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
|
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
if (!loading && profileForm.name.trim()) {
|
if (!loading && profileForm.firstname.trim() && profileForm.lastname.trim()) {
|
||||||
e.currentTarget.style.background = styles.buttonPrimary.background;
|
e.currentTarget.style.background = styles.buttonPrimary.background;
|
||||||
e.currentTarget.style.transform = 'none';
|
e.currentTarget.style.transform = 'none';
|
||||||
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
|
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
|
||||||
|
|||||||
@@ -550,7 +550,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
return await employeeService.getAvailabilities(emp.id);
|
return await employeeService.getAvailabilities(emp.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Failed to load availabilities for ${emp.name}:`, error);
|
console.error(`❌ Failed to load availabilities for ${emp.email}:`, error);
|
||||||
return []; // Return empty array instead of failing entire operation
|
return []; // Return empty array instead of failing entire operation
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -599,7 +599,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
console.warn('⚠️ Missing availabilities for employees:',
|
console.warn('⚠️ Missing availabilities for employees:',
|
||||||
missingEmployees.map(emp => emp.name));
|
missingEmployees.map(emp => emp.email));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -806,7 +806,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
displayText = assignedEmployees.map(empId => {
|
displayText = assignedEmployees.map(empId => {
|
||||||
const employee = employees.find(emp => emp.id === empId);
|
const employee = employees.find(emp => emp.id === empId);
|
||||||
return employee ? employee.name : 'Unbekannt';
|
return employee ? employee.email : 'Unbekannt';
|
||||||
}).join(', ');
|
}).join(', ');
|
||||||
}
|
}
|
||||||
} else if (assignmentResult) {
|
} else if (assignmentResult) {
|
||||||
@@ -821,7 +821,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
assignedEmployees = getAssignmentsForScheduledShift(scheduledShift);
|
assignedEmployees = getAssignmentsForScheduledShift(scheduledShift);
|
||||||
displayText = assignedEmployees.map(empId => {
|
displayText = assignedEmployees.map(empId => {
|
||||||
const employee = employees.find(emp => emp.id === empId);
|
const employee = employees.find(emp => emp.id === empId);
|
||||||
return employee ? employee.name : 'Unbekannt';
|
return employee ? employee.email : 'Unbekannt';
|
||||||
}).join(', ');
|
}).join(', ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user