| """ |
| Configuration Page for Supply Roster Optimization Tool |
| Contains all user input controls with default values from config |
| """ |
|
|
| import streamlit as st |
| import datetime |
| import sys |
| import os |
| from src.config import optimization_config |
| from src.config.constants import ShiftType, LineType, DefaultConfig |
|
|
| def clear_optimization_cache(): |
| """Clear all cached optimization results and validation data""" |
| |
| if 'optimization_results' in st.session_state: |
| del st.session_state.optimization_results |
| |
| |
| if 'show_validation_after_save' in st.session_state: |
| del st.session_state.show_validation_after_save |
| if 'settings_just_saved' in st.session_state: |
| del st.session_state.settings_just_saved |
| |
| |
| validation_cache_key = f"validation_results_{st.session_state.get('start_date', 'default')}" |
| if validation_cache_key in st.session_state: |
| del st.session_state[validation_cache_key] |
|
|
| def render_config_page(): |
| """Render the configuration page with all user input controls""" |
| |
| st.title("βοΈ Settings") |
| st.markdown("---") |
| |
|
|
| st.markdown("Adjust the settings for your workforce optimization. These settings control how the system schedules employees and calculates costs.") |
| |
| |
| initialize_session_state() |
| |
| |
| tab1, tab2, tab3, tab4, tab5 = st.tabs(["π
Schedule", "π₯ Workforce", "π Operations", "π° Cost", "π Data Selection"]) |
| |
| with tab1: |
| render_schedule_config() |
| |
| with tab2: |
| render_workforce_config() |
| |
| with tab3: |
| render_operations_config() |
| |
| with tab4: |
| render_cost_config() |
| |
| with tab5: |
| render_data_selection_config() |
| |
| |
| st.markdown("---") |
| col1, col2, col3 = st.columns([1, 1, 1]) |
| with col2: |
| if st.button("πΎ Save Settings", type="primary", use_container_width=True): |
| config = save_configuration() |
| st.success("β
Settings saved successfully!") |
| |
| |
| clear_optimization_cache() |
| |
| |
| st.session_state.show_validation_after_save = True |
| st.session_state.settings_just_saved = True |
| |
| |
| st.rerun() |
| |
| |
| st.markdown("---") |
| if 'optimization_config' in st.session_state: |
| with st.expander("π Settings Summary", expanded=False): |
| display_user_friendly_summary(st.session_state.optimization_config) |
| |
| |
| if st.session_state.get('show_validation_after_save', False) or st.session_state.get('settings_just_saved', False): |
| st.markdown("---") |
| st.header("π Data Validation Results") |
| st.markdown("Analyzing your demand data to identify potential optimization issues...") |
| |
| |
| validation_cache_key = f"validation_results_{st.session_state.get('start_date', 'default')}" |
| |
| if validation_cache_key not in st.session_state: |
| |
| with st.spinner("π Running data validation..."): |
| try: |
| from src.demand_validation_viz import DemandValidationViz |
| |
| |
| validator = DemandValidationViz() |
| print("validator",validator) |
| if validator.load_data(): |
| |
| validation_df = validator.validate_all_products() |
| print("validation_df", validation_df) |
| summary_stats = validator.get_summary_statistics(validation_df) |
| print("summary_stats", summary_stats) |
| |
| st.session_state[validation_cache_key] = { |
| 'validation_df': validation_df, |
| 'summary_stats': summary_stats, |
| 'validator': validator |
| } |
| else: |
| st.error("β Failed to load validation data") |
| st.session_state[validation_cache_key] = None |
| |
| except Exception as e: |
| st.error(f"β Error in demand validation: {str(e)}") |
| st.session_state[validation_cache_key] = None |
| |
| |
| if st.session_state.get(validation_cache_key): |
| |
| cached_results = st.session_state[validation_cache_key] |
| |
| validation_df = cached_results['validation_df'] |
| |
| summary_stats = cached_results['summary_stats'] |
| |
| |
| st.subheader("π Summary Statistics") |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.metric("Total Products", summary_stats['total_products']) |
| st.metric("Included in Optimization", summary_stats['included_products'], delta="Ready for optimization") |
| |
| with col2: |
| st.metric("Total Demand", f"{summary_stats['total_demand']:,}") |
| st.metric("Excluded from Optimization", summary_stats['excluded_products'], delta="Omitted") |
| |
| with col3: |
| st.metric("Included Demand", f"{summary_stats['included_demand']:,}", delta="Will be optimized") |
| st.metric("UNICEF Staff Needed", summary_stats['total_unicef_needed']) |
| |
| with col4: |
| st.metric("Excluded Demand", f"{summary_stats['excluded_demand']:,}", delta="Omitted") |
| st.metric("Humanizer Staff Needed", summary_stats['total_humanizer_needed']) |
| |
| |
| included_df = validation_df[validation_df['Excluded from Optimization'] == False].copy() |
| excluded_df = validation_df[validation_df['Excluded from Optimization'] == True].copy() |
| |
| |
| st.subheader("β
Products Included in Optimization") |
| st.write(f"**{len(included_df)} products** will be included in the optimization with total demand of **{included_df['Demand'].sum():,} units**") |
| |
| if len(included_df) > 0: |
| |
| included_columns = ['Product ID', 'Demand', 'Product Type', 'Line Type', 'UNICEF Staff', 'Humanizer Staff', 'Production Speed (units/hour)', 'Validation Status'] |
| |
| st.dataframe( |
| included_df[included_columns], |
| use_container_width=True, |
| height=300 |
| ) |
| else: |
| st.warning("No products are included in optimization!") |
| |
| |
| st.subheader("π« Products Excluded from Optimization") |
| st.write(f"**{len(excluded_df)} products** are excluded from optimization with total demand of **{excluded_df['Demand'].sum():,} units**") |
| st.info("These products are omitted from optimization due to missing line assignments or zero staffing requirements.") |
| |
| if len(excluded_df) > 0: |
| |
| exclusion_reasons = excluded_df['Exclusion Reasons'].value_counts() |
| st.write("**Exclusion reasons:**") |
| for reason, count in exclusion_reasons.items(): |
| st.write(f"β’ {reason}: {count} products") |
| |
| |
| excluded_columns = ['Product ID', 'Demand', 'Product Type', 'Exclusion Reasons', 'UNICEF Staff', 'Humanizer Staff', 'Line Type'] |
| |
| st.dataframe( |
| excluded_df[excluded_columns], |
| use_container_width=True, |
| height=200 |
| ) |
| else: |
| st.info("No products are excluded from optimization.") |
| |
| |
| st.info("π‘ **Review validation results above before running optimization.** " + |
| "Fix any critical issues (especially missing line assignments) to improve optimization success.") |
| else: |
| st.error("β Validation failed to run properly") |
| st.info("π‘ You can still proceed with optimization, but data issues may cause problems.") |
| |
| |
| col1, col2, col3 = st.columns([1, 1, 1]) |
| with col2: |
| if st.button("β
Validation Reviewed - Continue to Optimization", use_container_width=True): |
| st.session_state.show_validation_after_save = False |
| st.session_state.settings_just_saved = False |
| |
| if validation_cache_key in st.session_state: |
| del st.session_state[validation_cache_key] |
| st.rerun() |
| |
| |
| st.markdown("---") |
| st.header("π Run Optimization") |
| st.markdown("Once you've configured your settings, run the optimization to generate the optimal workforce schedule.") |
| st.markdown("**π‘ Tip:** Optimization automatically clears all previous cache and results to ensure fresh calculations with your current settings.") |
| |
| col1, col2, col3 = st.columns([1, 1, 1]) |
| with col1: |
| if st.button("π§Ή Clear Cache", use_container_width=True): |
| clear_all_cache_and_results() |
| with col2: |
| if st.button("π Optimize Schedule", type="primary", use_container_width=True): |
| |
| validation_warnings = check_critical_data_issues() |
| if validation_warnings: |
| st.warning("β οΈ **Data validation warnings detected:**") |
| for warning in validation_warnings: |
| st.warning(f"β’ {warning}") |
| st.warning("**These issues may cause optimization to fail. Consider fixing them first.**") |
| |
| |
| if st.button("β οΈ Proceed with Optimization Anyway", type="secondary"): |
| run_optimization() |
| else: |
| run_optimization() |
| with col3: |
| |
| if 'optimization_results' in st.session_state and st.session_state.optimization_results is not None: |
| st.success("β
Results Available") |
| elif 'optimization_config' in st.session_state: |
| st.info("π Settings Saved - Ready to Optimize") |
| else: |
| st.info("β³ No Results Yet") |
| |
| |
| if 'optimization_results' in st.session_state and st.session_state.optimization_results is not None: |
| st.markdown("---") |
| display_optimization_results(st.session_state.optimization_results) |
|
|
| def initialize_session_state(): |
| """Initialize session state with default values from constants.py""" |
| |
| |
| |
| |
| |
| st.session_state.setdefault('start_date', datetime.date(2025, 7, 7)) |
| st.session_state.setdefault('schedule_type', DefaultConfig.SCHEDULE_TYPE) |
| |
| |
| st.session_state.setdefault('evening_shift_mode', DefaultConfig.EVENING_SHIFT_MODE) |
| st.session_state.setdefault('evening_shift_threshold', DefaultConfig.EVENING_SHIFT_DEMAND_THRESHOLD) |
| |
| |
| st.session_state.setdefault('fixed_staff_mode', DefaultConfig.FIXED_STAFF_MODE) |
| st.session_state.setdefault('fixed_min_unicef_per_day', DefaultConfig.FIXED_MIN_UNICEF_PER_DAY) |
| |
| |
| st.session_state.setdefault('payment_mode_shift_1', DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.REGULAR]) |
| st.session_state.setdefault('payment_mode_shift_2', DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.EVENING]) |
| st.session_state.setdefault('payment_mode_shift_3', DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.OVERTIME]) |
| |
| |
| st.session_state.setdefault('max_hour_per_person_per_day', DefaultConfig.MAX_HOUR_PER_PERSON_PER_DAY) |
| st.session_state.setdefault('max_hours_shift_1', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.REGULAR]) |
| st.session_state.setdefault('max_hours_shift_2', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.EVENING]) |
| st.session_state.setdefault('max_hours_shift_3', DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.OVERTIME]) |
| |
| |
| st.session_state.setdefault('max_unicef_per_day', DefaultConfig.MAX_UNICEF_PER_DAY) |
| st.session_state.setdefault('max_humanizer_per_day', DefaultConfig.MAX_HUMANIZER_PER_DAY) |
| |
| |
| st.session_state.setdefault('line_count_long_line', DefaultConfig.LINE_COUNT_LONG_LINE) |
| st.session_state.setdefault('line_count_mini_load', DefaultConfig.LINE_COUNT_MINI_LOAD) |
| st.session_state.setdefault('max_parallel_workers_long_line', DefaultConfig.MAX_PARALLEL_WORKERS_LONG_LINE) |
| st.session_state.setdefault('max_parallel_workers_mini_load', DefaultConfig.MAX_PARALLEL_WORKERS_MINI_LOAD) |
| |
| |
| st.session_state.setdefault('unicef_rate_shift_1', DefaultConfig.UNICEF_RATE_SHIFT_1) |
| st.session_state.setdefault('unicef_rate_shift_2', DefaultConfig.UNICEF_RATE_SHIFT_2) |
| st.session_state.setdefault('unicef_rate_shift_3', DefaultConfig.UNICEF_RATE_SHIFT_3) |
| st.session_state.setdefault('humanizer_rate_shift_1', DefaultConfig.HUMANIZER_RATE_SHIFT_1) |
| st.session_state.setdefault('humanizer_rate_shift_2', DefaultConfig.HUMANIZER_RATE_SHIFT_2) |
| st.session_state.setdefault('humanizer_rate_shift_3', DefaultConfig.HUMANIZER_RATE_SHIFT_3) |
| |
| |
| st.session_state.setdefault('selected_employee_types', ["UNICEF Fixed term", "Humanizer"]) |
| st.session_state.setdefault('selected_shifts', [ShiftType.REGULAR, ShiftType.OVERTIME]) |
| st.session_state.setdefault('selected_lines', [LineType.LONG_LINE, LineType.MINI_LOAD]) |
|
|
| def render_schedule_config(): |
| """Render schedule configuration section""" |
| st.header("π
Schedule Configuration") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.date_input( |
| "Start Date", |
| value=datetime.date(2025, 7, 7), |
| key='start_date', |
| help="Exact start date to filter demand data - will only use orders that start on this specific date" |
| ) |
| |
| with col2: |
| st.info("π‘ **Date Filtering**: System will use only demand data that starts on the exact date you select.") |
|
|
| |
| |
| st.subheader("π Evening Shift Configuration") |
| |
| shift_mode_options = ['normal', 'activate_evening', 'always_available'] |
| |
| st.selectbox( |
| "Evening Shift Mode", |
| options=shift_mode_options, |
| index=shift_mode_options.index(DefaultConfig.EVENING_SHIFT_MODE), |
| key='evening_shift_mode', |
| help=""" |
| - **Normal**: Only regular shift (1) and overtime shift (3) |
| - **Activate Evening**: Allow evening shift (2) when demand is high |
| - **Always Available**: Evening shift always available as option |
| """ |
| ) |
| |
| if st.session_state.evening_shift_mode == 'activate_evening': |
| st.slider( |
| "Evening Shift Activation Threshold", |
| min_value=0.1, |
| max_value=1.0, |
| value=DefaultConfig.EVENING_SHIFT_DEMAND_THRESHOLD, |
| key='evening_shift_threshold', |
| step=0.1, |
| help="Activate evening shift if regular+overtime capacity < threshold of demand" |
| ) |
|
|
| def render_workforce_config(): |
| """Render workforce configuration section""" |
| st.header("π₯ Workforce Configuration") |
| |
| |
| staff_mode_options = ['mandatory', 'available', 'priority', 'none'] |
| |
| st.selectbox( |
| "Fixed Staff Constraint Mode", |
| options=staff_mode_options, |
| index=staff_mode_options.index(DefaultConfig.FIXED_STAFF_MODE), |
| key='fixed_staff_mode', |
| help=""" |
| - **Mandatory**: Forces all fixed staff to work full hours every day |
| - **Available**: Staff available up to limits but not forced |
| - **Priority**: Fixed staff used first, then temporary staff (recommended) |
| - **None**: Purely demand-driven scheduling |
| """ |
| ) |
| |
| |
| st.subheader("π¨βπΌ Daily Workforce Limits") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.number_input( |
| "Max UNICEF Fixed Term per Day", |
| min_value=1, |
| max_value=50, |
| value=DefaultConfig.MAX_UNICEF_PER_DAY, |
| key='max_unicef_per_day', |
| help="Maximum number of UNICEF fixed term employees per day" |
| ) |
| |
| with col2: |
| st.number_input( |
| "Max Humanizer per Day", |
| min_value=1, |
| max_value=50, |
| value=DefaultConfig.MAX_HUMANIZER_PER_DAY, |
| key='max_humanizer_per_day', |
| help="Maximum number of Humanizer employees per day" |
| ) |
| |
| |
| st.subheader("π Fixed Minimum Requirements") |
| |
| st.number_input( |
| "Fixed Minimum UNICEF per Day", |
| min_value=0, |
| max_value=20, |
| value=DefaultConfig.FIXED_MIN_UNICEF_PER_DAY, |
| key='fixed_min_unicef_per_day', |
| help="Minimum number of UNICEF Fixed term employees required every working day (constraint)" |
| ) |
| |
| |
| st.subheader("β° Working Hours Configuration") |
| |
| st.number_input( |
| "Max Hours per Person per Day", |
| min_value=1, |
| max_value=24, |
| value=DefaultConfig.MAX_HOUR_PER_PERSON_PER_DAY, |
| key='max_hour_per_person_per_day', |
| help="Legal maximum working hours per person per day" |
| ) |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.number_input( |
| "Max Hours - Shift 1 (Regular)", |
| min_value=1.0, |
| max_value=12.0, |
| value=float(DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.REGULAR]), |
| key='max_hours_shift_1', |
| step=0.5, |
| help="Maximum hours per person for regular shift" |
| ) |
| |
| with col2: |
| st.number_input( |
| "Max Hours - Shift 2 (Evening)", |
| min_value=1.0, |
| max_value=12.0, |
| value=float(DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.EVENING]), |
| key='max_hours_shift_2', |
| step=0.5, |
| help="Maximum hours per person for evening shift" |
| ) |
| |
| with col3: |
| st.number_input( |
| "Max Hours - Shift 3 (Overtime)", |
| min_value=1.0, |
| max_value=12.0, |
| value=float(DefaultConfig.MAX_HOUR_PER_SHIFT_PER_PERSON[ShiftType.OVERTIME]), |
| key='max_hours_shift_3', |
| step=0.5, |
| help="Maximum hours per person for overtime shift" |
| ) |
|
|
| def render_operations_config(): |
| """Render operations configuration section""" |
| st.header("π Operations Configuration") |
| |
| |
| st.subheader("π¦ Production Line Configuration") |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.number_input( |
| "Number of Long Lines", |
| min_value=1, |
| max_value=20, |
| value=DefaultConfig.LINE_COUNT_LONG_LINE, |
| key='line_count_long_line', |
| help="Number of long line production lines available" |
| ) |
| |
| st.number_input( |
| "Max Workers per Long Line", |
| min_value=1, |
| max_value=50, |
| value=DefaultConfig.MAX_PARALLEL_WORKERS_LONG_LINE, |
| key='max_parallel_workers_long_line', |
| help="Maximum number of workers that can work simultaneously on a long line" |
| ) |
| |
| with col2: |
| st.number_input( |
| "Number of Mini Load Lines", |
| min_value=1, |
| max_value=20, |
| value=DefaultConfig.LINE_COUNT_MINI_LOAD, |
| key='line_count_mini_load', |
| help="Number of mini load production lines available" |
| ) |
| |
| st.number_input( |
| "Max Workers per Mini Load Line", |
| min_value=1, |
| max_value=50, |
| value=DefaultConfig.MAX_PARALLEL_WORKERS_MINI_LOAD, |
| key='max_parallel_workers_mini_load', |
| help="Maximum number of workers that can work simultaneously on a mini load line" |
| ) |
|
|
| def render_cost_config(): |
| """Render cost configuration section""" |
| st.header("π° Cost Configuration") |
| |
| |
| st.subheader("π³ Payment Mode Configuration") |
| |
| st.markdown(""" |
| **Payment Modes:** |
| - **Bulk**: If employee works any hours in shift, pay for full shift hours |
| - **Partial**: Pay only for actual hours worked |
| """) |
| |
| col1, col2, col3 = st.columns(3) |
| |
| payment_options = ['bulk', 'partial'] |
| |
| with col1: |
| st.selectbox( |
| "Shift 1 (Regular) Payment", |
| options=payment_options, |
| index=payment_options.index(DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.REGULAR]), |
| key='payment_mode_shift_1', |
| help="Payment mode for regular shift" |
| ) |
| |
| with col2: |
| st.selectbox( |
| "Shift 2 (Evening) Payment", |
| options=payment_options, |
| index=payment_options.index(DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.EVENING]), |
| key='payment_mode_shift_2', |
| help="Payment mode for evening shift" |
| ) |
| |
| with col3: |
| st.selectbox( |
| "Shift 3 (Overtime) Payment", |
| options=payment_options, |
| index=payment_options.index(DefaultConfig.PAYMENT_MODE_CONFIG[ShiftType.OVERTIME]), |
| key='payment_mode_shift_3', |
| help="Payment mode for overtime shift" |
| ) |
| |
| |
| st.subheader("π΅ Hourly Rates Configuration") |
| |
| st.markdown("**UNICEF Fixed Term Hourly Rates:**") |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.number_input( |
| "Shift 1 (Regular) - UNICEF", |
| min_value=0.0, |
| max_value=200.0, |
| value=float(DefaultConfig.UNICEF_RATE_SHIFT_1), |
| key='unicef_rate_shift_1', |
| step=0.01, |
| format="%.2f", |
| help="Hourly rate for UNICEF Fixed Term staff during regular shift" |
| ) |
| |
| with col2: |
| st.number_input( |
| "Shift 2 (Evening) - UNICEF", |
| min_value=0.0, |
| max_value=200.0, |
| value=float(DefaultConfig.UNICEF_RATE_SHIFT_2), |
| key='unicef_rate_shift_2', |
| step=0.01, |
| format="%.2f", |
| help="Hourly rate for UNICEF Fixed Term staff during evening shift" |
| ) |
| |
| with col3: |
| st.number_input( |
| "Shift 3 (Overtime) - UNICEF", |
| min_value=0.0, |
| max_value=200.0, |
| value=float(DefaultConfig.UNICEF_RATE_SHIFT_3), |
| key='unicef_rate_shift_3', |
| step=0.01, |
| format="%.2f", |
| help="Hourly rate for UNICEF Fixed Term staff during overtime shift" |
| ) |
| |
| st.markdown("**Humanizer Hourly Rates:**") |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.number_input( |
| "Shift 1 (Regular) - Humanizer", |
| min_value=0.0, |
| max_value=200.0, |
| value=float(DefaultConfig.HUMANIZER_RATE_SHIFT_1), |
| key='humanizer_rate_shift_1', |
| step=0.01, |
| format="%.2f", |
| help="Hourly rate for Humanizer staff during regular shift" |
| ) |
| |
| with col2: |
| st.number_input( |
| "Shift 2 (Evening) - Humanizer", |
| min_value=0.0, |
| max_value=200.0, |
| value=float(DefaultConfig.HUMANIZER_RATE_SHIFT_2), |
| key='humanizer_rate_shift_2', |
| step=0.01, |
| format="%.2f", |
| help="Hourly rate for Humanizer staff during evening shift" |
| ) |
| |
| with col3: |
| st.number_input( |
| "Shift 3 (Overtime) - Humanizer", |
| min_value=0.0, |
| max_value=200.0, |
| value=float(DefaultConfig.HUMANIZER_RATE_SHIFT_3), |
| key='humanizer_rate_shift_3', |
| step=0.01, |
| format="%.2f", |
| help="Hourly rate for Humanizer staff during overtime shift" |
| ) |
|
|
| def render_data_selection_config(): |
| """Render data selection configuration section""" |
| st.header("π Data Selection Configuration") |
| |
| st.markdown("Configure which data elements to include in the optimization.") |
| |
| |
| st.subheader("π₯ Employee Types") |
| available_employee_types = ["UNICEF Fixed term", "Humanizer"] |
| |
| selected_employee_types = st.multiselect( |
| "Select Employee Types to Include", |
| available_employee_types, |
| default=st.session_state.selected_employee_types, |
| help="Choose which employee types to include in the optimization" |
| ) |
| st.session_state.selected_employee_types = selected_employee_types |
| |
| |
| st.subheader("π Shifts") |
| available_shifts = list(optimization_config.shift_code_to_name().keys()) |
| shift_names = optimization_config.shift_code_to_name() |
| |
| selected_shifts = st.multiselect( |
| "Select Shifts to Include", |
| available_shifts, |
| default=st.session_state.selected_shifts, |
| format_func=lambda x: f"Shift {x} ({shift_names[x]})", |
| help="Choose which shifts to include in the optimization" |
| ) |
| st.session_state.selected_shifts = selected_shifts |
| |
| |
| st.subheader("π Production Lines") |
| available_lines = list(optimization_config.line_code_to_name().keys()) |
| line_names = optimization_config.line_code_to_name() |
| |
| selected_lines = st.multiselect( |
| "Select Production Lines to Include", |
| available_lines, |
| default=st.session_state.selected_lines, |
| format_func=lambda x: f"Line {x} ({line_names[x]})", |
| help="Choose which production lines to include in the optimization" |
| ) |
| st.session_state.selected_lines = selected_lines |
| |
| |
| if not selected_employee_types: |
| st.error("β οΈ At least one employee type must be selected!") |
| if not selected_shifts: |
| st.error("β οΈ At least one shift must be selected!") |
| if not selected_lines: |
| st.error("β οΈ At least one production line must be selected!") |
|
|
| def save_configuration(): |
| """Save current configuration to session state and potentially to file""" |
| |
| |
| try: |
| import src.preprocess.extract as extract |
| import pandas as pd |
| from datetime import datetime |
| |
| |
| start_datetime = datetime.combine(st.session_state.start_date, datetime.min.time()) |
| demand_data = extract.read_orders_data(start_date=start_datetime) |
| |
| if not demand_data.empty: |
| |
| finish_dates = pd.to_datetime(demand_data["Basic finish date"]).dt.date.unique() |
| finish_dates = sorted(finish_dates) |
| |
| if finish_dates: |
| calculated_end_date = max(finish_dates) |
| calculated_days = (calculated_end_date - st.session_state.start_date).days + 1 |
| st.info(f"π Found demand data starting on {st.session_state.start_date} ending on {calculated_end_date} ({calculated_days} days, {len(demand_data)} orders)") |
| else: |
| raise Exception("No finish date found for start date") |
| |
| |
| else: |
| |
| |
| st.warning(f"β οΈ No demand data found for start date {st.session_state.start_date}") |
| raise Exception("No demand data found for start date") |
| except Exception as e: |
| st.warning(f"Could not determine date range from data: {e}. Using default 5-day period.") |
| raise Exception("Could not determine date range from data") |
| |
| |
| |
| |
| |
| |
| st.session_state.end_date = calculated_end_date |
| st.session_state.planning_days = calculated_days |
| |
| |
| config = { |
| 'date_range': { |
| 'start_date': st.session_state.start_date, |
| 'end_date': st.session_state.end_date, |
| 'planning_days': st.session_state.planning_days, |
| }, |
| 'schedule_type': st.session_state.schedule_type, |
| 'evening_shift_mode': st.session_state.evening_shift_mode, |
| 'evening_shift_threshold': st.session_state.evening_shift_threshold, |
| 'fixed_staff_mode': st.session_state.fixed_staff_mode, |
| 'payment_mode_config': { |
| ShiftType.REGULAR: st.session_state.payment_mode_shift_1, |
| ShiftType.EVENING: st.session_state.payment_mode_shift_2, |
| ShiftType.OVERTIME: st.session_state.payment_mode_shift_3, |
| }, |
| 'workforce_limits': { |
| 'max_unicef_per_day': st.session_state.max_unicef_per_day, |
| 'max_humanizer_per_day': st.session_state.max_humanizer_per_day, |
| 'fixed_min_unicef_per_day': st.session_state.fixed_min_unicef_per_day, |
| }, |
| 'working_hours': { |
| 'max_hour_per_person_per_day': st.session_state.max_hour_per_person_per_day, |
| 'max_hours_per_shift': { |
| ShiftType.REGULAR: st.session_state.max_hours_shift_1, |
| ShiftType.EVENING: st.session_state.max_hours_shift_2, |
| ShiftType.OVERTIME: st.session_state.max_hours_shift_3, |
| } |
| }, |
| 'operations': { |
| 'line_counts': { |
| LineType.LONG_LINE: st.session_state.line_count_long_line, |
| LineType.MINI_LOAD: st.session_state.line_count_mini_load, |
| }, |
| 'max_parallel_workers': { |
| LineType.LONG_LINE: st.session_state.max_parallel_workers_long_line, |
| LineType.MINI_LOAD: st.session_state.max_parallel_workers_mini_load, |
| } |
| }, |
| 'cost_rates': { |
| 'UNICEF Fixed term': { |
| ShiftType.REGULAR: st.session_state.unicef_rate_shift_1, |
| ShiftType.EVENING: st.session_state.unicef_rate_shift_2, |
| ShiftType.OVERTIME: st.session_state.unicef_rate_shift_3, |
| }, |
| 'Humanizer': { |
| ShiftType.REGULAR: st.session_state.humanizer_rate_shift_1, |
| ShiftType.EVENING: st.session_state.humanizer_rate_shift_2, |
| ShiftType.OVERTIME: st.session_state.humanizer_rate_shift_3, |
| } |
| }, |
| 'data_selection': { |
| 'selected_employee_types': st.session_state.get('selected_employee_types', []), |
| 'selected_shifts': st.session_state.get('selected_shifts', []), |
| 'selected_lines': st.session_state.get('selected_lines', []), |
| } |
| } |
| |
| |
| date_span_length = st.session_state.planning_days |
| date_span = list(range(1, date_span_length + 1)) |
| |
| |
| st.session_state.line_counts = config['operations']['line_counts'] |
| st.session_state.cost_list_per_emp_shift = config['cost_rates'] |
| st.session_state.payment_mode_config = config['payment_mode_config'] |
| st.session_state.max_employee_per_type_on_day = { |
| "UNICEF Fixed term": {t: st.session_state.max_unicef_per_day for t in date_span}, |
| "Humanizer": {t: st.session_state.max_humanizer_per_day for t in date_span} |
| } |
| |
| |
| st.session_state.optimization_config = config |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| return config |
|
|
| def display_user_friendly_summary(config): |
| """Display a user-friendly summary of the configuration settings""" |
| |
| |
| st.subheader("π
Schedule Settings") |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| st.write(f"**Start Date:** {config['date_range']['start_date']}") |
| with col2: |
| st.write(f"**Planning Period:** {config['date_range']['planning_days']} days") |
| with col3: |
| st.write(f"**End Date:** {config['date_range']['end_date']}") |
| with col4: |
| st.write(f"**Schedule Type:** {config['schedule_type'].title()}") |
| |
| |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| st.write(f"**Evening Shift Mode:** {config['evening_shift_mode'].replace('_', ' ').title()}") |
| |
| |
| if config['evening_shift_mode'] == 'activate_evening': |
| with col2: |
| st.write(f"**Evening Shift Threshold:** {config['evening_shift_threshold']:.0%} demand capacity") |
| |
| |
| st.subheader("π₯ Workforce Settings") |
| col1, col2 = st.columns(2) |
| with col1: |
| st.write(f"**Max UNICEF Staff per Day:** {config['workforce_limits']['max_unicef_per_day']} people") |
| st.write(f"**Max Humanizer Staff per Day:** {config['workforce_limits']['max_humanizer_per_day']} people") |
| st.write(f"**Fixed Minimum UNICEF per Day:** {config['workforce_limits']['fixed_min_unicef_per_day']} people") |
| with col2: |
| st.write(f"**Staff Management Mode:** {config['fixed_staff_mode'].replace('_', ' ').title()}") |
| st.write(f"**Max Hours per Person per Day:** {config['working_hours']['max_hour_per_person_per_day']} hours") |
| |
| |
| st.subheader("π Operations Settings") |
| col1, col2 = st.columns(2) |
| with col1: |
| st.write(f"**Long Lines Available:** {config['operations']['line_counts'][LineType.LONG_LINE]} lines") |
| st.write(f"**Mini Load Lines Available:** {config['operations']['line_counts'][LineType.MINI_LOAD]} lines") |
| with col2: |
| st.write(f"**Max Workers per Long Line:** {config['operations']['max_parallel_workers'][LineType.LONG_LINE]} people") |
| st.write(f"**Max Workers per Mini Load Line:** {config['operations']['max_parallel_workers'][LineType.MINI_LOAD]} people") |
| |
| |
| st.subheader("π° Cost Settings") |
| st.write("**Hourly Rates:**") |
| |
| col1, col2 = st.columns(2) |
| with col1: |
| st.write("*UNICEF Fixed Term Staff:*") |
| st.write(f"β’ Regular Shift: β¬{config['cost_rates']['UNICEF Fixed term'][ShiftType.REGULAR]:.2f}/hour") |
| st.write(f"β’ Evening Shift: β¬{config['cost_rates']['UNICEF Fixed term'][ShiftType.EVENING]:.2f}/hour") |
| st.write(f"β’ Overtime Shift: β¬{config['cost_rates']['UNICEF Fixed term'][ShiftType.OVERTIME]:.2f}/hour") |
| |
| with col2: |
| st.write("*Humanizer Staff:*") |
| st.write(f"β’ Regular Shift: β¬{config['cost_rates']['Humanizer'][ShiftType.REGULAR]:.2f}/hour") |
| st.write(f"β’ Evening Shift: β¬{config['cost_rates']['Humanizer'][ShiftType.EVENING]:.2f}/hour") |
| st.write(f"β’ Overtime Shift: β¬{config['cost_rates']['Humanizer'][ShiftType.OVERTIME]:.2f}/hour") |
| |
| |
| st.write("**Payment Modes:**") |
| payment_descriptions = { |
| 'bulk': 'Full shift payment (even for partial hours)', |
| 'partial': 'Pay only for actual hours worked' |
| } |
| |
| col1, col2, col3 = st.columns(3) |
| with col1: |
| mode = config['payment_mode_config'][ShiftType.REGULAR] |
| st.write(f"β’ **Regular Shift:** {mode.title()}") |
| st.caption(payment_descriptions[mode]) |
| with col2: |
| mode = config['payment_mode_config'][ShiftType.EVENING] |
| st.write(f"β’ **Evening Shift:** {mode.title()}") |
| st.caption(payment_descriptions[mode]) |
| with col3: |
| mode = config['payment_mode_config'][ShiftType.OVERTIME] |
| st.write(f"β’ **Overtime Shift:** {mode.title()}") |
| st.caption(payment_descriptions[mode]) |
| |
| |
| if 'data_selection' in config: |
| st.subheader("π Data Selection") |
| col1, col2, col3 = st.columns(3) |
| with col1: |
| employee_types = config['data_selection']['selected_employee_types'] |
| st.write(f"**Employee Types:** {len(employee_types)} selected") |
| for emp_type in employee_types: |
| st.write(f"β’ {emp_type}") |
| |
| with col2: |
| shifts = config['data_selection']['selected_shifts'] |
| shift_names = ShiftType.get_all_names() |
| st.write(f"**Shifts:** {len(shifts)} selected") |
| for shift in shifts: |
| st.write(f"β’ Shift {shift} ({shift_names.get(shift, 'Unknown')})") |
| |
| with col3: |
| lines = config['data_selection']['selected_lines'] |
| line_names = LineType.get_all_names() |
| st.write(f"**Production Lines:** {len(lines)} selected") |
| for line in lines: |
| st.write(f"β’ Line {line} ({line_names.get(line, 'Unknown')})") |
| |
| |
| st.subheader("π Quick Summary") |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| duration = config['date_range']['planning_days'] |
| st.metric("Planning Period", f"{duration} days") |
| |
| with col2: |
| total_staff = config['workforce_limits']['max_unicef_per_day'] + config['workforce_limits']['max_humanizer_per_day'] |
| st.metric("Max Daily Staff", f"{total_staff} people") |
| |
| with col3: |
| total_lines = config['operations']['line_counts'][LineType.LONG_LINE] + config['operations']['line_counts'][LineType.MINI_LOAD] |
| st.metric("Production Lines", f"{total_lines} lines") |
| |
| with col4: |
| avg_unicef_rate = sum(config['cost_rates']['UNICEF Fixed term'].values()) / 3 |
| st.metric("Avg UNICEF Rate", f"β¬{avg_unicef_rate:.2f}/hr") |
| |
| |
| st.subheader("π Demand Data Preview") |
| try: |
| import src.preprocess.extract as extract |
| from datetime import datetime |
| |
| |
| start_date = config['date_range']['start_date'] |
| start_datetime = datetime.combine(start_date, datetime.min.time()) |
| demand_data = extract.read_orders_data(start_date=start_datetime) |
| |
| if not demand_data.empty: |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| with col1: |
| st.metric("π¦ Total Orders", len(demand_data)) |
| |
| with col2: |
| total_quantity = demand_data['Order quantity (GMEIN)'].sum() |
| st.metric("π Total Quantity", total_quantity) |
| |
| with col3: |
| unique_products = demand_data['Material Number'].nunique() |
| st.metric("π·οΈ Unique Products", unique_products) |
| |
| with col4: |
| |
| if 'Basic finish date' in demand_data.columns: |
| import pandas as pd |
| finish_dates = pd.to_datetime(demand_data['Basic finish date']).dt.date |
| min_finish = finish_dates.min() |
| max_finish = finish_dates.max() |
| if min_finish == max_finish: |
| date_range = f"{min_finish}" |
| else: |
| date_range = f"{min_finish} to {max_finish}" |
| else: |
| date_range = "N/A" |
| st.metric("π
Finish Dates for the start date orders", date_range) |
| |
| |
| if 'Material' in demand_data.columns and 'Quantity' in demand_data.columns: |
| with st.expander("π Top 10 Products by Quantity", expanded=False): |
| top_products = demand_data.groupby('Material')['Quantity'].sum().sort_values(ascending=False).head(10) |
| |
| if not top_products.empty: |
| |
| import pandas as pd |
| top_products_df = pd.DataFrame({ |
| 'Product': top_products.index, |
| 'Total Quantity': top_products.values |
| }).reset_index(drop=True) |
| top_products_df.index = top_products_df.index + 1 |
| st.dataframe(top_products_df, use_container_width=True) |
| else: |
| st.info("No product quantity data available.") |
| |
| else: |
| st.warning(f"β οΈ No demand data found for {start_date}. Please select a different date or check your data files.") |
| |
| except Exception as e: |
| st.error(f"β Error loading demand data: {e}") |
|
|
| def clear_all_cache_and_results(): |
| """Clear all cached data, modules, and results from previous runs""" |
| import importlib |
| import sys |
| |
| st.info("π§Ή Clearing all cached data and previous results...") |
| |
| |
| clear_optimization_cache() |
| |
| |
| keys_to_clear = [ |
| 'demand_dictionary', |
| 'kit_hierarchy_data', |
| 'team_requirements' |
| ] |
| |
| cleared_keys = [] |
| for key in keys_to_clear: |
| if key in st.session_state: |
| del st.session_state[key] |
| cleared_keys.append(key) |
| |
| if cleared_keys: |
| st.write(f"ποΈ Cleared session state: {', '.join(cleared_keys)}") |
| |
| |
| modules_to_reload = [ |
| 'src.preprocess.extract', |
| 'src.preprocess.transform', |
| 'src.config.optimization_config', |
| 'src.models.optimizer_real' |
| ] |
| |
| reloaded_modules = [] |
| for module_name in modules_to_reload: |
| if module_name in sys.modules: |
| importlib.reload(sys.modules[module_name]) |
| reloaded_modules.append(module_name) |
| |
| if reloaded_modules: |
| st.write(f"π Reloaded modules: {', '.join(reloaded_modules)}") |
| |
| |
| if 'end_date' in st.session_state and 'planning_days' in st.session_state: |
| calculated_end_date = st.session_state.end_date |
| planning_days = st.session_state.planning_days |
| else: |
| |
| from datetime import timedelta |
| calculated_end_date = st.session_state.start_date + timedelta(days=4) |
| planning_days = 5 |
| |
| |
| |
| st.write(f"π
Date configuration: {st.session_state.start_date} to {calculated_end_date} ({planning_days} days)") |
| st.info("π‘ Dates will be applied when optimization runs (via optimization_config.get_date_span())") |
| |
| st.success("β
All caches and previous results cleared!") |
|
|
| def run_optimization(): |
| """Run the optimization model and store results""" |
| try: |
| st.info("π Running optimization... This may take a few moments.") |
| |
| |
| |
| clear_all_cache_and_results() |
| |
| |
| st.info("π Performing pre-optimization data validation...") |
| validation_warnings = check_critical_data_issues() |
| if validation_warnings: |
| with st.expander("β οΈ Data Validation Warnings", expanded=True): |
| for warning in validation_warnings: |
| st.warning(f"β’ {warning}") |
| st.warning("**Optimization may fail due to these issues. Consider fixing them first.**") |
| else: |
| st.success("β
Data validation passed - no critical issues detected") |
| |
| |
| if 'end_date' in st.session_state and 'planning_days' in st.session_state: |
| calculated_end_date = st.session_state.end_date |
| planning_days = st.session_state.planning_days |
| st.info(f"ποΈ Planning period: {st.session_state.start_date} to {calculated_end_date} ({planning_days} days)") |
| else: |
| st.info(f"ποΈ Start date: {st.session_state.start_date} (will determine end date from demand data)") |
| |
| st.info(f"π₯ Max UNICEF/day: {st.session_state.max_unicef_per_day}, Max Humanizer/day: {st.session_state.max_humanizer_per_day}") |
| |
| |
| from src.models.optimizer_real import Optimizer |
| |
| |
| with st.spinner('Optimizing workforce schedule...'): |
| optimizer = Optimizer() |
| results = optimizer.run_optimization() |
| |
| if results is None: |
| st.error("β Optimization failed! The problem may be infeasible with current settings.") |
| if validation_warnings: |
| st.error("π‘ **Likely cause:** Data validation warnings detected above. Fix missing line assignments and other data issues first.") |
| st.error("Try adjusting your workforce limits, line counts, or evening shift settings.") |
| return |
| |
| |
| st.session_state.optimization_results = results |
| st.success("β
Optimization completed successfully!") |
| st.rerun() |
| |
| except Exception as e: |
| import traceback |
| st.error(f"β Error during optimization: {str(e)}") |
| st.error("Please check your settings and data files.") |
| |
| |
| with st.expander("π Show detailed error traceback"): |
| st.code(traceback.format_exc()) |
| |
| |
| print("\n" + "="*60) |
| print("β OPTIMIZATION ERROR - FULL TRACEBACK:") |
| print("="*60) |
| traceback.print_exc() |
| print("="*60) |
|
|
| def check_critical_data_issues(): |
| """ |
| Quick check for critical data issues that could cause optimization failure |
| Returns list of warning messages |
| """ |
| warnings = [] |
| |
| try: |
| |
| from src.demand_validation_viz import DemandValidationViz |
| |
| |
| validator = DemandValidationViz() |
| if not validator.load_data(): |
| warnings.append("Failed to load validation data") |
| return warnings |
| |
| |
| validation_df = validator.validate_all_products() |
| summary_stats = validator.get_summary_statistics(validation_df) |
| |
| |
| if summary_stats['no_line_assignment'] > 0: |
| warnings.append(f"{summary_stats['no_line_assignment']} products missing line assignments") |
| |
| if summary_stats['no_staffing'] > 0: |
| warnings.append(f"{summary_stats['no_staffing']} products missing staffing requirements") |
| |
| if summary_stats['no_speed'] > 0: |
| warnings.append(f"{summary_stats['no_speed']} products missing production speed data") |
| |
| |
| |
| |
| |
| |
| return warnings |
| |
| except Exception as e: |
| warnings.append(f"Validation check failed: {str(e)}") |
| return warnings |
|
|
| def display_optimization_results(results): |
| """Import and display optimization results""" |
| from ui.pages.optimization_results import display_optimization_results as display_results |
| display_results(results) |
|
|