mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
frontend with ony errors
This commit is contained in:
75
frontend/src/models/Employee.ts
Normal file
75
frontend/src/models/Employee.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
// backend/src/models/Employee.ts
|
||||
export interface Employee {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: 'admin' | 'maintenance' | 'user';
|
||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
||||
contractType: 'small' | 'large';
|
||||
canWorkAlone: boolean;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
lastLogin?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateEmployeeRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
role: 'admin' | 'maintenance' | 'user';
|
||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
||||
contractType: 'small' | 'large';
|
||||
canWorkAlone: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateEmployeeRequest {
|
||||
name?: string;
|
||||
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[];
|
||||
}
|
||||
105
frontend/src/models/ShiftPlan.ts
Normal file
105
frontend/src/models/ShiftPlan.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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;
|
||||
isTemplate?: boolean;
|
||||
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'>[];
|
||||
}
|
||||
|
||||
export interface UpdateRequiredEmployeesRequest {
|
||||
requiredEmployees: number;
|
||||
}
|
||||
108
frontend/src/models/defaults/employeeDefaults.ts
Normal file
108
frontend/src/models/defaults/employeeDefaults.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
export const ROLE_CONFIG = [
|
||||
{ value: 'user', label: 'Mitarbeiter', description: 'Kann eigene Schichten einsehen', color: '#27ae60' },
|
||||
{ value: 'instandhalter', label: 'Instandhalter', description: 'Kann Schichtpläne erstellen und Mitarbeiter verwalten', color: '#3498db' },
|
||||
{ value: 'admin', label: 'Administrator', description: 'Voller Zugriff auf alle Funktionen', color: '#e74c3c' }
|
||||
] as const;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
3
frontend/src/models/defaults/index.ts
Normal file
3
frontend/src/models/defaults/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// backend/src/models/defaults/index.ts
|
||||
export * from './employeeDefaults.js';
|
||||
export * from './shiftPlanDefaults.js';
|
||||
146
frontend/src/models/defaults/shiftPlanDefaults.ts
Normal file
146
frontend/src/models/defaults/shiftPlanDefaults.ts
Normal 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;
|
||||
40
frontend/src/models/helpers/employeeHelpers.ts
Normal file
40
frontend/src/models/helpers/employeeHelpers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// backend/src/models/helpers/employeeHelpers.ts
|
||||
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
|
||||
|
||||
// Simplified validation - use schema validation instead
|
||||
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!employee.email?.includes('@')) {
|
||||
errors.push('Valid email is required');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Simplified business logic helpers
|
||||
export const isManager = (employee: Employee): boolean =>
|
||||
employee.employeeType === 'manager';
|
||||
|
||||
export const isTrainee = (employee: Employee): boolean =>
|
||||
employee.employeeType === 'trainee';
|
||||
|
||||
export const isExperienced = (employee: Employee): boolean =>
|
||||
employee.employeeType === 'experienced';
|
||||
|
||||
export const isAdmin = (employee: Employee): boolean =>
|
||||
employee.role === 'admin';
|
||||
|
||||
export const canEmployeeWorkAlone = (employee: Employee): boolean =>
|
||||
employee.canWorkAlone && isExperienced(employee);
|
||||
|
||||
export const getEmployeeWorkHours = (employee: Employee): number =>
|
||||
isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
|
||||
3
frontend/src/models/helpers/index.ts
Normal file
3
frontend/src/models/helpers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// backend/src/models/helpers/index.ts
|
||||
export * from './employeeHelpers.js';
|
||||
export * from './shiftPlanHelpers.js';
|
||||
118
frontend/src/models/helpers/shiftPlanHelpers.ts
Normal file
118
frontend/src/models/helpers/shiftPlanHelpers.ts
Normal 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user