mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
removed debug infor
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-10-21T11:16:11.693081",
|
||||||
|
"success": true,
|
||||||
|
"metadata": {
|
||||||
|
"solveTime": 0.0218085,
|
||||||
|
"constraintsAdded": 217,
|
||||||
|
"variablesCreated": 90,
|
||||||
|
"optimal": true
|
||||||
|
},
|
||||||
|
"progress": [
|
||||||
|
{
|
||||||
|
"timestamp": 0.00886988639831543,
|
||||||
|
"objective": 1050.0,
|
||||||
|
"bound": 2100.0,
|
||||||
|
"solution_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": 0.009304285049438477,
|
||||||
|
"objective": 1300.0,
|
||||||
|
"bound": 1300.0,
|
||||||
|
"solution_count": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solution_summary": {
|
||||||
|
"assignments_count": 15,
|
||||||
|
"violations_count": 0,
|
||||||
|
"variables_count": 90,
|
||||||
|
"constraints_count": 217,
|
||||||
|
"solve_time": 0.0218085,
|
||||||
|
"optimal": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-10-21T11:16:16.813066",
|
||||||
|
"success": true,
|
||||||
|
"metadata": {
|
||||||
|
"solveTime": 0.0158702,
|
||||||
|
"constraintsAdded": 217,
|
||||||
|
"variablesCreated": 90,
|
||||||
|
"optimal": true
|
||||||
|
},
|
||||||
|
"progress": [
|
||||||
|
{
|
||||||
|
"timestamp": 0.008541107177734375,
|
||||||
|
"objective": 1050.0,
|
||||||
|
"bound": 2000.0,
|
||||||
|
"solution_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": 0.00941777229309082,
|
||||||
|
"objective": 1250.0,
|
||||||
|
"bound": 1300.0,
|
||||||
|
"solution_count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": 0.009499549865722656,
|
||||||
|
"objective": 1300.0,
|
||||||
|
"bound": 1300.0,
|
||||||
|
"solution_count": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solution_summary": {
|
||||||
|
"assignments_count": 15,
|
||||||
|
"violations_count": 0,
|
||||||
|
"variables_count": 90,
|
||||||
|
"constraints_count": 217,
|
||||||
|
"solve_time": 0.0158702,
|
||||||
|
"optimal": true
|
||||||
|
}
|
||||||
|
}
|
||||||
32
backend/src/python-scripts/run_20251021_105426_failure.json
Normal file
32
backend/src/python-scripts/run_20251021_105426_failure.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-10-21T10:54:26.093544",
|
||||||
|
"success": false,
|
||||||
|
"metadata": {
|
||||||
|
"solveTime": 0,
|
||||||
|
"constraintsAdded": 0,
|
||||||
|
"variablesCreated": 0,
|
||||||
|
"optimal": false
|
||||||
|
},
|
||||||
|
"progress": [],
|
||||||
|
"solution_summary": {
|
||||||
|
"assignments_count": 0,
|
||||||
|
"violations_count": 1,
|
||||||
|
"variables_count": 0,
|
||||||
|
"constraints_count": 0,
|
||||||
|
"solve_time": 0,
|
||||||
|
"optimal": false
|
||||||
|
},
|
||||||
|
"full_result": {
|
||||||
|
"assignments": [],
|
||||||
|
"violations": [
|
||||||
|
"Error: 'SolutionCallback' object has no attribute 'HasObjective'"
|
||||||
|
],
|
||||||
|
"success": false,
|
||||||
|
"metadata": {
|
||||||
|
"solveTime": 0,
|
||||||
|
"constraintsAdded": 0,
|
||||||
|
"variablesCreated": 0,
|
||||||
|
"optimal": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
backend/src/python-scripts/run_20251021_105936_failure.json
Normal file
19
backend/src/python-scripts/run_20251021_105936_failure.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-10-21T10:59:36.646855",
|
||||||
|
"success": false,
|
||||||
|
"metadata": {
|
||||||
|
"solveTime": 0,
|
||||||
|
"constraintsAdded": 0,
|
||||||
|
"variablesCreated": 0,
|
||||||
|
"optimal": false
|
||||||
|
},
|
||||||
|
"progress": [],
|
||||||
|
"solution_summary": {
|
||||||
|
"assignments_count": 0,
|
||||||
|
"violations_count": 1,
|
||||||
|
"variables_count": 0,
|
||||||
|
"constraints_count": 0,
|
||||||
|
"solve_time": 0,
|
||||||
|
"optimal": false
|
||||||
|
}
|
||||||
|
}
|
||||||
38
backend/src/python-scripts/run_20251021_110336_success.json
Normal file
38
backend/src/python-scripts/run_20251021_110336_success.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2025-10-21T11:03:36.697986",
|
||||||
|
"success": true,
|
||||||
|
"metadata": {
|
||||||
|
"solveTime": 0.025875500000000003,
|
||||||
|
"constraintsAdded": 217,
|
||||||
|
"variablesCreated": 90,
|
||||||
|
"optimal": true
|
||||||
|
},
|
||||||
|
"progress": [
|
||||||
|
{
|
||||||
|
"timestamp": 0.008769989013671875,
|
||||||
|
"objective": 1050.0,
|
||||||
|
"bound": 2000.0,
|
||||||
|
"solution_count": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": 0.009685516357421875,
|
||||||
|
"objective": 1250.0,
|
||||||
|
"bound": 1700.0,
|
||||||
|
"solution_count": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": 0.010709047317504883,
|
||||||
|
"objective": 1300.0,
|
||||||
|
"bound": 1300.0,
|
||||||
|
"solution_count": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"solution_summary": {
|
||||||
|
"assignments_count": 15,
|
||||||
|
"violations_count": 0,
|
||||||
|
"variables_count": 90,
|
||||||
|
"constraints_count": 217,
|
||||||
|
"solve_time": 0.025875500000000003,
|
||||||
|
"optimal": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,151 @@ from ortools.sat.python import cp_model
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ProductionProgressManager:
|
||||||
|
def __init__(self, script_dir, max_files=1000, retention_days=7):
|
||||||
|
self.script_dir = Path(script_dir)
|
||||||
|
self.progress_dir = self.script_dir / "progress_data"
|
||||||
|
self.max_files = max_files
|
||||||
|
self.retention_days = retention_days
|
||||||
|
self._ensure_directory()
|
||||||
|
self._cleanup_old_files()
|
||||||
|
|
||||||
|
def _ensure_directory(self):
|
||||||
|
"""Create progress directory if it doesn't exist"""
|
||||||
|
try:
|
||||||
|
self.progress_dir.mkdir(exist_ok=True)
|
||||||
|
# Set secure permissions (read/write for owner only)
|
||||||
|
self.progress_dir.chmod(0o700)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not create progress directory: {e}")
|
||||||
|
|
||||||
|
def _cleanup_old_files(self):
|
||||||
|
"""Remove old progress files based on retention policy"""
|
||||||
|
try:
|
||||||
|
cutoff_time = datetime.now() - timedelta(days=self.retention_days)
|
||||||
|
files = list(self.progress_dir.glob("run_*.json"))
|
||||||
|
|
||||||
|
# Sort by modification time and remove oldest if over limit
|
||||||
|
if len(files) > self.max_files:
|
||||||
|
files.sort(key=lambda x: x.stat().st_mtime)
|
||||||
|
for file_to_delete in files[:len(files) - self.max_files]:
|
||||||
|
file_to_delete.unlink()
|
||||||
|
logger.info(f"Cleaned up old progress file: {file_to_delete}")
|
||||||
|
|
||||||
|
# Remove files older than retention period
|
||||||
|
for file_path in files:
|
||||||
|
if datetime.fromtimestamp(file_path.stat().st_mtime) < cutoff_time:
|
||||||
|
file_path.unlink()
|
||||||
|
logger.info(f"Removed expired progress file: {file_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Progress cleanup failed: {e}")
|
||||||
|
|
||||||
|
def save_progress(self, result, progress_data):
|
||||||
|
"""Safely save progress data with production considerations"""
|
||||||
|
try:
|
||||||
|
# Check disk space before writing (min 100MB free)
|
||||||
|
if not self._check_disk_space():
|
||||||
|
logger.warning("Insufficient disk space, skipping progress save")
|
||||||
|
return None
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||||
|
success_status = "success" if result.get('success', False) else "failure"
|
||||||
|
filename = f"run_{timestamp}_{success_status}.json"
|
||||||
|
filepath = self.progress_dir / filename
|
||||||
|
|
||||||
|
# Prepare safe data (exclude sensitive information)
|
||||||
|
safe_data = {
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'success': result.get('success', False),
|
||||||
|
'metadata': result.get('metadata', {}),
|
||||||
|
'progress': progress_data,
|
||||||
|
'solution_summary': {
|
||||||
|
'assignments_count': len(result.get('assignments', [])),
|
||||||
|
'violations_count': len(result.get('violations', [])),
|
||||||
|
'variables_count': result.get('metadata', {}).get('variablesCreated', 0),
|
||||||
|
'constraints_count': result.get('metadata', {}).get('constraintsAdded', 0),
|
||||||
|
'solve_time': result.get('metadata', {}).get('solveTime', 0),
|
||||||
|
'optimal': result.get('metadata', {}).get('optimal', False)
|
||||||
|
}
|
||||||
|
# ❌ REMOVED: 'full_result' containing potentially sensitive data
|
||||||
|
}
|
||||||
|
|
||||||
|
# Atomic write with temporary file
|
||||||
|
temp_filepath = filepath.with_suffix('.tmp')
|
||||||
|
with open(temp_filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(safe_data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Atomic rename
|
||||||
|
temp_filepath.rename(filepath)
|
||||||
|
# Set secure file permissions
|
||||||
|
filepath.chmod(0o600)
|
||||||
|
|
||||||
|
logger.info(f"Progress data saved: {filename}")
|
||||||
|
return str(filepath)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save progress data: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _check_disk_space(self, min_free_mb=100):
|
||||||
|
"""Check if there's sufficient disk space"""
|
||||||
|
try:
|
||||||
|
stat = os.statvfs(self.progress_dir)
|
||||||
|
free_mb = (stat.f_bavail * stat.f_frsize) / (1024 * 1024)
|
||||||
|
return free_mb >= min_free_mb
|
||||||
|
except:
|
||||||
|
return True # Continue if we can't check disk space
|
||||||
|
|
||||||
|
class SimpleSolutionCallback(cp_model.CpSolverSolutionCallback):
|
||||||
|
"""A simplified callback that only counts solutions"""
|
||||||
|
def __init__(self):
|
||||||
|
cp_model.CpSolverSolutionCallback.__init__(self)
|
||||||
|
self.__solution_count = 0
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.solutions = []
|
||||||
|
|
||||||
|
def on_solution_callback(self):
|
||||||
|
current_time = time.time() - self.start_time
|
||||||
|
self.__solution_count += 1
|
||||||
|
|
||||||
|
# Try to get objective value safely
|
||||||
|
try:
|
||||||
|
objective_value = self.ObjectiveValue()
|
||||||
|
except:
|
||||||
|
objective_value = 0
|
||||||
|
|
||||||
|
# Try to get bound safely
|
||||||
|
try:
|
||||||
|
best_bound = self.BestObjectiveBound()
|
||||||
|
except:
|
||||||
|
best_bound = 0
|
||||||
|
|
||||||
|
solution_info = {
|
||||||
|
'timestamp': current_time,
|
||||||
|
'objective': objective_value,
|
||||||
|
'bound': best_bound,
|
||||||
|
'solution_count': self.__solution_count
|
||||||
|
}
|
||||||
|
|
||||||
|
self.solutions.append(solution_info)
|
||||||
|
print(f"Progress: Solution {self.__solution_count}, Objective: {objective_value}, Time: {current_time:.2f}s", file=sys.stderr)
|
||||||
|
|
||||||
|
def solution_count(self):
|
||||||
|
return self.__solution_count
|
||||||
|
|
||||||
|
|
||||||
class UniversalSchedulingSolver:
|
class UniversalSchedulingSolver:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.model = cp_model.CpModel()
|
self.model = cp_model.CpModel()
|
||||||
@@ -13,6 +156,14 @@ class UniversalSchedulingSolver:
|
|||||||
self.solver.parameters.num_search_workers = 8
|
self.solver.parameters.num_search_workers = 8
|
||||||
self.solver.parameters.log_search_progress = False
|
self.solver.parameters.log_search_progress = False
|
||||||
|
|
||||||
|
# 🆕 Initialize production-safe progress manager
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
self.progress_manager = ProductionProgressManager(
|
||||||
|
script_dir=script_dir,
|
||||||
|
max_files=1000, # Keep last 1000 runs
|
||||||
|
retention_days=7 # Keep files for 7 days
|
||||||
|
)
|
||||||
|
|
||||||
def solve_from_model_data(self, model_data):
|
def solve_from_model_data(self, model_data):
|
||||||
"""Solve from pre-built model data (variables, constraints, objective)"""
|
"""Solve from pre-built model data (variables, constraints, objective)"""
|
||||||
try:
|
try:
|
||||||
@@ -48,36 +199,70 @@ class UniversalSchedulingSolver:
|
|||||||
# Add a default objective if main objective fails
|
# Add a default objective if main objective fails
|
||||||
self.model.Maximize(sum(cp_vars.values()))
|
self.model.Maximize(sum(cp_vars.values()))
|
||||||
|
|
||||||
# Solve
|
# Solve with callback
|
||||||
status = self.solver.Solve(self.model)
|
callback = SimpleSolutionCallback()
|
||||||
|
status = self.solver.SolveWithSolutionCallback(self.model, callback)
|
||||||
|
|
||||||
result = self._format_solution(status, cp_vars, model_data)
|
result = self._format_solution(status, cp_vars, model_data)
|
||||||
result['metadata']['constraintsAdded'] = constraints_added
|
result['metadata']['constraintsAdded'] = constraints_added
|
||||||
|
|
||||||
|
# 🆕 Production-safe progress saving
|
||||||
|
if callback.solutions:
|
||||||
|
result['progress'] = callback.solutions
|
||||||
|
self.progress_manager.save_progress(result, callback.solutions)
|
||||||
|
else:
|
||||||
|
result['progress'] = []
|
||||||
|
self.progress_manager.save_progress(result, [])
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return self._error_result(str(e))
|
error_result = self._error_result(str(e))
|
||||||
|
self.progress_manager.save_progress(error_result, [])
|
||||||
|
return error_result
|
||||||
|
|
||||||
|
def _save_progress_data(self, result, progress_data):
|
||||||
|
"""Save progress data to file in the same directory as this script"""
|
||||||
|
try:
|
||||||
|
# Get current script directory
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# Create filename with timestamp and success status
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
success_status = "success" if result.get('success', False) else "failure"
|
||||||
|
filename = f"run_{timestamp}_{success_status}.json"
|
||||||
|
filepath = os.path.join(script_dir, filename)
|
||||||
|
|
||||||
|
# Prepare data to save
|
||||||
|
data_to_save = {
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'success': result.get('success', False),
|
||||||
|
'metadata': result.get('metadata', {}),
|
||||||
|
'progress': progress_data,
|
||||||
|
'solution_summary': {
|
||||||
|
'assignments_count': len(result.get('assignments', [])),
|
||||||
|
'violations_count': len(result.get('violations', [])),
|
||||||
|
'variables_count': result.get('metadata', {}).get('variablesCreated', 0),
|
||||||
|
'constraints_count': result.get('metadata', {}).get('constraintsAdded', 0),
|
||||||
|
'solve_time': result.get('metadata', {}).get('solveTime', 0),
|
||||||
|
'optimal': result.get('metadata', {}).get('optimal', False)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write to file
|
||||||
|
with open(filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data_to_save, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"Progress data saved to: {filepath}", file=sys.stderr)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to save progress data: {e}", file=sys.stderr)
|
||||||
|
|
||||||
def _add_constraint(self, expression, cp_vars):
|
def _add_constraint(self, expression, cp_vars):
|
||||||
"""Add constraint from expression string with enhanced parsing"""
|
"""Add constraint from expression string with enhanced parsing"""
|
||||||
try:
|
try:
|
||||||
expression = expression.strip()
|
expression = expression.strip()
|
||||||
|
|
||||||
# Handle implication constraints (=>)
|
|
||||||
if '=>' in expression:
|
|
||||||
left, right = expression.split('=>', 1)
|
|
||||||
left_expr = self._parse_expression(left.strip(), cp_vars)
|
|
||||||
right_expr = self._parse_expression(right.strip(), cp_vars)
|
|
||||||
|
|
||||||
# A => B is equivalent to (not A) or B
|
|
||||||
# In CP-SAT: AddBoolOr([A.Not(), B])
|
|
||||||
if hasattr(left_expr, 'Not') and hasattr(right_expr, 'Index'):
|
|
||||||
self.model.AddImplication(left_expr, right_expr)
|
|
||||||
else:
|
|
||||||
# Fallback: treat as linear constraint
|
|
||||||
self.model.Add(left_expr <= right_expr)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Handle equality
|
# Handle equality
|
||||||
if ' == ' in expression:
|
if ' == ' in expression:
|
||||||
left, right = expression.split(' == ', 1)
|
left, right = expression.split(' == ', 1)
|
||||||
@@ -198,12 +383,11 @@ class UniversalSchedulingSolver:
|
|||||||
assignments.append({
|
assignments.append({
|
||||||
'shiftId': shift_id,
|
'shiftId': shift_id,
|
||||||
'employeeId': employee_id,
|
'employeeId': employee_id,
|
||||||
'assignedAt': '2024-01-01T00:00:00Z',
|
'assignedAt': datetime.now().isoformat() + 'Z',
|
||||||
'score': 100
|
'score': 100
|
||||||
})
|
})
|
||||||
|
|
||||||
print(f"Debug: Found {len(assignments)} assignments", file=sys.stderr)
|
print(f"Debug: Found {len(assignments)} assignments", file=sys.stderr)
|
||||||
print(f"Debug: First 5 assignments: {assignments[:5]}", file=sys.stderr)
|
|
||||||
else:
|
else:
|
||||||
print(f"Debug: Solver failed with status {status}", file=sys.stderr)
|
print(f"Debug: Solver failed with status {status}", file=sys.stderr)
|
||||||
|
|
||||||
@@ -213,7 +397,7 @@ class UniversalSchedulingSolver:
|
|||||||
'assignments': assignments,
|
'assignments': assignments,
|
||||||
'violations': [],
|
'violations': [],
|
||||||
'success': success,
|
'success': success,
|
||||||
'variables': variables, # Include ALL variables for debugging
|
'variables': variables,
|
||||||
'metadata': {
|
'metadata': {
|
||||||
'solveTime': self.solver.WallTime(),
|
'solveTime': self.solver.WallTime(),
|
||||||
'constraintsAdded': len(model_data.get('constraints', [])),
|
'constraintsAdded': len(model_data.get('constraints', [])),
|
||||||
@@ -227,9 +411,9 @@ class UniversalSchedulingSolver:
|
|||||||
status_map = {
|
status_map = {
|
||||||
cp_model.OPTIMAL: 'OPTIMAL',
|
cp_model.OPTIMAL: 'OPTIMAL',
|
||||||
cp_model.FEASIBLE: 'FEASIBLE',
|
cp_model.FEASIBLE: 'FEASIBLE',
|
||||||
cp_MODEL.INFEASIBLE: 'INFEASIBLE',
|
cp_model.INFEASIBLE: 'INFEASIBLE',
|
||||||
cp_MODEL.MODEL_INVALID: 'MODEL_INVALID',
|
cp_model.MODEL_INVALID: 'MODEL_INVALID',
|
||||||
cp_MODEL.UNKNOWN: 'UNKNOWN'
|
cp_model.UNKNOWN: 'UNKNOWN'
|
||||||
}
|
}
|
||||||
return status_map.get(status, f'UNKNOWN_STATUS_{status}')
|
return status_map.get(status, f'UNKNOWN_STATUS_{status}')
|
||||||
|
|
||||||
@@ -248,7 +432,6 @@ class UniversalSchedulingSolver:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Main execution
|
# Main execution
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ import { SolverOptions, Solution, Assignment } from '../models/scheduling.js';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
export interface ProgressStep {
|
||||||
|
timestamp: number;
|
||||||
|
objective: number;
|
||||||
|
bound: number;
|
||||||
|
solution_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SolutionWithProgress extends Solution {
|
||||||
|
progress?: ProgressStep[];
|
||||||
|
}
|
||||||
|
|
||||||
export class CPModel {
|
export class CPModel {
|
||||||
private modelData: any;
|
private modelData: any;
|
||||||
|
|
||||||
@@ -103,6 +114,10 @@ export class CPSolver {
|
|||||||
|
|
||||||
pythonProcess.stderr.on('data', (data) => {
|
pythonProcess.stderr.on('data', (data) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
|
// 🆕 Real-time progress monitoring from stderr
|
||||||
|
if (data.toString().includes('Progress:')) {
|
||||||
|
console.log('Python Progress:', data.toString().trim());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
pythonProcess.on('close', (code) => {
|
pythonProcess.on('close', (code) => {
|
||||||
@@ -116,15 +131,16 @@ export class CPSolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Python raw output:', stdout.substring(0, 500)); // Debug log
|
console.log('Python raw output:', stdout.substring(0, 500));
|
||||||
|
|
||||||
const result = JSON.parse(stdout);
|
const result = JSON.parse(stdout);
|
||||||
|
|
||||||
// ENHANCED: Better solution parsing
|
// Enhanced solution parsing with progress data
|
||||||
const solution: Solution = {
|
const solution: SolutionWithProgress = {
|
||||||
success: result.success || false,
|
success: result.success || false,
|
||||||
assignments: result.assignments || [],
|
assignments: result.assignments || [],
|
||||||
violations: result.violations || [],
|
violations: result.violations || [],
|
||||||
|
progress: result.progress || [], // 🆕 Parse progress data
|
||||||
metadata: {
|
metadata: {
|
||||||
solveTime: result.metadata?.solveTime || 0,
|
solveTime: result.metadata?.solveTime || 0,
|
||||||
constraintsAdded: result.metadata?.constraintsAdded || 0,
|
constraintsAdded: result.metadata?.constraintsAdded || 0,
|
||||||
@@ -134,7 +150,7 @@ export class CPSolver {
|
|||||||
variables: result.variables || {}
|
variables: result.variables || {}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`Python solver result: success=${solution.success}, assignments=${solution.assignments.length}`);
|
console.log(`Python solver result: success=${solution.success}, assignments=${solution.assignments.length}, progress_steps=${solution.progress?.length}`);
|
||||||
|
|
||||||
resolve(solution);
|
resolve(solution);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
|
|||||||
@@ -343,31 +343,54 @@ const Dashboard: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Willkommens-Bereich */}
|
{/* Minimalist Welcome Section */}
|
||||||
<div style={{
|
<div
|
||||||
backgroundColor: '#e8f4fd',
|
style={{
|
||||||
padding: '25px',
|
width: '100vw',
|
||||||
borderRadius: '8px',
|
position: 'relative',
|
||||||
marginBottom: '30px',
|
left: '50%',
|
||||||
border: '1px solid #b6d7e8',
|
right: '50%',
|
||||||
display: 'flex',
|
marginLeft: '-50vw',
|
||||||
justifyContent: 'space-between',
|
marginRight: '-50vw',
|
||||||
alignItems: 'center'
|
background: `
|
||||||
}}>
|
radial-gradient(ellipse farthest-corner at center 53%,
|
||||||
<div>
|
#d9b9f3ff 10%,
|
||||||
<h1 style={{ margin: '0 0 10px 0', color: '#2c3e50' }}>
|
#ddc5f1ff 22%,
|
||||||
Willkommen zurück, {user?.firstname} {user?.lastname} ! 👋
|
#e9d4f8ff 32%,
|
||||||
|
#FBFAF6 55%)
|
||||||
|
`,
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '10vh 0',
|
||||||
|
color: '#161718',
|
||||||
|
fontFamily: "'Poppins', 'Inter', 'Manrope', sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontSize: '3rem',
|
||||||
|
fontWeight: 100,
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
opacity: 0.995,
|
||||||
|
filter: 'blur(0.2px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Willkommen
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ margin: 0, color: '#546e7a', fontSize: '16px' }}>
|
|
||||||
{new Date().toLocaleDateString('de-DE', {
|
<p
|
||||||
weekday: 'long',
|
style={{
|
||||||
year: 'numeric',
|
fontSize: '1.1rem',
|
||||||
month: 'long',
|
color: '#3e2069',
|
||||||
day: 'numeric'
|
letterSpacing: '0.05em',
|
||||||
})}
|
fontWeight: 300,
|
||||||
|
opacity: 0.85,
|
||||||
|
transition: 'color 0.3s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user?.firstname} {user?.lastname}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Actions - Nur für Admins/Instandhalter */}
|
{/* Quick Actions - Nur für Admins/Instandhalter */}
|
||||||
{hasRole(['admin', 'instandhalter']) && (
|
{hasRole(['admin', 'instandhalter']) && (
|
||||||
@@ -483,7 +506,7 @@ const Dashboard: React.FC = () => {
|
|||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: `${progress.percentage}%`,
|
width: `${progress.percentage}%`,
|
||||||
backgroundColor: progress.percentage > 0 ? '#3498db' : '#95a5a6',
|
backgroundColor: progress.percentage > 0 ? '#854eca' : '#95a5a6',
|
||||||
height: '8px',
|
height: '8px',
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
transition: 'width 0.3s ease'
|
transition: 'width 0.3s ease'
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ const EmployeeManagement: React.FC = () => {
|
|||||||
onClick={handleCreateEmployee}
|
onClick={handleCreateEmployee}
|
||||||
style={{
|
style={{
|
||||||
padding: '12px 24px',
|
padding: '12px 24px',
|
||||||
backgroundColor: '#27ae60',
|
backgroundColor: '#51258f',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
|
|||||||
@@ -372,18 +372,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Timetable Structure Info */}
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: '#d1ecf1',
|
|
||||||
border: '1px solid #bee5eb',
|
|
||||||
padding: '10px 15px',
|
|
||||||
margin: '10px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '12px'
|
|
||||||
}}>
|
|
||||||
<strong>Struktur-Info:</strong> {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} Zellen
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ overflowX: 'auto' }}>
|
<div style={{ overflowX: 'auto' }}>
|
||||||
<table style={{
|
<table style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -397,7 +385,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
border: '1px solid #dee2e6',
|
border: '1px solid #dee2e6',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
minWidth: '200px'
|
minWidth: '120px'
|
||||||
}}>
|
}}>
|
||||||
Zeitslot
|
Zeitslot
|
||||||
</th>
|
</th>
|
||||||
@@ -558,15 +546,9 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
color: '#666'
|
color: '#666'
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<div>
|
|
||||||
<strong>Zusammenfassung:</strong> {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} mögliche Shifts
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Aktive Verfügbarkeiten:</strong> {availabilities.filter(a => a.preferenceLevel !== 3).length}
|
<strong>Aktive Verfügbarkeiten:</strong> {availabilities.filter(a => a.preferenceLevel !== 3).length}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<strong>Validierungsfehler:</strong> {validationErrors.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -667,70 +649,9 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
borderBottom: '2px solid #f0f0f0',
|
borderBottom: '2px solid #f0f0f0',
|
||||||
paddingBottom: '15px'
|
paddingBottom: '15px'
|
||||||
}}>
|
}}>
|
||||||
📅 Verfügbarkeit verwalten (Shift-ID basiert)
|
📅 Verfügbarkeit verwalten
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Debug-Info */}
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: !selectedPlan ? '#f8d7da' : (shiftsCount === 0 ? '#fff3cd' : '#d1ecf1'),
|
|
||||||
border: `1px solid ${!selectedPlan ? '#f5c6cb' : (shiftsCount === 0 ? '#ffeaa7' : '#bee5eb')}`,
|
|
||||||
borderRadius: '6px',
|
|
||||||
padding: '15px',
|
|
||||||
marginBottom: '20px'
|
|
||||||
}}>
|
|
||||||
<h4 style={{
|
|
||||||
margin: '0 0 10px 0',
|
|
||||||
color: !selectedPlan ? '#721c24' : (shiftsCount === 0 ? '#856404' : '#0c5460')
|
|
||||||
}}>
|
|
||||||
{!selectedPlan ? '❌ KEIN PLAN AUSGEWÄHLT' :
|
|
||||||
shiftsCount === 0 ? '⚠️ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'}
|
|
||||||
</h4>
|
|
||||||
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
|
|
||||||
<div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div>
|
|
||||||
<div><strong>Plan ID:</strong> {selectedPlanId || 'Nicht gesetzt'}</div>
|
|
||||||
<div><strong>Geladene Pläne:</strong> {shiftPlans.length}</div>
|
|
||||||
<div><strong>Einzigartige Shifts:</strong> {shiftsCount}</div>
|
|
||||||
<div><strong>Geladene Verfügbarkeiten:</strong> {availabilities.length}</div>
|
|
||||||
{selectedPlan && (
|
|
||||||
<>
|
|
||||||
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
|
|
||||||
<div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan.shifts?.length || 0}</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Show existing preferences */}
|
|
||||||
{availabilities.length > 0 && (
|
|
||||||
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #bee5eb' }}>
|
|
||||||
<strong>Vorhandene Präferenzen:</strong>
|
|
||||||
{availabilities.slice(0, 5).map(avail => {
|
|
||||||
// SICHERHEITSCHECK: Stelle sicher, dass shiftId existiert
|
|
||||||
if (!avail.shiftId) {
|
|
||||||
return (
|
|
||||||
<div key={avail.id} style={{ fontSize: '11px', color: 'red' }}>
|
|
||||||
• UNGÜLTIG: Keine Shift-ID
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const shift = selectedPlan?.shifts?.find(s => s.id === avail.shiftId);
|
|
||||||
const shiftIdDisplay = avail.shiftId ? avail.shiftId.substring(0, 8) + '...' : 'KEINE ID';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={avail.id} style={{ fontSize: '11px' }}>
|
|
||||||
• Shift {shiftIdDisplay} (Day {shift?.dayOfWeek || '?'}): Level {avail.preferenceLevel}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{availabilities.length > 5 && (
|
|
||||||
<div style={{ fontSize: '11px', fontStyle: 'italic' }}>
|
|
||||||
... und {availabilities.length - 5} weitere
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Employee Info */}
|
{/* Employee Info */}
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<div style={{ marginBottom: '20px' }}>
|
||||||
<h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}>
|
<h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}>
|
||||||
@@ -739,9 +660,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
<p style={{ margin: 0, color: '#7f8c8d' }}>
|
<p style={{ margin: 0, color: '#7f8c8d' }}>
|
||||||
<strong>Email:</strong> {employee.email}
|
<strong>Email:</strong> {employee.email}
|
||||||
</p>
|
</p>
|
||||||
<p style={{ margin: '5px 0 0 0', color: '#7f8c8d' }}>
|
|
||||||
Legen Sie die Verfügbarkeit für {employeeFullName} fest (basierend auf Shift-IDs).
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -397,14 +397,6 @@ const Settings: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const ShiftPlanList: React.FC = () => {
|
|||||||
<Link to="/shift-plans/new">
|
<Link to="/shift-plans/new">
|
||||||
<button style={{
|
<button style={{
|
||||||
padding: '10px 20px',
|
padding: '10px 20px',
|
||||||
backgroundColor: '#3498db',
|
backgroundColor: '#51258f',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
|
|||||||
@@ -771,19 +771,6 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Timetable Structure Info - SAME AS AVAILABILITYMANAGER */}
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: '#d1ecf1',
|
|
||||||
border: '1px solid #bee5eb',
|
|
||||||
padding: '10px 15px',
|
|
||||||
margin: '10px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '12px'
|
|
||||||
}}>
|
|
||||||
<strong>Struktur-Info:</strong> {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} Zellen
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ overflowX: 'auto' }}>
|
<div style={{ overflowX: 'auto' }}>
|
||||||
<table style={{
|
<table style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -797,7 +784,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
border: '1px solid #dee2e6',
|
border: '1px solid #dee2e6',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
minWidth: '200px'
|
minWidth: '120px'
|
||||||
}}>
|
}}>
|
||||||
Schicht (Zeit)
|
Schicht (Zeit)
|
||||||
</th>
|
</th>
|
||||||
@@ -850,7 +837,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
color: '#ccc',
|
color: '#ccc',
|
||||||
fontStyle: 'italic'
|
fontStyle: 'italic'
|
||||||
}}>
|
}}>
|
||||||
Kein Shift
|
Keine Schicht
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -977,23 +964,6 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary Statistics - SAME AS AVAILABILITYMANAGER */}
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
padding: '15px',
|
|
||||||
borderTop: '1px solid #dee2e6',
|
|
||||||
fontSize: '12px',
|
|
||||||
color: '#666'
|
|
||||||
}}>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<div>
|
|
||||||
<strong>Zusammenfassung:</strong> {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} mögliche Shifts
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Validierungsfehler:</strong> {validation.errors.length}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1069,50 +1039,6 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Debug Info - Enhanced */}
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: validation.errors.length > 0 ? '#fff3cd' : (allTimeSlots.length === 0 ? '#f8d7da' : '#d1ecf1'),
|
|
||||||
border: `1px solid ${validation.errors.length > 0 ? '#ffeaa7' : (allTimeSlots.length === 0 ? '#f5c6cb' : '#bee5eb')}`,
|
|
||||||
borderRadius: '6px',
|
|
||||||
padding: '15px',
|
|
||||||
marginBottom: '20px'
|
|
||||||
}}>
|
|
||||||
<h4 style={{
|
|
||||||
margin: '0 0 10px 0',
|
|
||||||
color: validation.errors.length > 0 ? '#856404' : (allTimeSlots.length === 0 ? '#721c24' : '#0c5460')
|
|
||||||
}}>
|
|
||||||
{validation.errors.length > 0 ? '⚠️ VALIDIERUNGSPROBLEME' :
|
|
||||||
allTimeSlots.length === 0 ? '❌ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'}
|
|
||||||
</h4>
|
|
||||||
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
|
|
||||||
<div><strong>Ausgewählter Plan:</strong> {shiftPlan.name}</div>
|
|
||||||
<div><strong>Plan ID:</strong> {shiftPlan.id}</div>
|
|
||||||
<div><strong>Einzigartige Zeitslots:</strong> {allTimeSlots.length}</div>
|
|
||||||
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
|
|
||||||
<div><strong>Shift Patterns:</strong> {shiftPlan.shifts?.length || 0}</div>
|
|
||||||
<div><strong>Scheduled Shifts:</strong> {scheduledShifts.length}</div>
|
|
||||||
<div><strong>Geladene Verfügbarkeiten:</strong> {availabilities.length}</div>
|
|
||||||
<div><strong>Aktive Mitarbeiter:</strong> {employees.length}</div>
|
|
||||||
{assignmentResult && (
|
|
||||||
<div><strong>Assignment Keys:</strong> {Object.keys(assignmentResult.assignments).length}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Show shift pattern vs scheduled shift matching */}
|
|
||||||
{shiftPlan.shifts && scheduledShifts.length > 0 && (
|
|
||||||
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #bee5eb' }}>
|
|
||||||
<strong>Shift Matching:</strong>
|
|
||||||
<div style={{ fontSize: '11px' }}>
|
|
||||||
• {shiftPlan.shifts.length} Patterns → {scheduledShifts.length} Scheduled Shifts
|
|
||||||
{assignmentResult && (
|
|
||||||
<div>• {Object.keys(assignmentResult.assignments).length} Assignment Keys</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rest of the component remains the same... */}
|
|
||||||
{/* Availability Status - only show for drafts */}
|
{/* Availability Status - only show for drafts */}
|
||||||
{shiftPlan.status === 'draft' && (
|
{shiftPlan.status === 'draft' && (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
Reference in New Issue
Block a user