HaLim commited on
Commit
131af7c
Β·
1 Parent(s): ac5a7da

Add streamlit

Browse files
app.py CHANGED
@@ -25,12 +25,12 @@ st.sidebar.markdown("---")
25
  # Navigation
26
  page = st.sidebar.selectbox(
27
  "Navigate to:",
28
- ["βš™οΈ Configuration", "πŸ“Š Optimization Results"],
29
  index=0
30
  )
31
 
32
  # Main app content
33
- if page == "βš™οΈ Configuration":
34
  # Import and render the config page
35
  from config_page import render_config_page
36
 
@@ -40,7 +40,5 @@ if page == "βš™οΈ Configuration":
40
  render_config_page()
41
 
42
  elif page == "πŸ“Š Optimization Results":
43
- # Import and render the optimization page
44
- from optimization_page import render_optimization_page
45
-
46
- render_optimization_page()
 
25
  # Navigation
26
  page = st.sidebar.selectbox(
27
  "Navigate to:",
28
+ ["βš™οΈ Settings", "πŸ“Š Optimization Results"],
29
  index=0
30
  )
31
 
32
  # Main app content
33
+ if page == "βš™οΈ Settings":
34
  # Import and render the config page
35
  from config_page import render_config_page
36
 
 
40
  render_config_page()
41
 
42
  elif page == "πŸ“Š Optimization Results":
43
+ st.title("πŸ“Š Optimization Results")
44
+ st.info("🚧 Optimization results page is under development. Please use the Settings page to configure your optimization parameters.")
 
 
config_page.py CHANGED
@@ -14,9 +14,9 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src'))
14
  def render_config_page():
15
  """Render the configuration page with all user input controls"""
16
 
17
- st.title("βš™οΈ Configuration")
18
  st.markdown("---")
19
- st.markdown("Configure optimization parameters and constraints for roster planning.")
20
 
21
  # Initialize session state for all configuration values
22
  initialize_session_state()
@@ -40,54 +40,101 @@ def render_config_page():
40
  st.markdown("---")
41
  col1, col2, col3 = st.columns([1, 1, 1])
42
  with col2:
43
- if st.button("πŸ’Ύ Save Configuration", type="primary", use_container_width=True):
44
- save_configuration()
45
- st.success("βœ… Configuration saved successfully!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  def initialize_session_state():
48
- """Initialize session state with default values from config"""
49
-
50
- # Default values based on optimization_config.py
51
- defaults = {
52
- # Schedule configuration
53
- 'start_date': datetime.date(2025, 7, 7),
54
- 'end_date': datetime.date(2025, 7, 11),
55
- 'schedule_type': 'daily',
56
-
57
- # Shift configuration
58
- 'evening_shift_mode': 'normal',
59
- 'evening_shift_threshold': 0.9,
60
-
61
- # Fixed staff configuration
62
- 'fixed_staff_mode': 'priority',
63
-
64
- # Payment configuration
65
- 'payment_mode_shift_1': 'bulk',
66
- 'payment_mode_shift_2': 'bulk',
67
- 'payment_mode_shift_3': 'partial',
68
-
69
- # Workforce limits
70
- 'max_unicef_per_day': 8,
71
- 'max_humanizer_per_day': 10,
72
-
73
- # Operations
74
- 'max_parallel_workers_long_line': 15,
75
- 'max_parallel_workers_mini_load': 15,
76
- 'max_hour_per_person_per_day': 14,
77
 
78
- # Shift hours
79
- 'max_hours_shift_1': 7.5,
80
- 'max_hours_shift_2': 7.5,
81
- 'max_hours_shift_3': 5.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- # Line counts (will be loaded from data)
84
- 'line_count_long_line': 6,
85
- 'line_count_mini_load': 7,
86
- }
87
 
88
- # Initialize session state with defaults if not already set
89
  for key, value in defaults.items():
90
- if key not in st.session_state:
91
  st.session_state[key] = value
92
 
93
  def render_schedule_config():
@@ -204,7 +251,7 @@ def render_workforce_config():
204
  "Max Hours - Shift 1 (Regular)",
205
  min_value=1.0,
206
  max_value=12.0,
207
- value=st.session_state.max_hours_shift_1,
208
  step=0.5,
209
  help="Maximum hours per person for regular shift"
210
  )
@@ -214,7 +261,7 @@ def render_workforce_config():
214
  "Max Hours - Shift 2 (Evening)",
215
  min_value=1.0,
216
  max_value=12.0,
217
- value=st.session_state.max_hours_shift_2,
218
  step=0.5,
219
  help="Maximum hours per person for evening shift"
220
  )
@@ -224,7 +271,7 @@ def render_workforce_config():
224
  "Max Hours - Shift 3 (Overtime)",
225
  min_value=1.0,
226
  max_value=12.0,
227
- value=st.session_state.max_hours_shift_3,
228
  step=0.5,
229
  help="Maximum hours per person for overtime shift"
230
  )
@@ -311,28 +358,148 @@ def render_cost_config():
311
  help="Payment mode for overtime shift"
312
  )
313
 
314
- # Cost information display (read-only from config)
315
- st.subheader("πŸ’΅ Hourly Rates (From Configuration)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
- # Display current cost configuration
318
- cost_info = """
319
- **UNICEF Fixed Term:**
320
- - Shift 1 (Regular): €43.27/hour
321
- - Shift 2 (Evening): €43.27/hour
322
- - Shift 3 (Overtime): €64.91/hour
 
 
 
 
323
 
324
- **Humanizer:**
325
- - Shift 1 (Regular): €27.94/hour
326
- - Shift 2 (Evening): €27.94/hour
327
- - Shift 3 (Overtime): €41.91/hour
328
- """
 
 
 
 
 
329
 
330
- st.info(cost_info)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
  def save_configuration():
333
  """Save current configuration to session state and potentially to file"""
334
 
335
- # Create configuration dictionary
336
  config = {
337
  'date_range': {
338
  'start_date': st.session_state.start_date,
@@ -361,19 +528,197 @@ def save_configuration():
361
  },
362
  'operations': {
363
  'line_counts': {
364
- 'long_line': st.session_state.line_count_long_line,
365
- 'mini_load': st.session_state.line_count_mini_load,
366
  },
367
  'max_parallel_workers': {
368
  6: st.session_state.max_parallel_workers_long_line, # long line id
369
  7: st.session_state.max_parallel_workers_mini_load, # mini load id
370
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  }
372
  }
373
 
374
- # Store in session state for other pages to access
 
 
 
 
 
 
 
 
 
375
  st.session_state.optimization_config = config
376
 
377
- # Display configuration summary
378
- with st.expander("πŸ“‹ Configuration Summary", expanded=False):
379
- st.json(config)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  def render_config_page():
15
  """Render the configuration page with all user input controls"""
16
 
17
+ st.title("βš™οΈ Settings")
18
  st.markdown("---")
19
+ st.markdown("Adjust the settings for your workforce optimization. These settings control how the system schedules employees and calculates costs.")
20
 
21
  # Initialize session state for all configuration values
22
  initialize_session_state()
 
40
  st.markdown("---")
41
  col1, col2, col3 = st.columns([1, 1, 1])
42
  with col2:
43
+ if st.button("πŸ’Ύ Save Settings", type="primary", use_container_width=True):
44
+ config = save_configuration()
45
+ st.success("βœ… Settings saved successfully!")
46
+
47
+ # Display settings summary at full width (outside columns)
48
+ st.markdown("---")
49
+ if 'optimization_config' in st.session_state:
50
+ with st.expander("πŸ“‹ Settings Summary", expanded=False):
51
+ display_user_friendly_summary(st.session_state.optimization_config)
52
+
53
+ # Optimization section
54
+ st.markdown("---")
55
+ st.header("πŸš€ Run Optimization")
56
+ st.markdown("Once you've configured your settings, run the optimization to generate the optimal workforce schedule.")
57
+
58
+ col1, col2, col3 = st.columns([1, 1, 1])
59
+ with col2:
60
+ if st.button("πŸš€ Optimize Schedule", type="primary", use_container_width=True):
61
+ run_optimization()
62
+
63
+ # Display optimization results if available
64
+ if 'optimization_results' in st.session_state and st.session_state.optimization_results is not None:
65
+ st.markdown("---")
66
+ display_optimization_results(st.session_state.optimization_results)
67
 
68
  def initialize_session_state():
69
+ """Initialize session state with values from optimization_config.py (single source of truth)"""
70
+
71
+ # Load ALL values from optimization_config.py - NO hard-coded defaults here
72
+ try:
73
+ sys.path.append('src')
74
+ from config.optimization_config import (
75
+ # Import the actual computed values, not just constants
76
+ EVENING_SHIFT_MODE, EVENING_SHIFT_DEMAND_THRESHOLD,
77
+ FIXED_STAFF_CONSTRAINT_MODE, DAILY_WEEKLY_SCHEDULE,
78
+ MAX_HOUR_PER_PERSON_PER_DAY, MAX_HOUR_PER_SHIFT_PER_PERSON,
79
+ MAX_PARALLEL_WORKERS, COST_LIST_PER_EMP_SHIFT,
80
+ PAYMENT_MODE_CONFIG, LINE_CNT_PER_TYPE,
81
+ MAX_EMPLOYEE_PER_TYPE_ON_DAY, start_date, end_date
82
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
+ # Get the actual computed default values from optimization_config.py
85
+ defaults = {
86
+ # Schedule configuration - from optimization_config.py
87
+ 'start_date': start_date.date() if hasattr(start_date, 'date') else start_date,
88
+ 'end_date': end_date.date() if hasattr(end_date, 'date') else end_date,
89
+ 'schedule_type': DAILY_WEEKLY_SCHEDULE,
90
+
91
+ # Shift configuration - from optimization_config.py
92
+ 'evening_shift_mode': EVENING_SHIFT_MODE,
93
+ 'evening_shift_threshold': EVENING_SHIFT_DEMAND_THRESHOLD,
94
+
95
+ # Fixed staff configuration - from optimization_config.py
96
+ 'fixed_staff_mode': FIXED_STAFF_CONSTRAINT_MODE,
97
+
98
+ # Payment configuration - from optimization_config.py
99
+ 'payment_mode_shift_1': PAYMENT_MODE_CONFIG.get(1),
100
+ 'payment_mode_shift_2': PAYMENT_MODE_CONFIG.get(2),
101
+ 'payment_mode_shift_3': PAYMENT_MODE_CONFIG.get(3),
102
+
103
+ # Working hours - from optimization_config.py
104
+ 'max_hour_per_person_per_day': MAX_HOUR_PER_PERSON_PER_DAY,
105
+ 'max_hours_shift_1': MAX_HOUR_PER_SHIFT_PER_PERSON.get(1),
106
+ 'max_hours_shift_2': MAX_HOUR_PER_SHIFT_PER_PERSON.get(2),
107
+ 'max_hours_shift_3': MAX_HOUR_PER_SHIFT_PER_PERSON.get(3),
108
+
109
+ # Operations - from optimization_config.py
110
+ 'max_parallel_workers_long_line': MAX_PARALLEL_WORKERS.get(6),
111
+ 'max_parallel_workers_mini_load': MAX_PARALLEL_WORKERS.get(7),
112
+
113
+ # Workforce limits - from optimization_config.py (computed values)
114
+ 'max_unicef_per_day': list(MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("UNICEF Fixed term", {}).values())[0] if MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("UNICEF Fixed term") else 8,
115
+ 'max_humanizer_per_day': list(MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("Humanizer", {}).values())[0] if MAX_EMPLOYEE_PER_TYPE_ON_DAY.get("Humanizer") else 10,
116
+
117
+ # Line counts - from optimization_config.py (data-driven)
118
+ 'line_count_long_line': LINE_CNT_PER_TYPE.get(6),
119
+ 'line_count_mini_load': LINE_CNT_PER_TYPE.get(7),
120
+
121
+ # Cost rates - from optimization_config.py (computed or default values)
122
+ 'unicef_rate_shift_1': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(1),
123
+ 'unicef_rate_shift_2': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(2),
124
+ 'unicef_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("UNICEF Fixed term", {}).get(3),
125
+ 'humanizer_rate_shift_1': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(1),
126
+ 'humanizer_rate_shift_2': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(2),
127
+ 'humanizer_rate_shift_3': COST_LIST_PER_EMP_SHIFT.get("Humanizer", {}).get(3),
128
+ }
129
 
130
+ except Exception as e:
131
+ st.error(f"❌ Could not load values from optimization_config.py: {e}")
132
+ st.error("Please check that the optimization_config.py file is working correctly.")
133
+ st.stop() # Stop execution - we shouldn't have fallback values here
134
 
135
+ # Initialize session state with values from optimization_config.py
136
  for key, value in defaults.items():
137
+ if key not in st.session_state and value is not None:
138
  st.session_state[key] = value
139
 
140
  def render_schedule_config():
 
251
  "Max Hours - Shift 1 (Regular)",
252
  min_value=1.0,
253
  max_value=12.0,
254
+ value=float(st.session_state.max_hours_shift_1),
255
  step=0.5,
256
  help="Maximum hours per person for regular shift"
257
  )
 
261
  "Max Hours - Shift 2 (Evening)",
262
  min_value=1.0,
263
  max_value=12.0,
264
+ value=float(st.session_state.max_hours_shift_2),
265
  step=0.5,
266
  help="Maximum hours per person for evening shift"
267
  )
 
271
  "Max Hours - Shift 3 (Overtime)",
272
  min_value=1.0,
273
  max_value=12.0,
274
+ value=float(st.session_state.max_hours_shift_3),
275
  step=0.5,
276
  help="Maximum hours per person for overtime shift"
277
  )
 
358
  help="Payment mode for overtime shift"
359
  )
360
 
361
+ # Hourly rates configuration - editable with defaults from config
362
+ st.subheader("πŸ’΅ Hourly Rates Configuration")
363
+
364
+ st.markdown("**UNICEF Fixed Term Hourly Rates:**")
365
+ col1, col2, col3 = st.columns(3)
366
+
367
+ with col1:
368
+ st.session_state.unicef_rate_shift_1 = st.number_input(
369
+ "Shift 1 (Regular) - UNICEF",
370
+ min_value=0.0,
371
+ max_value=200.0,
372
+ value=float(st.session_state.unicef_rate_shift_1),
373
+ step=0.01,
374
+ format="%.2f",
375
+ help="Hourly rate for UNICEF Fixed Term staff during regular shift"
376
+ )
377
+
378
+ with col2:
379
+ st.session_state.unicef_rate_shift_2 = st.number_input(
380
+ "Shift 2 (Evening) - UNICEF",
381
+ min_value=0.0,
382
+ max_value=200.0,
383
+ value=float(st.session_state.unicef_rate_shift_2),
384
+ step=0.01,
385
+ format="%.2f",
386
+ help="Hourly rate for UNICEF Fixed Term staff during evening shift"
387
+ )
388
+
389
+ with col3:
390
+ st.session_state.unicef_rate_shift_3 = st.number_input(
391
+ "Shift 3 (Overtime) - UNICEF",
392
+ min_value=0.0,
393
+ max_value=200.0,
394
+ value=float(st.session_state.unicef_rate_shift_3),
395
+ step=0.01,
396
+ format="%.2f",
397
+ help="Hourly rate for UNICEF Fixed Term staff during overtime shift"
398
+ )
399
+
400
+ st.markdown("**Humanizer Hourly Rates:**")
401
+ col1, col2, col3 = st.columns(3)
402
 
403
+ with col1:
404
+ st.session_state.humanizer_rate_shift_1 = st.number_input(
405
+ "Shift 1 (Regular) - Humanizer",
406
+ min_value=0.0,
407
+ max_value=200.0,
408
+ value=float(st.session_state.humanizer_rate_shift_1),
409
+ step=0.01,
410
+ format="%.2f",
411
+ help="Hourly rate for Humanizer staff during regular shift"
412
+ )
413
 
414
+ with col2:
415
+ st.session_state.humanizer_rate_shift_2 = st.number_input(
416
+ "Shift 2 (Evening) - Humanizer",
417
+ min_value=0.0,
418
+ max_value=200.0,
419
+ value=float(st.session_state.humanizer_rate_shift_2),
420
+ step=0.01,
421
+ format="%.2f",
422
+ help="Hourly rate for Humanizer staff during evening shift"
423
+ )
424
 
425
+ with col3:
426
+ st.session_state.humanizer_rate_shift_3 = st.number_input(
427
+ "Shift 3 (Overtime) - Humanizer",
428
+ min_value=0.0,
429
+ max_value=200.0,
430
+ value=float(st.session_state.humanizer_rate_shift_3),
431
+ step=0.01,
432
+ format="%.2f",
433
+ help="Hourly rate for Humanizer staff during overtime shift"
434
+ )
435
+
436
+ def render_data_selection_config():
437
+ """Render data selection configuration section"""
438
+ st.header("πŸ“Š Data Selection Configuration")
439
+
440
+ st.markdown("Configure which data elements to include in the optimization.")
441
+
442
+ # Employee types selection
443
+ st.subheader("πŸ‘₯ Employee Types")
444
+ available_employee_types = ["UNICEF Fixed term", "Humanizer"]
445
+
446
+ if 'selected_employee_types' not in st.session_state:
447
+ st.session_state.selected_employee_types = available_employee_types
448
+
449
+ selected_employee_types = st.multiselect(
450
+ "Select Employee Types to Include",
451
+ available_employee_types,
452
+ default=st.session_state.get('selected_employee_types', available_employee_types),
453
+ help="Choose which employee types to include in the optimization"
454
+ )
455
+ st.session_state.selected_employee_types = selected_employee_types
456
+
457
+ # Shifts selection
458
+ st.subheader("πŸ• Shifts")
459
+ available_shifts = [1, 2, 3]
460
+ shift_names = {1: "Regular", 2: "Evening", 3: "Overtime"}
461
+
462
+ if 'selected_shifts' not in st.session_state:
463
+ st.session_state.selected_shifts = available_shifts
464
+
465
+ selected_shifts = st.multiselect(
466
+ "Select Shifts to Include",
467
+ available_shifts,
468
+ default=st.session_state.get('selected_shifts', available_shifts),
469
+ format_func=lambda x: f"Shift {x} ({shift_names[x]})",
470
+ help="Choose which shifts to include in the optimization"
471
+ )
472
+ st.session_state.selected_shifts = selected_shifts
473
+
474
+ # Production lines selection
475
+ st.subheader("🏭 Production Lines")
476
+ available_lines = [6, 7]
477
+ line_names = {6: "Long Line", 7: "Mini Load"}
478
+
479
+ if 'selected_lines' not in st.session_state:
480
+ st.session_state.selected_lines = available_lines
481
+
482
+ selected_lines = st.multiselect(
483
+ "Select Production Lines to Include",
484
+ available_lines,
485
+ default=st.session_state.get('selected_lines', available_lines),
486
+ format_func=lambda x: f"Line {x} ({line_names[x]})",
487
+ help="Choose which production lines to include in the optimization"
488
+ )
489
+ st.session_state.selected_lines = selected_lines
490
+
491
+ # Validation warnings
492
+ if not selected_employee_types:
493
+ st.error("⚠️ At least one employee type must be selected!")
494
+ if not selected_shifts:
495
+ st.error("⚠️ At least one shift must be selected!")
496
+ if not selected_lines:
497
+ st.error("⚠️ At least one production line must be selected!")
498
 
499
  def save_configuration():
500
  """Save current configuration to session state and potentially to file"""
501
 
502
+ # Create comprehensive configuration dictionary
503
  config = {
504
  'date_range': {
505
  'start_date': st.session_state.start_date,
 
528
  },
529
  'operations': {
530
  'line_counts': {
531
+ 6: st.session_state.line_count_long_line, # Use line IDs directly
532
+ 7: st.session_state.line_count_mini_load,
533
  },
534
  'max_parallel_workers': {
535
  6: st.session_state.max_parallel_workers_long_line, # long line id
536
  7: st.session_state.max_parallel_workers_mini_load, # mini load id
537
  }
538
+ },
539
+ 'cost_rates': {
540
+ 'UNICEF Fixed term': {
541
+ 1: st.session_state.unicef_rate_shift_1,
542
+ 2: st.session_state.unicef_rate_shift_2,
543
+ 3: st.session_state.unicef_rate_shift_3,
544
+ },
545
+ 'Humanizer': {
546
+ 1: st.session_state.humanizer_rate_shift_1,
547
+ 2: st.session_state.humanizer_rate_shift_2,
548
+ 3: st.session_state.humanizer_rate_shift_3,
549
+ }
550
+ },
551
+ 'data_selection': {
552
+ 'selected_employee_types': st.session_state.get('selected_employee_types', []),
553
+ 'selected_shifts': st.session_state.get('selected_shifts', []),
554
+ 'selected_lines': st.session_state.get('selected_lines', []),
555
  }
556
  }
557
 
558
+ # Store individual items in session state for optimization_config.py to access
559
+ st.session_state.line_counts = config['operations']['line_counts']
560
+ st.session_state.cost_list_per_emp_shift = config['cost_rates']
561
+ st.session_state.payment_mode_config = config['payment_mode_config']
562
+ st.session_state.max_employee_per_type_on_day = {
563
+ "UNICEF Fixed term": {t: st.session_state.max_unicef_per_day for t in range(1, 6)},
564
+ "Humanizer": {t: st.session_state.max_humanizer_per_day for t in range(1, 6)}
565
+ }
566
+
567
+ # Store complete configuration
568
  st.session_state.optimization_config = config
569
 
570
+ # Return config for use in main function
571
+ return config
572
+
573
+ def display_user_friendly_summary(config):
574
+ """Display a user-friendly summary of the configuration settings"""
575
+
576
+ # Schedule Settings
577
+ st.subheader("πŸ“… Schedule Settings")
578
+ col1, col2, col3, col4 = st.columns(4)
579
+ with col1:
580
+ st.write(f"**Start Date:** {config['date_range']['start_date']}")
581
+ with col2:
582
+ st.write(f"**End Date:** {config['date_range']['end_date']}")
583
+ with col3:
584
+ st.write(f"**Schedule Type:** {config['schedule_type'].title()}")
585
+ with col4:
586
+ st.write(f"**Evening Shift Mode:** {config['evening_shift_mode'].replace('_', ' ').title()}")
587
+
588
+ # Show additional schedule details if evening shift threshold is relevant
589
+ if config['evening_shift_mode'] == 'activate_evening':
590
+ st.write(f"**Evening Shift Threshold:** {config['evening_shift_threshold']:.0%} demand capacity")
591
+
592
+ # Workforce Settings
593
+ st.subheader("πŸ‘₯ Workforce Settings")
594
+ col1, col2 = st.columns(2)
595
+ with col1:
596
+ st.write(f"**Max UNICEF Staff per Day:** {config['workforce_limits']['max_unicef_per_day']} people")
597
+ st.write(f"**Max Humanizer Staff per Day:** {config['workforce_limits']['max_humanizer_per_day']} people")
598
+ with col2:
599
+ st.write(f"**Staff Management Mode:** {config['fixed_staff_mode'].replace('_', ' ').title()}")
600
+ st.write(f"**Max Hours per Person per Day:** {config['working_hours']['max_hour_per_person_per_day']} hours")
601
+
602
+ # Operations Settings
603
+ st.subheader("🏭 Operations Settings")
604
+ col1, col2 = st.columns(2)
605
+ with col1:
606
+ st.write(f"**Long Lines Available:** {config['operations']['line_counts'][6]} lines")
607
+ st.write(f"**Mini Load Lines Available:** {config['operations']['line_counts'][7]} lines")
608
+ with col2:
609
+ st.write(f"**Max Workers per Long Line:** {config['operations']['max_parallel_workers'][6]} people")
610
+ st.write(f"**Max Workers per Mini Load Line:** {config['operations']['max_parallel_workers'][7]} people")
611
+
612
+ # Cost Settings
613
+ st.subheader("πŸ’° Cost Settings")
614
+ st.write("**Hourly Rates:**")
615
+
616
+ col1, col2 = st.columns(2)
617
+ with col1:
618
+ st.write("*UNICEF Fixed Term Staff:*")
619
+ st.write(f"β€’ Regular Shift: €{config['cost_rates']['UNICEF Fixed term'][1]:.2f}/hour")
620
+ st.write(f"β€’ Evening Shift: €{config['cost_rates']['UNICEF Fixed term'][2]:.2f}/hour")
621
+ st.write(f"β€’ Overtime Shift: €{config['cost_rates']['UNICEF Fixed term'][3]:.2f}/hour")
622
+
623
+ with col2:
624
+ st.write("*Humanizer Staff:*")
625
+ st.write(f"β€’ Regular Shift: €{config['cost_rates']['Humanizer'][1]:.2f}/hour")
626
+ st.write(f"β€’ Evening Shift: €{config['cost_rates']['Humanizer'][2]:.2f}/hour")
627
+ st.write(f"β€’ Overtime Shift: €{config['cost_rates']['Humanizer'][3]:.2f}/hour")
628
+
629
+ # Payment Settings
630
+ st.write("**Payment Modes:**")
631
+ payment_descriptions = {
632
+ 'bulk': 'Full shift payment (even for partial hours)',
633
+ 'partial': 'Pay only for actual hours worked'
634
+ }
635
+
636
+ col1, col2, col3 = st.columns(3)
637
+ with col1:
638
+ mode = config['payment_mode_config'][1]
639
+ st.write(f"β€’ **Regular Shift:** {mode.title()}")
640
+ st.caption(payment_descriptions[mode])
641
+ with col2:
642
+ mode = config['payment_mode_config'][2]
643
+ st.write(f"β€’ **Evening Shift:** {mode.title()}")
644
+ st.caption(payment_descriptions[mode])
645
+ with col3:
646
+ mode = config['payment_mode_config'][3]
647
+ st.write(f"β€’ **Overtime Shift:** {mode.title()}")
648
+ st.caption(payment_descriptions[mode])
649
+
650
+ # Data Selection Settings (if available)
651
+ if 'data_selection' in config:
652
+ st.subheader("πŸ“Š Data Selection")
653
+ col1, col2, col3 = st.columns(3)
654
+ with col1:
655
+ employee_types = config['data_selection']['selected_employee_types']
656
+ st.write(f"**Employee Types:** {len(employee_types)} selected")
657
+ for emp_type in employee_types:
658
+ st.write(f"β€’ {emp_type}")
659
+
660
+ with col2:
661
+ shifts = config['data_selection']['selected_shifts']
662
+ shift_names = {1: "Regular", 2: "Evening", 3: "Overtime"}
663
+ st.write(f"**Shifts:** {len(shifts)} selected")
664
+ for shift in shifts:
665
+ st.write(f"β€’ Shift {shift} ({shift_names.get(shift, 'Unknown')})")
666
+
667
+ with col3:
668
+ lines = config['data_selection']['selected_lines']
669
+ line_names = {6: "Long Line", 7: "Mini Load"}
670
+ st.write(f"**Production Lines:** {len(lines)} selected")
671
+ for line in lines:
672
+ st.write(f"β€’ Line {line} ({line_names.get(line, 'Unknown')})")
673
+
674
+ # Summary totals
675
+ st.subheader("πŸ“Š Quick Summary")
676
+ col1, col2, col3, col4 = st.columns(4)
677
+
678
+ with col1:
679
+ duration = (config['date_range']['end_date'] - config['date_range']['start_date']).days + 1
680
+ st.metric("Planning Period", f"{duration} days")
681
+
682
+ with col2:
683
+ total_staff = config['workforce_limits']['max_unicef_per_day'] + config['workforce_limits']['max_humanizer_per_day']
684
+ st.metric("Max Daily Staff", f"{total_staff} people")
685
+
686
+ with col3:
687
+ total_lines = config['operations']['line_counts'][6] + config['operations']['line_counts'][7]
688
+ st.metric("Production Lines", f"{total_lines} lines")
689
+
690
+ with col4:
691
+ avg_unicef_rate = sum(config['cost_rates']['UNICEF Fixed term'].values()) / 3
692
+ st.metric("Avg UNICEF Rate", f"€{avg_unicef_rate:.2f}/hr")
693
+
694
+ def run_optimization():
695
+ """Run the optimization model and store results"""
696
+ try:
697
+ st.info("πŸ”„ Running optimization... This may take a few moments.")
698
+
699
+ # Import and run the optimization
700
+ sys.path.append('src')
701
+ from models.optimizer_real import solve_fixed_team_weekly
702
+
703
+ # Run the optimization
704
+ with st.spinner('Optimizing workforce schedule...'):
705
+ results = solve_fixed_team_weekly()
706
+
707
+ if results is None:
708
+ st.error("❌ Optimization failed! The problem may be infeasible with current settings.")
709
+ st.error("Try adjusting your workforce limits, line counts, or evening shift settings.")
710
+ return
711
+
712
+ # Store results in session state
713
+ st.session_state.optimization_results = results
714
+ st.success("βœ… Optimization completed successfully!")
715
+ st.experimental_rerun() # Refresh to show results
716
+
717
+ except Exception as e:
718
+ st.error(f"❌ Error during optimization: {str(e)}")
719
+ st.error("Please check your settings and data files.")
720
+
721
+ def display_optimization_results(results):
722
+ """Import and display optimization results"""
723
+ from optimization_results import display_optimization_results as display_results
724
+ display_results(results)
optimization_results.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Optimization Results Display Functions for Streamlit
3
+ Handles visualization of optimization results with charts and tables
4
+ """
5
+
6
+ import streamlit as st
7
+ import pandas as pd
8
+ import plotly.express as px
9
+ import plotly.graph_objects as go
10
+ import sys
11
+
12
+ def display_optimization_results(results):
13
+ """Display comprehensive optimization results with visualizations"""
14
+ st.header("πŸ“Š Optimization Results")
15
+
16
+ # Create tabs for different views
17
+ tab1, tab2, tab3, tab4, tab5 = st.tabs([
18
+ "πŸ“ˆ Weekly Summary",
19
+ "πŸ“… Daily Deep Dive",
20
+ "🏭 Line Schedules",
21
+ "πŸ“¦ Kit Production",
22
+ "πŸ’° Cost Analysis"
23
+ ])
24
+
25
+ with tab1:
26
+ display_weekly_summary(results)
27
+
28
+ with tab2:
29
+ display_daily_deep_dive(results)
30
+
31
+ with tab3:
32
+ display_line_schedules(results)
33
+
34
+ with tab4:
35
+ display_kit_production(results)
36
+
37
+ with tab5:
38
+ display_cost_analysis(results)
39
+
40
+ def display_weekly_summary(results):
41
+ """Display weekly summary with key metrics and charts"""
42
+ st.subheader("πŸ“ˆ Weekly Performance Summary")
43
+
44
+ # Key metrics
45
+ col1, col2, col3, col4 = st.columns(4)
46
+
47
+ with col1:
48
+ total_cost = results['objective']
49
+ st.metric("Total Cost", f"€{total_cost:,.2f}")
50
+
51
+ with col2:
52
+ total_production = sum(results['weekly_production'].values())
53
+ st.metric("Total Production", f"{total_production:,.0f} units")
54
+
55
+ with col3:
56
+ # Calculate fulfillment rate
57
+ sys.path.append('src')
58
+ from config.optimization_config import DEMAND_DICTIONARY
59
+ total_demand = sum(DEMAND_DICTIONARY.values())
60
+ fulfillment_rate = (total_production / total_demand * 100) if total_demand > 0 else 0
61
+ st.metric("Fulfillment Rate", f"{fulfillment_rate:.1f}%")
62
+
63
+ with col4:
64
+ # Calculate cost per unit
65
+ cost_per_unit = total_cost / total_production if total_production > 0 else 0
66
+ st.metric("Cost per Unit", f"€{cost_per_unit:.2f}")
67
+
68
+ # Production vs Demand Chart
69
+ st.subheader("🎯 Production vs Demand")
70
+
71
+ from config.optimization_config import DEMAND_DICTIONARY
72
+ prod_demand_data = []
73
+ for product, production in results['weekly_production'].items():
74
+ demand = DEMAND_DICTIONARY.get(product, 0)
75
+ prod_demand_data.append({
76
+ 'Product': product,
77
+ 'Production': production,
78
+ 'Demand': demand,
79
+ 'Gap': production - demand
80
+ })
81
+
82
+ df_prod = pd.DataFrame(prod_demand_data)
83
+
84
+ if not df_prod.empty:
85
+ # Bar chart comparing production vs demand
86
+ fig = go.Figure()
87
+ fig.add_trace(go.Bar(name='Production', x=df_prod['Product'], y=df_prod['Production'],
88
+ marker_color='lightblue'))
89
+ fig.add_trace(go.Bar(name='Demand', x=df_prod['Product'], y=df_prod['Demand'],
90
+ marker_color='orange'))
91
+
92
+ fig.update_layout(
93
+ title='Weekly Production vs Demand by Product',
94
+ xaxis_title='Product',
95
+ yaxis_title='Units',
96
+ barmode='group',
97
+ height=400
98
+ )
99
+ st.plotly_chart(fig, use_container_width=True)
100
+
101
+ def display_daily_deep_dive(results):
102
+ """Display daily breakdown with workforce utilization"""
103
+ st.subheader("πŸ“… Daily Workforce Utilization")
104
+
105
+ # Workforce utilization by day
106
+ workforce_data = []
107
+ for row in results['person_hours_by_day']:
108
+ workforce_data.append({
109
+ 'Day': f"Day {row['day']}",
110
+ 'Employee Type': row['emp_type'],
111
+ 'Used Hours': row['used_person_hours'],
112
+ 'Available Hours': row['cap_person_hours'],
113
+ 'Utilization %': (row['used_person_hours'] / row['cap_person_hours'] * 100) if row['cap_person_hours'] > 0 else 0
114
+ })
115
+
116
+ df_workforce = pd.DataFrame(workforce_data)
117
+
118
+ if not df_workforce.empty:
119
+ # Utilization percentage chart
120
+ fig = px.bar(df_workforce, x='Day', y='Utilization %', color='Employee Type',
121
+ title='Daily Workforce Utilization by Employee Type',
122
+ height=400)
123
+ fig.update_layout(yaxis_title='Utilization Percentage')
124
+ st.plotly_chart(fig, use_container_width=True)
125
+
126
+ # Detailed table
127
+ st.subheader("πŸ“‹ Daily Workforce Details")
128
+ st.dataframe(df_workforce, use_container_width=True)
129
+
130
+ def display_line_schedules(results):
131
+ """Display line schedules showing what runs when and with how many workers"""
132
+ st.subheader("🏭 Production Line Schedules")
133
+
134
+ # Process schedule data
135
+ schedule_data = []
136
+ sys.path.append('src')
137
+ from config.optimization_config import TEAM_REQ_PER_PRODUCT, shift_code_to_name, line_code_to_name
138
+
139
+ # Get the mapping dictionaries
140
+ shift_names = shift_code_to_name()
141
+ line_names = line_code_to_name()
142
+
143
+ for row in results['run_schedule']:
144
+ # Get team requirements for this product
145
+ unicef_workers = TEAM_REQ_PER_PRODUCT.get('UNICEF Fixed term', {}).get(row['product'], 0)
146
+ humanizer_workers = TEAM_REQ_PER_PRODUCT.get('Humanizer', {}).get(row['product'], 0)
147
+ total_workers = unicef_workers + humanizer_workers
148
+
149
+ # Convert codes to readable names
150
+ line_name = line_names.get(row['line_type_id'], f"Line {row['line_type_id']}")
151
+ shift_name = shift_names.get(row['shift'], f"Shift {row['shift']}")
152
+
153
+ schedule_data.append({
154
+ 'Day': f"Day {row['day']}",
155
+ 'Line': f"{line_name} {row['line_idx']}",
156
+ 'Shift': shift_name,
157
+ 'Product': row['product'],
158
+ 'Hours': round(row['run_hours'], 2),
159
+ 'Units': round(row['units'], 0),
160
+ 'UNICEF Workers': unicef_workers,
161
+ 'Humanizer Workers': humanizer_workers,
162
+ 'Total Workers': total_workers
163
+ })
164
+
165
+ df_schedule = pd.DataFrame(schedule_data)
166
+
167
+ if not df_schedule.empty:
168
+ # Timeline view
169
+ st.subheader("⏰ Production Timeline")
170
+
171
+ # Create a Gantt-like chart
172
+ fig = px.bar(df_schedule, x='Hours', y='Line', color='Product',
173
+ facet_col='Day', orientation='h',
174
+ title='Production Schedule by Line and Day',
175
+ height=500)
176
+ fig.update_layout(showlegend=True)
177
+ st.plotly_chart(fig, use_container_width=True)
178
+
179
+ # Detailed schedule table
180
+ st.subheader("πŸ“‹ Detailed Production Schedule")
181
+ st.dataframe(df_schedule, use_container_width=True)
182
+
183
+ def display_kit_production(results):
184
+ """Display kit production details"""
185
+ st.subheader("πŸ“¦ Kit Production Analysis")
186
+
187
+ # Weekly production summary
188
+ production_data = []
189
+ sys.path.append('src')
190
+ from config.optimization_config import DEMAND_DICTIONARY
191
+
192
+ for product, production in results['weekly_production'].items():
193
+ demand = DEMAND_DICTIONARY.get(product, 0)
194
+ production_data.append({
195
+ 'Product': product,
196
+ 'Production': production,
197
+ 'Demand': demand,
198
+ 'Fulfillment %': (production / demand * 100) if demand > 0 else 0,
199
+ 'Over/Under': production - demand
200
+ })
201
+
202
+ df_production = pd.DataFrame(production_data)
203
+
204
+ if not df_production.empty:
205
+ # Fulfillment rate chart
206
+ fig = px.bar(df_production, x='Product', y='Fulfillment %',
207
+ title='Kit Fulfillment Rate by Product',
208
+ color='Fulfillment %',
209
+ color_continuous_scale=['red', 'yellow', 'green'],
210
+ height=400)
211
+ fig.add_hline(y=100, line_dash="dash", line_color="black",
212
+ annotation_text="100% Target")
213
+ st.plotly_chart(fig, use_container_width=True)
214
+
215
+ # Production summary table
216
+ st.subheader("πŸ“‹ Kit Production Summary")
217
+ st.dataframe(df_production, use_container_width=True)
218
+
219
+ def display_cost_analysis(results):
220
+ """Display cost breakdown and analysis"""
221
+ st.subheader("πŸ’° Cost Breakdown Analysis")
222
+
223
+ # Calculate cost breakdown
224
+ sys.path.append('src')
225
+ from config.optimization_config import COST_LIST_PER_EMP_SHIFT, TEAM_REQ_PER_PRODUCT, shift_code_to_name, line_code_to_name
226
+
227
+ # Get the mapping dictionaries
228
+ shift_names = shift_code_to_name()
229
+ line_names = line_code_to_name()
230
+
231
+ cost_data = []
232
+ total_cost_by_type = {}
233
+
234
+ for row in results['run_schedule']:
235
+ product = row['product']
236
+ hours = row['run_hours']
237
+ shift = row['shift']
238
+ shift_name = shift_names.get(shift, f"Shift {shift}")
239
+ line_name = line_names.get(row['line_type_id'], f"Line {row['line_type_id']}")
240
+
241
+ # Calculate costs for this production run (accounting for payment mode)
242
+ from config.optimization_config import PAYMENT_MODE_CONFIG, MAX_HOUR_PER_SHIFT_PER_PERSON
243
+
244
+ for emp_type in ['UNICEF Fixed term', 'Humanizer']:
245
+ workers_needed = TEAM_REQ_PER_PRODUCT.get(emp_type, {}).get(product, 0)
246
+ hourly_rate = COST_LIST_PER_EMP_SHIFT.get(emp_type, {}).get(shift, 0)
247
+
248
+ # Check payment mode for this shift
249
+ payment_mode = PAYMENT_MODE_CONFIG.get(shift, "partial")
250
+
251
+ if payment_mode == "bulk" and hours > 0:
252
+ # Bulk payment: pay for full shift hours if workers are active
253
+ shift_hours = MAX_HOUR_PER_SHIFT_PER_PERSON.get(shift, hours)
254
+ cost = workers_needed * shift_hours * hourly_rate
255
+ display_hours = shift_hours # Show full shift hours in display
256
+ else:
257
+ # Partial payment: pay for actual hours worked
258
+ cost = workers_needed * hours * hourly_rate
259
+ display_hours = hours # Show actual hours in display
260
+
261
+ if emp_type not in total_cost_by_type:
262
+ total_cost_by_type[emp_type] = 0
263
+ total_cost_by_type[emp_type] += cost
264
+
265
+ if cost > 0:
266
+ # Add payment mode indicator to shift name for clarity
267
+ payment_indicator = f" ({payment_mode})" if payment_mode == "bulk" else ""
268
+ cost_data.append({
269
+ 'Employee Type': emp_type,
270
+ 'Day': f"Day {row['day']}",
271
+ 'Shift': f"{shift_name}{payment_indicator}",
272
+ 'Line': f"{line_name} {row['line_idx']}",
273
+ 'Product': product,
274
+ 'Actual Hours': round(hours, 2),
275
+ 'Paid Hours': round(display_hours, 2),
276
+ 'Workers': workers_needed,
277
+ 'Hourly Rate': f"€{hourly_rate:.2f}",
278
+ 'Cost': round(cost, 2)
279
+ })
280
+
281
+ # Total cost metrics
282
+ total_cost = results['objective']
283
+ col1, col2, col3, col4 = st.columns(4)
284
+
285
+ with col1:
286
+ st.metric("Total Cost", f"€{total_cost:,.2f}")
287
+
288
+ with col2:
289
+ unicef_cost = total_cost_by_type.get('UNICEF Fixed term', 0)
290
+ st.metric("UNICEF Cost", f"€{unicef_cost:,.2f}")
291
+
292
+ with col3:
293
+ humanizer_cost = total_cost_by_type.get('Humanizer', 0)
294
+ st.metric("Humanizer Cost", f"€{humanizer_cost:,.2f}")
295
+
296
+ with col4:
297
+ avg_daily_cost = total_cost / len(set(row['day'] for row in results['run_schedule'])) if results['run_schedule'] else 0
298
+ st.metric("Avg Daily Cost", f"€{avg_daily_cost:,.2f}")
299
+
300
+ # Cost breakdown pie chart
301
+ if total_cost_by_type:
302
+ fig = px.pie(values=list(total_cost_by_type.values()),
303
+ names=list(total_cost_by_type.keys()),
304
+ title='Cost Distribution by Employee Type')
305
+ st.plotly_chart(fig, use_container_width=True)
306
+
307
+ # Detailed cost table
308
+ if cost_data:
309
+ df_costs = pd.DataFrame(cost_data)
310
+ st.subheader("πŸ“‹ Detailed Cost Breakdown")
311
+ st.dataframe(df_costs, use_container_width=True)
src/config/optimization_config.py CHANGED
@@ -13,26 +13,24 @@ importlib.reload(transformed_data) # Uncomment if needed
13
 
14
 
15
  def get_date_span():
16
- print(f"πŸ”§ FORCING NEW DATE RANGE FOR TESTING")
17
- # Force use new date range to match user's data
18
- from datetime import datetime
19
- return list(range(1, 6)), datetime(2025, 7, 7), datetime(2025, 7, 11) # Force 5 days for user's data
 
 
 
 
 
 
 
 
 
20
 
21
- # Original logic (commented out for testing)
22
- # try:
23
- # start_date = dashboard.start_date
24
- # end_date = dashboard.end_date
25
- # date_span = list(range(1, (end_date - start_date).days + 2))
26
- # print(f"date from user input")
27
- # print("date span",date_span)
28
- # print("start date",start_date)
29
- # print("end date",end_date)
30
- # return date_span, start_date, end_date
31
- # except Exception as e:
32
- # print(f"using default value for date span")
33
- # # Updated to match the user's data in COOIS_Released_Prod_Orders.csv
34
- # from datetime import datetime
35
- # return list(range(1, 6)), datetime(2025, 7, 7), datetime(2025, 7, 11) # Default 5 days
36
 
37
 
38
  #fetch date from streamlit or default value. The streamlit and default references the demand data (COOIS_Planned_and_Released.csv)
@@ -84,8 +82,8 @@ def get_shift_list():
84
 
85
  # Evening shift activation mode - define early to avoid circular dependency
86
  # Options:
87
- # "normal" - Only use regular shift (1) and overtime shift (2)
88
- # "activate_evening" - Allow evening shift (3) when demand is too high or cost-effective
89
  # "always_available" - Evening shift always available as option
90
  EVENING_SHIFT_MODE = "normal" # Default: only regular + overtime
91
 
@@ -100,12 +98,12 @@ def get_active_shift_list():
100
  all_shifts = get_shift_list()
101
 
102
  if EVENING_SHIFT_MODE == "normal":
103
- # Only regular (1) and overtime (2) shifts
104
- active_shifts = [s for s in all_shifts if s in [1, 2]]
105
- print(f"[SHIFT MODE] Normal mode: Using shifts {active_shifts} (Regular + Overtime only)")
106
 
107
  elif EVENING_SHIFT_MODE == "activate_evening":
108
- # All shifts including evening (3)
109
  active_shifts = list(all_shifts)
110
  print(f"[SHIFT MODE] Evening activated: Using all shifts {active_shifts}")
111
 
@@ -116,7 +114,7 @@ def get_active_shift_list():
116
 
117
  else:
118
  # Default to normal mode
119
- active_shifts = [s for s in all_shifts if s in [1, 2]]
120
  print(f"[SHIFT MODE] Unknown mode '{EVENING_SHIFT_MODE}', defaulting to normal: {active_shifts}")
121
 
122
  return active_shifts
@@ -184,53 +182,74 @@ KIT_LINE_MATCH_DICT = get_kit_line_match()
184
 
185
  def get_line_cnt_per_type():
186
  try:
187
- streamlit_line_cnt_per_type = dashboard.line_cnt_per_type
188
- return streamlit_line_cnt_per_type
 
 
 
189
  except Exception as e:
190
- print(f"using default value for line cnt per type")
191
- line_df = extract.read_packaging_line_data()
192
- line_cnt_per_type = line_df.set_index("id")["line_count"].to_dict()
193
- print("line cnt per type")
194
- print(line_cnt_per_type)
195
- return line_cnt_per_type
 
196
 
197
  LINE_CNT_PER_TYPE = get_line_cnt_per_type()
198
  print("line cnt per type",LINE_CNT_PER_TYPE)
199
 
200
  def get_demand_dictionary():
201
  try:
202
- streamlit_demand_dictionary = dashboard.demand_dictionary
203
- return streamlit_demand_dictionary
 
 
 
204
  except Exception as e:
205
-
206
- # Use released orders instead of planned orders for demand
207
- demand_df = extract.read_released_orders_data(start_date=start_date, end_date=end_date)
208
- demand_dictionary = demand_df.groupby('Material Number')["Order quantity (GMEIN)"].sum().to_dict()
209
- print(f"πŸ“ˆ DEMAND DATA: {len(demand_dictionary)} products with total demand {sum(demand_dictionary.values())}")
210
- return demand_dictionary
 
 
211
 
212
  DEMAND_DICTIONARY = get_demand_dictionary()
213
  print(f"🎯 FINAL DEMAND: {DEMAND_DICTIONARY}")
214
 
215
  def get_cost_list_per_emp_shift():
216
  try:
217
- streamlit_cost_list_per_emp_shift = dashboard.cost_list_per_emp_shift
218
- return streamlit_cost_list_per_emp_shift
 
 
 
219
  except Exception as e:
220
- print(f"using default value for cost list per emp shift")
221
- shift_cost_df = extract.read_shift_cost_data()
222
- #question - Important : there is multiple type of employment type in terms of the cost
223
- #Shift 1 = normal, 2 = evening, 3 = overtime
224
- return {"UNICEF Fixed term":{1:43.27,2:43.27,3:64.91},"Humanizer":{1:27.94,2:27.94,3:41.91}}
 
225
 
226
  def shift_code_to_name():
227
  shift_code_to_name_dict = {
228
- 1: "normal",
229
- 2: "evening",
230
- 3: "overtime"
231
  }
232
  return shift_code_to_name_dict
233
 
 
 
 
 
 
 
 
 
234
  COST_LIST_PER_EMP_SHIFT = get_cost_list_per_emp_shift()
235
  # print("cost list per emp shift",COST_LIST_PER_EMP_SHIFT)
236
 
@@ -295,19 +314,24 @@ print("team requirements per product:", TEAM_REQ_PER_PRODUCT)
295
 
296
  def get_max_employee_per_type_on_day():
297
  try:
298
- max_employee_per_type_on_day = dashboard.max_employee_per_type_on_day
299
- return max_employee_per_type_on_day
 
 
 
300
  except Exception as e:
301
- print(f"using default value for max employee per type on day")
302
- max_employee_per_type_on_day = {
303
- "UNICEF Fixed term": {
304
- t: 8 for t in DATE_SPAN
305
- },
306
- "Humanizer": {
307
- t: 10 for t in DATE_SPAN
308
- }
 
309
  }
310
- return max_employee_per_type_on_day
 
311
 
312
  MAX_EMPLOYEE_PER_TYPE_ON_DAY = get_max_employee_per_type_on_day()
313
  print("max employee per type on day",MAX_EMPLOYEE_PER_TYPE_ON_DAY)
@@ -318,12 +342,17 @@ MAX_HOUR_PER_PERSON_PER_DAY = 14 # legal standard
318
  MAX_HOUR_PER_SHIFT_PER_PERSON = {1: 7.5, 2: 7.5, 3: 5} #1 = normal, 2 = evening, 3 = overtime
319
  def get_per_product_speed():
320
  try:
321
- streamlit_per_product_speed = dashboard.per_product_speed
322
- return streamlit_per_product_speed
 
 
 
323
  except Exception as e:
324
- print(f"using default value for per product speed")
325
- per_product_speed = extract.read_package_speed_data()
326
- return per_product_speed
 
 
327
 
328
 
329
  # Get per product speed - will use actual product names from PRODUCT_LIST
 
13
 
14
 
15
  def get_date_span():
16
+ try:
17
+ # Try to get from streamlit session state (from config page)
18
+ import streamlit as st
19
+ if hasattr(st, 'session_state') and 'start_date' in st.session_state and 'end_date' in st.session_state:
20
+ from datetime import datetime
21
+ start_date = datetime.combine(st.session_state.start_date, datetime.min.time())
22
+ end_date = datetime.combine(st.session_state.end_date, datetime.min.time())
23
+ date_span = list(range(1, (end_date - start_date).days + 2))
24
+ print(f"Using dates from config page: {start_date} to {end_date}")
25
+ print("date span", date_span)
26
+ return date_span, start_date, end_date
27
+ except Exception as e:
28
+ print(f"Could not get dates from streamlit session: {e}")
29
 
30
+ print(f"Loading default date values")
31
+ # Default to match the user's data in COOIS_Released_Prod_Orders.csv
32
+ from datetime import datetime
33
+ return list(range(1, 6)), datetime(2025, 7, 7), datetime(2025, 7, 11) # Default 5 days
 
 
 
 
 
 
 
 
 
 
 
34
 
35
 
36
  #fetch date from streamlit or default value. The streamlit and default references the demand data (COOIS_Planned_and_Released.csv)
 
82
 
83
  # Evening shift activation mode - define early to avoid circular dependency
84
  # Options:
85
+ # "normal" - Only use regular shift (1) and overtime shift (3) - NO evening shift
86
+ # "activate_evening" - Allow evening shift (2) when demand is too high or cost-effective
87
  # "always_available" - Evening shift always available as option
88
  EVENING_SHIFT_MODE = "normal" # Default: only regular + overtime
89
 
 
98
  all_shifts = get_shift_list()
99
 
100
  if EVENING_SHIFT_MODE == "normal":
101
+ # Only regular (1) and overtime (3) shifts - NO evening shift
102
+ active_shifts = [s for s in all_shifts if s in [1, 3]]
103
+ print(f"[SHIFT MODE] Normal mode: Using shifts {active_shifts} (Regular + Overtime only, NO evening)")
104
 
105
  elif EVENING_SHIFT_MODE == "activate_evening":
106
+ # All shifts including evening (2)
107
  active_shifts = list(all_shifts)
108
  print(f"[SHIFT MODE] Evening activated: Using all shifts {active_shifts}")
109
 
 
114
 
115
  else:
116
  # Default to normal mode
117
+ active_shifts = [s for s in all_shifts if s in [1, 3]]
118
  print(f"[SHIFT MODE] Unknown mode '{EVENING_SHIFT_MODE}', defaulting to normal: {active_shifts}")
119
 
120
  return active_shifts
 
182
 
183
  def get_line_cnt_per_type():
184
  try:
185
+ # Try to get from streamlit session state (from config page)
186
+ import streamlit as st
187
+ if hasattr(st, 'session_state') and 'line_counts' in st.session_state:
188
+ print(f"Using line counts from config page: {st.session_state.line_counts}")
189
+ return st.session_state.line_counts
190
  except Exception as e:
191
+ print(f"Could not get line counts from streamlit session: {e}")
192
+
193
+ print(f"Loading default line count values from data files")
194
+ line_df = extract.read_packaging_line_data()
195
+ line_cnt_per_type = line_df.set_index("id")["line_count"].to_dict()
196
+ print("line cnt per type", line_cnt_per_type)
197
+ return line_cnt_per_type
198
 
199
  LINE_CNT_PER_TYPE = get_line_cnt_per_type()
200
  print("line cnt per type",LINE_CNT_PER_TYPE)
201
 
202
  def get_demand_dictionary():
203
  try:
204
+ # Try to get from streamlit session state (from config page)
205
+ import streamlit as st
206
+ if hasattr(st, 'session_state') and 'demand_dictionary' in st.session_state:
207
+ print(f"Using demand dictionary from config page: {len(st.session_state.demand_dictionary)} products")
208
+ return st.session_state.demand_dictionary
209
  except Exception as e:
210
+ print(f"Could not get demand dictionary from streamlit session: {e}")
211
+
212
+ print(f"Loading default demand values from data files")
213
+ # Use released orders instead of planned orders for demand
214
+ demand_df = extract.read_released_orders_data(start_date=start_date, end_date=end_date)
215
+ demand_dictionary = demand_df.groupby('Material Number')["Order quantity (GMEIN)"].sum().to_dict()
216
+ print(f"πŸ“ˆ DEMAND DATA: {len(demand_dictionary)} products with total demand {sum(demand_dictionary.values())}")
217
+ return demand_dictionary
218
 
219
  DEMAND_DICTIONARY = get_demand_dictionary()
220
  print(f"🎯 FINAL DEMAND: {DEMAND_DICTIONARY}")
221
 
222
  def get_cost_list_per_emp_shift():
223
  try:
224
+ # Try to get from streamlit session state (from config page)
225
+ import streamlit as st
226
+ if hasattr(st, 'session_state') and 'cost_list_per_emp_shift' in st.session_state:
227
+ print(f"Using cost list from config page: {st.session_state.cost_list_per_emp_shift}")
228
+ return st.session_state.cost_list_per_emp_shift
229
  except Exception as e:
230
+ print(f"Could not get cost list from streamlit session: {e}")
231
+
232
+ print(f"Loading default cost values")
233
+ # Default hourly rates - Important: multiple employment types with different costs
234
+ # Shift 1 = normal, 2 = evening, 3 = overtime
235
+ return {"UNICEF Fixed term":{1:43.27,2:43.27,3:64.91},"Humanizer":{1:27.94,2:27.94,3:41.91}}
236
 
237
  def shift_code_to_name():
238
  shift_code_to_name_dict = {
239
+ 1: "Regular",
240
+ 2: "Evening",
241
+ 3: "Overtime"
242
  }
243
  return shift_code_to_name_dict
244
 
245
+ def line_code_to_name():
246
+ """Convert line type IDs to readable names"""
247
+ line_code_to_name_dict = {
248
+ 6: "Long Line",
249
+ 7: "Mini Load"
250
+ }
251
+ return line_code_to_name_dict
252
+
253
  COST_LIST_PER_EMP_SHIFT = get_cost_list_per_emp_shift()
254
  # print("cost list per emp shift",COST_LIST_PER_EMP_SHIFT)
255
 
 
314
 
315
  def get_max_employee_per_type_on_day():
316
  try:
317
+ # Try to get from streamlit session state (from config page)
318
+ import streamlit as st
319
+ if hasattr(st, 'session_state') and 'max_employee_per_type_on_day' in st.session_state:
320
+ print(f"Using max employee counts from config page: {st.session_state.max_employee_per_type_on_day}")
321
+ return st.session_state.max_employee_per_type_on_day
322
  except Exception as e:
323
+ print(f"Could not get max employee counts from streamlit session: {e}")
324
+
325
+ print(f"Loading default max employee values")
326
+ max_employee_per_type_on_day = {
327
+ "UNICEF Fixed term": {
328
+ t: 8 for t in DATE_SPAN
329
+ },
330
+ "Humanizer": {
331
+ t: 10 for t in DATE_SPAN
332
  }
333
+ }
334
+ return max_employee_per_type_on_day
335
 
336
  MAX_EMPLOYEE_PER_TYPE_ON_DAY = get_max_employee_per_type_on_day()
337
  print("max employee per type on day",MAX_EMPLOYEE_PER_TYPE_ON_DAY)
 
342
  MAX_HOUR_PER_SHIFT_PER_PERSON = {1: 7.5, 2: 7.5, 3: 5} #1 = normal, 2 = evening, 3 = overtime
343
  def get_per_product_speed():
344
  try:
345
+ # Try to get from streamlit session state (from config page)
346
+ import streamlit as st
347
+ if hasattr(st, 'session_state') and 'per_product_speed' in st.session_state:
348
+ print(f"Using per product speed from config page: {st.session_state.per_product_speed}")
349
+ return st.session_state.per_product_speed
350
  except Exception as e:
351
+ print(f"Could not get per product speed from streamlit session: {e}")
352
+
353
+ print(f"Loading default per product speed from data files")
354
+ per_product_speed = extract.read_package_speed_data()
355
+ return per_product_speed
356
 
357
 
358
  # Get per product speed - will use actual product names from PRODUCT_LIST
src/models/optimizer_real.py CHANGED
@@ -148,7 +148,7 @@ def solve_fixed_team_weekly():
148
  total_demand = sum(DEMAND_DICTIONARY.get(p, 0) for p in P)
149
 
150
  # Calculate maximum capacity with regular + overtime shifts only
151
- regular_overtime_shifts = [s for s in S if s in [1, 2]] # Only shifts 1, 2
152
  max_capacity = 0
153
 
154
  for p in P:
@@ -316,15 +316,26 @@ def solve_fixed_team_weekly():
316
  <= MAX_HOUR_PER_PERSON_PER_DAY * N_day[e][t]
317
  )
318
 
319
- # 7) OT after usual (optional but kept)
320
- for e in E:
321
- for t in D:
322
- solver.Add(
323
- solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, 2, t] for p in P for ell in L)
324
- <=
325
- solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, 1, t] for p in P for ell in L)
326
- )
327
- # (if needed, evening(3) after usual(1): sum(...)_s=3 ≀ sum(...)_s=1)
 
 
 
 
 
 
 
 
 
 
 
328
 
329
  # 7.5) Bulk payment linking constraints are now handled inline in the cost calculation
330
 
 
148
  total_demand = sum(DEMAND_DICTIONARY.get(p, 0) for p in P)
149
 
150
  # Calculate maximum capacity with regular + overtime shifts only
151
+ regular_overtime_shifts = [s for s in S if s in [1, 3]] # Only shifts 1, 3 (regular + overtime)
152
  max_capacity = 0
153
 
154
  for p in P:
 
316
  <= MAX_HOUR_PER_PERSON_PER_DAY * N_day[e][t]
317
  )
318
 
319
+ # 7) Shift ordering constraints (only apply if shifts are available)
320
+ # Evening shift (2) after regular shift (1)
321
+ if 2 in S and 1 in S: # Only if both shifts are available
322
+ for e in E:
323
+ for t in D:
324
+ solver.Add(
325
+ solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, 2, t] for p in P for ell in L)
326
+ <=
327
+ solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, 1, t] for p in P for ell in L)
328
+ )
329
+
330
+ # Overtime shift (3) after regular shift (1)
331
+ if 3 in S and 1 in S: # Only if both shifts are available
332
+ for e in E:
333
+ for t in D:
334
+ solver.Add(
335
+ solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, 3, t] for p in P for ell in L)
336
+ <=
337
+ solver.Sum(TEAM_REQ_PER_PRODUCT[e][p] * T[p, ell, 1, t] for p in P for ell in L)
338
+ )
339
 
340
  # 7.5) Bulk payment linking constraints are now handled inline in the cost calculation
341