updated employee and shift structure

This commit is contained in:
2025-10-11 14:33:50 +02:00
parent eb49c58b2d
commit 5262b999aa
22 changed files with 1252 additions and 607 deletions

View File

@@ -3,9 +3,10 @@ export interface Employee {
id: string;
email: string;
name: string;
role: 'admin' | 'instandhalter' | 'user';
employeeType: 'chef' | 'neuling' | 'erfahren';
isSufficientlyIndependent: boolean;
role: 'admin' | 'maintenance' | 'user';
employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large';
canWorkAlone: boolean;
isActive: boolean;
createdAt: string;
lastLogin?: string | null;
@@ -15,19 +16,60 @@ export interface CreateEmployeeRequest {
email: string;
password: string;
name: string;
role: 'admin' | 'instandhalter' | 'user';
employeeType: 'chef' | 'neuling' | 'erfahren';
isSufficientlyIndependent: boolean;
role: 'admin' | 'maintenance' | 'user';
employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large';
canWorkAlone: boolean;
}
export interface UpdateEmployeeRequest {
name?: string;
role?: 'admin' | 'instandhalter' | 'user';
employeeType?: 'chef' | 'neuling' | 'erfahren';
isSufficientlyIndependent?: boolean;
role?: 'admin' | 'maintenance' | 'user';
employeeType?: 'manager' | 'trainee' | 'experienced';
contractType?: 'small' | 'large';
canWorkAlone?: boolean;
isActive?: boolean;
}
export interface EmployeeWithPassword extends Employee {
password: string;
}
export interface EmployeeAvailability {
id: string;
employeeId: string;
planId: string;
dayOfWeek: number; // 1=Monday, 7=Sunday
timeSlotId: string;
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
notes?: string;
}
export interface ManagerAvailability {
id: string;
employeeId: string;
planId: string;
dayOfWeek: number; // 1=Monday, 7=Sunday
timeSlotId: string;
isAvailable: boolean; // Simple available/not available
assignedBy: string; // Always self for manager
}
export interface CreateAvailabilityRequest {
planId: string;
availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[];
}
export interface UpdateAvailabilityRequest {
planId: string;
availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[];
}
export interface ManagerSelfAssignmentRequest {
planId: string;
assignments: Omit<ManagerAvailability, 'id' | 'employeeId' | 'assignedBy'>[];
}
export interface EmployeeWithAvailabilities extends Employee {
availabilities: EmployeeAvailability[];
}

View File

@@ -1,55 +0,0 @@
// backend/src/models/Shift.ts
export interface Shift {
id: string;
name: string;
description?: string;
isDefault: boolean;
createdBy: string;
createdAt: string;
shifts: ShiftSlot[];
}
export interface ShiftSlot {
id: string;
shiftId: string;
dayOfWeek: number;
name: string;
startTime: string;
endTime: string;
requiredEmployees: number;
color?: string;
}
export interface CreateShiftRequest {
name: string;
description?: string;
isDefault: boolean;
shifts: Omit<ShiftSlot, 'id' | 'shiftId'>[];
}
export interface UpdateShiftSlotRequest {
name?: string;
description?: string;
isDefault?: boolean;
shifts?: Omit<ShiftSlot, 'id' | 'shiftId'>[];
}
export interface ShiftPlan {
id: string;
name: string;
startDate: string;
endDate: string;
templateId?: string;
shifts: AssignedShift[];
status: 'draft' | 'published';
createdBy: string;
}
export interface AssignedShift {
id: string;
date: string;
startTime: string;
endTime: string;
requiredEmployees: number;
assignedEmployees: string[];
}

View File

@@ -0,0 +1,219 @@
// backend/src/models/ShiftPlan.ts
export interface ShiftPlan {
id: string;
name: string;
description?: string;
startDate?: string; // Optional for templates
endDate?: string; // Optional for templates
isTemplate: boolean;
status: 'draft' | 'published' | 'archived' | 'template';
createdBy: string;
createdAt: string;
timeSlots: TimeSlot[];
shifts: Shift[];
scheduledShifts?: ScheduledShift[]; // Only for non-template plans with dates
}
export interface TimeSlot {
id: string;
planId: string;
name: string;
startTime: string;
endTime: string;
description?: string;
}
export interface Shift {
id: string;
planId: string;
timeSlotId: string;
dayOfWeek: number; // 1=Monday, 7=Sunday
requiredEmployees: number;
color?: string;
}
export interface ScheduledShift {
id: string;
planId: string;
date: string;
timeSlotId: string;
requiredEmployees: number;
assignedEmployees: string[]; // employee IDs
}
export interface ShiftAssignment {
id: string;
scheduledShiftId: string;
employeeId: string;
assignmentStatus: 'assigned' | 'cancelled';
assignedAt: string;
assignedBy: string;
}
export interface EmployeeAvailability {
id: string;
employeeId: string;
planId: string;
dayOfWeek: number;
timeSlotId: string;
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
notes?: string;
}
// Request/Response DTOs
export interface CreateShiftPlanRequest {
name: string;
description?: string;
startDate?: string;
endDate?: string;
isTemplate: boolean;
timeSlots: Omit<TimeSlot, 'id' | 'planId'>[];
shifts: Omit<Shift, 'id' | 'planId'>[];
}
export interface UpdateShiftPlanRequest {
name?: string;
description?: string;
startDate?: string;
endDate?: string;
status?: 'draft' | 'published' | 'archived' | 'template';
timeSlots?: Omit<TimeSlot, 'id' | 'planId'>[];
shifts?: Omit<Shift, 'id' | 'planId'>[];
}
export interface CreateShiftFromTemplateRequest {
templatePlanId: string;
name: string;
startDate: string;
endDate: string;
description?: string;
}
export interface AssignEmployeeRequest {
employeeId: string;
scheduledShiftId: string;
}
export interface UpdateAvailabilityRequest {
planId: string;
availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[];
}
// Default time slots for ZEBRA (specific workplace)
export const DEFAULT_ZEBRA_TIME_SLOTS: Omit<TimeSlot, 'id' | 'planId'>[] = [
{
name: 'Vormittag',
startTime: '08:00',
endTime: '12:00',
description: 'Vormittagsschicht'
},
{
name: 'Nachmittag',
startTime: '11:30',
endTime: '15:30',
description: 'Nachmittagsschicht'
},
];
// Default time slots for general use
export const DEFAULT_TIME_SLOTS: Omit<TimeSlot, 'id' | 'planId'>[] = [
{
name: 'Vormittag',
startTime: '08:00',
endTime: '12:00',
description: 'Vormittagsschicht'
},
{
name: 'Nachmittag',
startTime: '11:30',
endTime: '15:30',
description: 'Nachmittagsschicht'
},
{
name: 'Abend',
startTime: '14:00',
endTime: '18:00',
description: 'Abendschicht'
},
];
// Helper functions
export function validateRequiredEmployees(shift: Shift | ScheduledShift): string[] {
const errors: string[] = [];
if (shift.requiredEmployees < 1) {
errors.push('Required employees must be at least 1');
}
if (shift.requiredEmployees > 10) {
errors.push('Required employees cannot exceed 10');
}
return errors;
}
export function isTemplate(plan: ShiftPlan): boolean {
return plan.isTemplate || plan.status === 'template';
}
export function hasDateRange(plan: ShiftPlan): boolean {
return !isTemplate(plan) && !!plan.startDate && !!plan.endDate;
}
export function validatePlanDates(plan: ShiftPlan): string[] {
const errors: string[] = [];
if (!isTemplate(plan)) {
if (!plan.startDate) errors.push('Start date is required for non-template plans');
if (!plan.endDate) errors.push('End date is required for non-template plans');
if (plan.startDate && plan.endDate && plan.startDate > plan.endDate) {
errors.push('Start date must be before end date');
}
}
return errors;
}
// Type guards
export function isScheduledShift(shift: Shift | ScheduledShift): shift is ScheduledShift {
return 'date' in shift;
}
// Template presets for quick setup
// Default shifts for ZEBRA standard week template with variable required employees
export const DEFAULT_ZEBRA_SHIFTS: Omit<Shift, 'id' | 'planId'>[] = [
// Monday-Thursday: Morning + Afternoon
...Array.from({ length: 4 }, (_, i) => i + 1).flatMap(day => [
{ timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' },
{ timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' }
]),
// Friday: Morning only
{ timeSlotId: 'morning', dayOfWeek: 5, requiredEmployees: 2, color: '#3498db' }
];
// Default shifts for general standard week template with variable required employees
export const DEFAULT_SHIFTS: Omit<Shift, 'id' | 'planId'>[] = [
// Monday-Friday: Morning + Afternoon + Evening
...Array.from({ length: 5 }, (_, i) => i + 1).flatMap(day => [
{ timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' },
{ timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' },
{ timeSlotId: 'evening', dayOfWeek: day, requiredEmployees: 1, color: '#2ecc71' } // Only 1 for evening
])
];
// Template presets for quick creation
export const TEMPLATE_PRESETS = {
ZEBRA_STANDARD: {
name: 'ZEBRA Standardwoche',
description: 'Standard Vorlage für ZEBRA: Mo-Do Vormittag+Nachmittag, Fr nur Vormittag',
timeSlots: DEFAULT_ZEBRA_TIME_SLOTS,
shifts: DEFAULT_ZEBRA_SHIFTS
},
GENERAL_STANDARD: {
name: 'Standard Wochenplan',
description: 'Standard Vorlage: Mo-Fr Vormittag+Nachmittag+Abend',
timeSlots: DEFAULT_TIME_SLOTS,
shifts: DEFAULT_SHIFTS
}
} as const;

View File

@@ -1,48 +0,0 @@
// backend/src/models/ShiftTemplate.ts
export interface TemplateShift {
id: string;
name: string;
description?: string;
isDefault: boolean;
createdBy: string;
createdAt: string;
shifts: TemplateShiftSlot[];
}
export interface TemplateShiftSlot {
id: string;
templateId: string;
dayOfWeek: number;
timeSlot: TemplateShiftTimeSlot;
requiredEmployees: number;
color?: string;
}
export interface TemplateShiftTimeSlot {
id: string;
name: string; // e.g., "Frühschicht", "Spätschicht"
startTime: string;
endTime: string;
}
export const DEFAULT_TIME_SLOTS: TemplateShiftTimeSlot[] = [
{ id: 'morning', name: 'Vormittag', startTime: '08:00', endTime: '12:00' },
{ id: 'afternoon', name: 'Nachmittag', startTime: '11:30', endTime: '15:30' },
];
export interface CreateShiftTemplateRequest {
name: string;
description?: string;
isDefault: boolean;
shifts: Omit<TemplateShiftSlot, 'id' | 'templateId'>[];
timeSlots: TemplateShiftTimeSlot[];
}
export interface UpdateShiftTemplateRequest {
name?: string;
description?: string;
isDefault?: boolean;
shifts?: Omit<TemplateShiftSlot, 'id' | 'templateId'>[];
timeSlots?: TemplateShiftTimeSlot[];
}

View File

@@ -1,15 +0,0 @@
// backend/src/models/User.ts
export interface User {
id: string;
email: string;
password: string; // gehashed
name: string;
role: 'admin' | 'instandhalter' | 'user';
createdAt: Date;
}
export interface UserSession {
userId: string;
token: string;
expiresAt: Date;
}

View File

@@ -0,0 +1,85 @@
// backend/src/models/defaults/employeeDefaults.ts
import { EmployeeAvailability, ManagerAvailability } from '../Employee.js';
// Default employee data for quick creation
export const EMPLOYEE_DEFAULTS = {
role: 'user' as const,
employeeType: 'experienced' as const,
contractType: 'small' as const,
canWorkAlone: false,
isActive: true
};
// Manager-specific defaults
export const MANAGER_DEFAULTS = {
role: 'admin' as const,
employeeType: 'manager' as const,
contractType: 'large' as const, // Not really used but required by DB
canWorkAlone: true,
isActive: true
};
// Contract type descriptions
export const CONTRACT_TYPE_DESCRIPTIONS = {
small: '1 Schicht pro Woche',
large: '2 Schichten pro Woche',
manager: 'Kein Vertragslimit - Immer MO und DI verfügbar'
} as const;
// Employee type descriptions
export const EMPLOYEE_TYPE_DESCRIPTIONS = {
manager: 'Chef - Immer MO und DI in beiden Schichten, kann eigene Schichten festlegen',
trainee: 'Neuling - Darf nicht alleine sein, benötigt erfahrene Begleitung',
experienced: 'Erfahren - Kann alleine arbeiten (wenn freigegeben)'
} as const;
// Availability preference descriptions
export const AVAILABILITY_PREFERENCES = {
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
2: { label: 'Möglich', color: '#f59e0b', description: 'Kann diese Schicht arbeiten' },
3: { label: 'Nicht möglich', color: '#ef4444', description: 'Kann diese Schicht nicht arbeiten' }
} as const;
// Default availability for new employees (all shifts unavailable as level 3)
export function createDefaultAvailabilities(employeeId: string, planId: string, timeSlotIds: string[]): Omit<EmployeeAvailability, 'id'>[] {
const availabilities: Omit<EmployeeAvailability, 'id'>[] = [];
// 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
});
}
}
return availabilities;
}
// Create complete manager availability for all days (default: only Mon-Tue available)
export function createManagerDefaultSchedule(managerId: string, planId: string, timeSlotIds: string[]): Omit<ManagerAvailability, 'id'>[] {
const assignments: Omit<ManagerAvailability, 'id'>[] = [];
// Monday to Sunday (1-7)
for (let dayOfWeek = 1; dayOfWeek <= 7; dayOfWeek++) {
for (const timeSlotId of timeSlotIds) {
// Default: available only on Monday (1) and Tuesday (2)
const isAvailable = dayOfWeek === 1 || dayOfWeek === 2;
assignments.push({
employeeId: managerId,
planId,
dayOfWeek,
timeSlotId,
isAvailable,
assignedBy: managerId
});
}
}
return assignments;
}

View File

@@ -0,0 +1,3 @@
// backend/src/models/defaults/index.ts
export * from './employeeDefaults.js';
export * from './shiftPlanDefaults.js';

View File

@@ -0,0 +1,146 @@
// backend/src/models/defaults/shiftPlanDefaults.ts
import { TimeSlot, Shift } from '../ShiftPlan.js';
// Default time slots for ZEBRA (specific workplace)
export const DEFAULT_ZEBRA_TIME_SLOTS: Omit<TimeSlot, 'id' | 'planId'>[] = [
{
name: 'Vormittag',
startTime: '08:00',
endTime: '12:00',
description: 'Vormittagsschicht'
},
{
name: 'Nachmittag',
startTime: '11:30',
endTime: '15:30',
description: 'Nachmittagsschicht'
},
];
// Default time slots for general use
export const DEFAULT_TIME_SLOTS: Omit<TimeSlot, 'id' | 'planId'>[] = [
{
name: 'Vormittag',
startTime: '08:00',
endTime: '12:00',
description: 'Vormittagsschicht'
},
{
name: 'Nachmittag',
startTime: '11:30',
endTime: '15:30',
description: 'Nachmittagsschicht'
},
{
name: 'Abend',
startTime: '14:00',
endTime: '18:00',
description: 'Abendschicht'
},
];
// Default shifts for ZEBRA standard week template with variable required employees
export const DEFAULT_ZEBRA_SHIFTS: Omit<Shift, 'id' | 'planId'>[] = [
// Monday-Thursday: Morning + Afternoon
...Array.from({ length: 4 }, (_, i) => i + 1).flatMap(day => [
{ timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' },
{ timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' }
]),
// Friday: Morning only
{ timeSlotId: 'morning', dayOfWeek: 5, requiredEmployees: 2, color: '#3498db' }
];
// Default shifts for general standard week template with variable required employees
export const DEFAULT_SHIFTS: Omit<Shift, 'id' | 'planId'>[] = [
// Monday-Friday: Morning + Afternoon + Evening
...Array.from({ length: 5 }, (_, i) => i + 1).flatMap(day => [
{ timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 2, color: '#3498db' },
{ timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 2, color: '#e74c3c' },
{ timeSlotId: 'evening', dayOfWeek: day, requiredEmployees: 1, color: '#2ecc71' } // Only 1 for evening
])
];
// Template presets for quick creation
export const TEMPLATE_PRESETS = {
ZEBRA_STANDARD: {
name: 'ZEBRA Standardwoche',
description: 'Standard Vorlage für ZEBRA: Mo-Do Vormittag+Nachmittag, Fr nur Vormittag',
timeSlots: DEFAULT_ZEBRA_TIME_SLOTS,
shifts: DEFAULT_ZEBRA_SHIFTS
},
ZEBRA_MINIMAL: {
name: 'ZEBRA Minimal',
description: 'ZEBRA mit minimaler Besetzung',
timeSlots: DEFAULT_ZEBRA_TIME_SLOTS,
shifts: [
...Array.from({ length: 5 }, (_, i) => i + 1).flatMap(day => [
{ timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 1, color: '#3498db' },
{ timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 1, color: '#e74c3c' }
])
]
},
ZEBRA_FULL: {
name: 'ZEBRA Vollbesetzung',
description: 'ZEBRA mit voller Besetzung',
timeSlots: DEFAULT_ZEBRA_TIME_SLOTS,
shifts: [
...Array.from({ length: 5 }, (_, i) => i + 1).flatMap(day => [
{ timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 3, color: '#3498db' },
{ timeSlotId: 'afternoon', dayOfWeek: day, requiredEmployees: 3, color: '#e74c3c' }
])
]
},
GENERAL_STANDARD: {
name: 'Standard Wochenplan',
description: 'Standard Vorlage: Mo-Fr Vormittag+Nachmittag+Abend',
timeSlots: DEFAULT_TIME_SLOTS,
shifts: DEFAULT_SHIFTS
},
ZEBRA_PART_TIME: {
name: 'ZEBRA Teilzeit',
description: 'ZEBRA Vorlage mit reduzierten Schichten',
timeSlots: DEFAULT_ZEBRA_TIME_SLOTS,
shifts: [
// Monday-Thursday: Morning only
...Array.from({ length: 4 }, (_, i) => i + 1).map(day => ({
timeSlotId: 'morning', dayOfWeek: day, requiredEmployees: 1, color: '#3498db'
}))
]
}
} as const;
// Helper function to create plan from preset
export function createPlanFromPreset(
presetName: keyof typeof TEMPLATE_PRESETS,
isTemplate: boolean = true,
startDate?: string,
endDate?: string
) {
const preset = TEMPLATE_PRESETS[presetName];
return {
name: preset.name,
description: preset.description,
startDate,
endDate,
isTemplate,
timeSlots: preset.timeSlots,
shifts: preset.shifts
};
}
// Color schemes for shifts
export const SHIFT_COLORS = {
morning: '#3498db', // Blue
afternoon: '#e74c3c', // Red
evening: '#2ecc71', // Green
night: '#9b59b6', // Purple
default: '#95a5a6' // Gray
} as const;
// Status descriptions
export const PLAN_STATUS_DESCRIPTIONS = {
draft: 'Entwurf - Kann bearbeitet werden',
published: 'Veröffentlicht - Für alle sichtbar',
archived: 'Archiviert - Nur noch lesbar',
template: 'Vorlage - Kann für neue Pläne verwendet werden'
} as const;

View File

@@ -0,0 +1,128 @@
// backend/src/models/helpers/employeeHelpers.ts
import { Employee, CreateEmployeeRequest, EmployeeAvailability, ManagerAvailability } from '../Employee.js';
// Validation helpers
export function validateEmployee(employee: CreateEmployeeRequest): string[] {
const errors: string[] = [];
if (!employee.email || !employee.email.includes('@')) {
errors.push('Valid email is required');
}
if (!employee.password || employee.password.length < 6) {
errors.push('Password must be at least 6 characters long');
}
if (!employee.name || employee.name.trim().length < 2) {
errors.push('Name is required and must be at least 2 characters long');
}
if (!employee.contractType) {
errors.push('Contract type is required');
}
return errors;
}
export function validateAvailability(availability: Omit<EmployeeAvailability, 'id' | 'employeeId'>): string[] {
const errors: string[] = [];
if (availability.dayOfWeek < 1 || availability.dayOfWeek > 7) {
errors.push('Day of week must be between 1 and 7');
}
if (![1, 2, 3].includes(availability.preferenceLevel)) {
errors.push('Preference level must be 1, 2, or 3');
}
if (!availability.timeSlotId) {
errors.push('Time slot ID is required');
}
if (!availability.planId) {
errors.push('Plan ID is required');
}
return errors;
}
// Employee type guards
export function isManager(employee: Employee): boolean {
return employee.employeeType === 'manager';
}
export function isTrainee(employee: Employee): boolean {
return employee.employeeType === 'trainee';
}
export function isExperienced(employee: Employee): boolean {
return employee.employeeType === 'experienced';
}
export function isAdmin(employee: Employee): boolean {
return employee.role === 'admin';
}
// Business logic helpers
export function canEmployeeWorkAlone(employee: Employee): boolean {
return employee.canWorkAlone && employee.employeeType === 'experienced';
}
export function getEmployeeWorkHours(employee: Employee): number {
// Manager: no contract limit, others: small=1, large=2 shifts per week
return isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
}
export function requiresAvailabilityPreference(employee: Employee): boolean {
// Only non-managers use the preference system
return !isManager(employee);
}
export function canSetOwnAvailability(employee: Employee): boolean {
// Manager can set their own specific shift assignments
return isManager(employee);
}
// Manager availability helpers
export function isManagerAvailable(
managerAssignments: ManagerAvailability[],
dayOfWeek: number,
timeSlotId: string
): boolean {
const assignment = managerAssignments.find(assignment =>
assignment.dayOfWeek === dayOfWeek &&
assignment.timeSlotId === timeSlotId
);
return assignment ? assignment.isAvailable : false;
}
export function getManagerAvailableShifts(managerAssignments: ManagerAvailability[]): ManagerAvailability[] {
return managerAssignments.filter(assignment => assignment.isAvailable);
}
export function updateManagerAvailability(
assignments: ManagerAvailability[],
dayOfWeek: number,
timeSlotId: string,
isAvailable: boolean
): ManagerAvailability[] {
return assignments.map(assignment =>
assignment.dayOfWeek === dayOfWeek && assignment.timeSlotId === timeSlotId
? { ...assignment, isAvailable }
: assignment
);
}
export function validateManagerMinimumAvailability(managerAssignments: ManagerAvailability[]): boolean {
const requiredShifts = [
{ dayOfWeek: 1, timeSlotId: 'morning' },
{ dayOfWeek: 1, timeSlotId: 'afternoon' },
{ dayOfWeek: 2, timeSlotId: 'morning' },
{ dayOfWeek: 2, timeSlotId: 'afternoon' }
];
return requiredShifts.every(required =>
isManagerAvailable(managerAssignments, required.dayOfWeek, required.timeSlotId)
);
}

View File

@@ -0,0 +1,3 @@
// backend/src/models/helpers/index.ts
export * from './employeeHelpers.js';
export * from './shiftPlanHelpers.js';

View File

@@ -0,0 +1,118 @@
// backend/src/models/helpers/shiftPlanHelpers.ts
import { ShiftPlan, Shift, ScheduledShift, TimeSlot } from '../ShiftPlan.js';
// Validation helpers
export function validateRequiredEmployees(shift: Shift | ScheduledShift): string[] {
const errors: string[] = [];
if (shift.requiredEmployees < 1) {
errors.push('Required employees must be at least 1');
}
if (shift.requiredEmployees > 10) {
errors.push('Required employees cannot exceed 10');
}
return errors;
}
export function isTemplate(plan: ShiftPlan): boolean {
return plan.isTemplate || plan.status === 'template';
}
export function hasDateRange(plan: ShiftPlan): boolean {
return !isTemplate(plan) && !!plan.startDate && !!plan.endDate;
}
export function validatePlanDates(plan: ShiftPlan): string[] {
const errors: string[] = [];
if (!isTemplate(plan)) {
if (!plan.startDate) errors.push('Start date is required for non-template plans');
if (!plan.endDate) errors.push('End date is required for non-template plans');
if (plan.startDate && plan.endDate && plan.startDate > plan.endDate) {
errors.push('Start date must be before end date');
}
}
return errors;
}
export function validateTimeSlot(timeSlot: { startTime: string; endTime: string }): string[] {
const errors: string[] = [];
if (!timeSlot.startTime || !timeSlot.endTime) {
errors.push('Start time and end time are required');
return errors;
}
const start = new Date(`2000-01-01T${timeSlot.startTime}`);
const end = new Date(`2000-01-01T${timeSlot.endTime}`);
if (start >= end) {
errors.push('Start time must be before end time');
}
return errors;
}
// Type guards
export function isScheduledShift(shift: Shift | ScheduledShift): shift is ScheduledShift {
return 'date' in shift;
}
export function isTemplateShift(shift: Shift | ScheduledShift): shift is Shift {
return 'dayOfWeek' in shift && !('date' in shift);
}
// Business logic helpers
export function getShiftsForDay(plan: ShiftPlan, dayOfWeek: number): Shift[] {
return plan.shifts.filter(shift => shift.dayOfWeek === dayOfWeek);
}
export function getTimeSlotById(plan: ShiftPlan, timeSlotId: string): TimeSlot | undefined {
return plan.timeSlots.find(slot => slot.id === timeSlotId);
}
export function calculateTotalRequiredEmployees(plan: ShiftPlan): number {
return plan.shifts.reduce((total, shift) => total + shift.requiredEmployees, 0);
}
export function getScheduledShiftByDateAndTime(
plan: ShiftPlan,
date: string,
timeSlotId: string
): 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[] = [];
if (!hasDateRange(plan)) {
errors.push('Plan must have a date range to be published');
}
if (plan.shifts.length === 0) {
errors.push('Plan must have at least one shift');
}
if (plan.timeSlots.length === 0) {
errors.push('Plan must have at least one time slot');
}
// Validate all shifts
plan.shifts.forEach((shift, index) => {
const shiftErrors = validateRequiredEmployees(shift);
if (shiftErrors.length > 0) {
errors.push(`Shift ${index + 1}: ${shiftErrors.join(', ')}`);
}
});
return {
canPublish: errors.length === 0,
errors
};
}