HaLim commited on
Commit
f73a608
Β·
1 Parent(s): 42b5ea5

Delete unncessesary files

Browse files
Home.py DELETED
@@ -1,236 +0,0 @@
1
- import streamlit as st
2
-
3
- # Page configuration - MUST be first Streamlit command
4
- st.set_page_config(
5
- page_title="SD Roster Tool - Home",
6
- page_icon="🏠",
7
- layout="wide",
8
- initial_sidebar_state="expanded"
9
- )
10
-
11
- # Now import everything else
12
- import pandas as pd
13
- import sys
14
- import os
15
- from datetime import datetime
16
-
17
- # Add src to path for imports
18
- sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
19
-
20
- # Custom CSS for better styling
21
- st.markdown("""
22
- <style>
23
- .main-header {
24
- font-size: 3rem;
25
- font-weight: bold;
26
- color: #1f77b4;
27
- margin-bottom: 2rem;
28
- text-align: center;
29
- }
30
- .section-header {
31
- font-size: 1.8rem;
32
- font-weight: bold;
33
- color: #2c3e50;
34
- margin: 1.5rem 0;
35
- }
36
- .feature-card {
37
- background-color: #ffffff;
38
- padding: 1.5rem;
39
- border-radius: 0.8rem;
40
- border-left: 5px solid #1f77b4;
41
- margin-bottom: 1.5rem;
42
- box-shadow: 0 2px 4px rgba(0,0,0,0.15);
43
- color: #2c3e50;
44
- border: 1px solid #e9ecef;
45
- }
46
- .feature-card h3 {
47
- color: #1f77b4;
48
- margin-top: 0;
49
- }
50
- .feature-card p {
51
- color: #2c3e50;
52
- }
53
- .feature-card ul {
54
- color: #2c3e50;
55
- }
56
- .navigation-button {
57
- width: 100%;
58
- height: 80px;
59
- font-size: 1.2rem;
60
- margin: 10px 0;
61
- }
62
- </style>
63
- """, unsafe_allow_html=True)
64
-
65
- # Initialize session state for shared variables
66
- if 'data_path' not in st.session_state:
67
- st.session_state.data_path = "data/my_roster_data"
68
- if 'target_date' not in st.session_state:
69
- st.session_state.target_date = ""
70
-
71
- # Title
72
- st.markdown('<h1 class="main-header">🏠 SD Roster Optimization Tool</h1>', unsafe_allow_html=True)
73
-
74
- # Introduction section
75
- col1, col2 = st.columns([2, 1])
76
-
77
- with col1:
78
- st.markdown("""
79
- ## πŸ“‹ Welcome to the Supply Chain Roster Optimization Tool
80
-
81
- This comprehensive tool helps you optimize workforce allocation and production scheduling
82
- using advanced mathematical optimization techniques. Navigate through the different sections
83
- to analyze your data and run optimizations.
84
-
85
- ### πŸ”§ Key Features:
86
- - **Advanced Optimization Engine**: Built on Google OR-Tools for mixed-integer programming
87
- - **Multi-constraint Support**: Handle complex business rules and staffing requirements
88
- - **Real-time Data Integration**: Work with your existing CSV data files
89
- - **Interactive Visualizations**: Rich charts and analytics for decision making
90
- - **Flexible Configuration**: Adjust parameters for different business scenarios
91
- """)
92
-
93
- with col2:
94
- st.markdown("### πŸš€ Quick Start")
95
-
96
- # Navigation buttons
97
- if st.button("πŸ“Š View Dataset Metadata", key="nav_metadata", help="Explore your data overview"):
98
- st.switch_page("pages/1_πŸ“Š_Dataset_Metadata.py")
99
-
100
- if st.button("🎯 Run Optimization", key="nav_optimization", help="Configure and run optimization"):
101
- st.switch_page("pages/2_🎯_Optimization.py")
102
-
103
- if st.button("πŸ“ˆ Enhanced Reports", key="nav_reports", help="View comprehensive reports and analytics"):
104
- st.switch_page("pages/3_πŸ“ˆ_Enhanced_Reports.py")
105
-
106
- # Global settings section
107
- st.markdown("---")
108
- st.markdown('<h2 class="section-header">🌐 Global Settings</h2>', unsafe_allow_html=True)
109
-
110
- col_set1, col_set2 = st.columns(2)
111
-
112
- with col_set1:
113
- st.markdown("### πŸ“ Data Configuration")
114
- new_data_path = st.text_input(
115
- "Data Path",
116
- value=st.session_state.data_path,
117
- help="Path to your CSV data files. This setting is shared across all pages."
118
- )
119
-
120
- if new_data_path != st.session_state.data_path:
121
- st.session_state.data_path = new_data_path
122
- st.success("βœ… Data path updated globally!")
123
-
124
- st.info(f"**Current data path:** `{st.session_state.data_path}`")
125
-
126
- with col_set2:
127
- st.markdown("### πŸ“… Date Configuration")
128
-
129
- # Try to load available dates
130
- try:
131
- sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
132
- import src.etl.transform as transform
133
-
134
- date_ranges = transform.get_date_ranges()
135
- if date_ranges:
136
- date_range_options = [""] + [f"{start.strftime('%Y-%m-%d')} to {end.strftime('%Y-%m-%d')}" for start, end in date_ranges]
137
- selected_range_str = st.selectbox(
138
- "Available Date Ranges:",
139
- options=date_range_options,
140
- help="Select from available date ranges in your data"
141
- )
142
-
143
- if selected_range_str:
144
- selected_index = date_range_options.index(selected_range_str) - 1
145
- start_date, end_date = date_ranges[selected_index]
146
- st.session_state.date_range = (start_date, end_date)
147
- st.success(f"βœ… Selected: {start_date} to {end_date}")
148
-
149
- except Exception as e:
150
- st.warning(f"Could not load date ranges: {e}")
151
- st.info("Date ranges will be available when data is properly configured.")
152
-
153
- # Overview sections
154
- st.markdown("---")
155
-
156
- col_info1, col_info2, col_info3 = st.columns(3)
157
-
158
- with col_info1:
159
- st.markdown("""
160
- <div class="feature-card">
161
- <h3>πŸ“Š Dataset Metadata</h3>
162
- <p>Comprehensive overview of your data including:</p>
163
- <ul>
164
- <li>Demand analysis and forecasting</li>
165
- <li>Employee availability and costs</li>
166
- <li>Production line capacities</li>
167
- <li>Historical performance data</li>
168
- </ul>
169
- </div>
170
- """, unsafe_allow_html=True)
171
-
172
- with col_info2:
173
- st.markdown("""
174
- <div class="feature-card">
175
- <h3>🎯 Optimization Engine</h3>
176
- <p>Advanced optimization features:</p>
177
- <ul>
178
- <li>Multi-objective optimization</li>
179
- <li>Constraint satisfaction</li>
180
- <li>Scenario analysis</li>
181
- <li>Cost minimization</li>
182
- </ul>
183
- </div>
184
- """, unsafe_allow_html=True)
185
-
186
- with col_info3:
187
- st.markdown("""
188
- <div class="feature-card">
189
- <h3>πŸ“ˆ Enhanced Reports</h3>
190
- <p>Comprehensive visualization and reporting:</p>
191
- <ul>
192
- <li>Employee costs per hour analysis</li>
193
- <li>Production plans & orders tracking</li>
194
- <li>Line allocation by day visualization</li>
195
- <li>Total cost breakdowns & scenarios</li>
196
- </ul>
197
- </div>
198
- """, unsafe_allow_html=True)
199
-
200
- # System status
201
- st.markdown("---")
202
- st.markdown("### πŸ” System Status")
203
-
204
- col_status1, col_status2, col_status3, col_status4 = st.columns(4)
205
-
206
- # Check system components
207
- try:
208
- import ortools
209
- ortools_status = "βœ… Available"
210
- except:
211
- ortools_status = "❌ Not installed"
212
-
213
- try:
214
- import plotly
215
- plotly_status = "βœ… Available"
216
- except:
217
- plotly_status = "❌ Not installed"
218
-
219
- data_status = "βœ… Configured" if os.path.exists(st.session_state.data_path) else "⚠️ Path not found"
220
-
221
- with col_status1:
222
- st.metric("OR-Tools", ortools_status)
223
- with col_status2:
224
- st.metric("Plotly", plotly_status)
225
- with col_status3:
226
- st.metric("Data Path", data_status)
227
- with col_status4:
228
- st.metric("Session State", "βœ… Active")
229
-
230
- # Footer
231
- st.markdown("---")
232
- st.markdown("""
233
- <div style='text-align: center; color: gray; padding: 2rem;'>
234
- <small>SD Roster Optimization Tool | Built with Streamlit & OR-Tools | Version 1.0</small>
235
- </div>
236
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docker/init/001_init.sql DELETED
@@ -1,16 +0,0 @@
1
- CREATE SCHEMA IF NOT EXISTS stg;
2
- CREATE SCHEMA IF NOT EXISTS dim;
3
- CREATE SCHEMA IF NOT EXISTS fact;
4
- CREATE SCHEMA IF NOT EXISTS rej;
5
- CREATE SCHEMA IF NOT EXISTS meta;
6
-
7
- CREATE TABLE IF NOT EXISTS meta.batch_log (
8
- batch_id BIGSERIAL PRIMARY KEY,
9
- source_file TEXT NOT NULL,
10
- source_hash TEXT,
11
- rows_read INT,
12
- rows_loaded INT,
13
- rows_rejected INT,
14
- started_at TIMESTAMPTZ DEFAULT NOW(),
15
- finished_at TIMESTAMPTZ
16
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/1_πŸ“Š_Dataset_Metadata.py DELETED
@@ -1,969 +0,0 @@
1
- import streamlit as st
2
-
3
- # Page configuration
4
- st.set_page_config(
5
- page_title="Dataset Metadata",
6
- page_icon="πŸ“Š",
7
- layout="wide"
8
- )
9
-
10
- # Import libraries
11
- import pandas as pd
12
- import plotly.express as px
13
- import plotly.graph_objects as go
14
- from plotly.subplots import make_subplots
15
- import sys
16
- import os
17
- from datetime import datetime, timedelta
18
- import numpy as np
19
-
20
- # Add src to path for imports
21
- sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src'))
22
-
23
- try:
24
- import src.etl.extract as extract
25
- import src.etl.transform as transform
26
- from src.config import optimization_config
27
- except ImportError as e:
28
- st.error(f"Error importing modules: {e}")
29
- st.stop()
30
-
31
- # Custom CSS
32
- st.markdown("""
33
- <style>
34
- .main-header {
35
- font-size: 2.5rem;
36
- font-weight: bold;
37
- color: #1f77b4;
38
- margin-bottom: 1rem;
39
- }
40
- .section-header {
41
- font-size: 1.5rem;
42
- font-weight: bold;
43
- color: #2c3e50;
44
- margin: 1rem 0;
45
- }
46
- .metric-card {
47
- background-color: #f8f9fa;
48
- padding: 1rem;
49
- border-radius: 0.5rem;
50
- border-left: 4px solid #1f77b4;
51
- margin-bottom: 1rem;
52
- }
53
- .info-box {
54
- background-color: #e7f3ff;
55
- padding: 1rem;
56
- border-radius: 0.5rem;
57
- border-left: 4px solid #0066cc;
58
- margin: 1rem 0;
59
- }
60
- </style>
61
- """, unsafe_allow_html=True)
62
-
63
- # Title
64
- st.markdown('<h1 class="main-header">πŸ“Š Dataset Metadata Overview</h1>', unsafe_allow_html=True)
65
-
66
- # Check if data path is available from session state
67
- if 'data_path' not in st.session_state:
68
- st.session_state.data_path = "data/my_roster_data"
69
-
70
- if 'date_range' not in st.session_state:
71
- st.session_state.date_range = None
72
-
73
- # Sidebar for date selection
74
- with st.sidebar:
75
- st.markdown("## πŸ“… Date Selection")
76
-
77
- try:
78
- date_ranges = transform.get_date_ranges()
79
- if date_ranges:
80
- date_range_options = [f"{start.strftime('%Y-%m-%d')} to {end.strftime('%Y-%m-%d')}" for start, end in date_ranges]
81
- selected_range_str = st.selectbox(
82
- "Select date range:",
83
- options=date_range_options,
84
- help="Available date ranges from released orders"
85
- )
86
-
87
- selected_index = date_range_options.index(selected_range_str)
88
- start_date, end_date = date_ranges[selected_index]
89
- st.session_state.date_range = (start_date, end_date)
90
-
91
- duration = (end_date - start_date).days + 1
92
- st.info(f"Duration: {duration} days")
93
-
94
- else:
95
- st.warning("No date ranges found")
96
- start_date = datetime(2025, 3, 24).date()
97
- end_date = datetime(2025, 3, 28).date()
98
- st.session_state.date_range = (start_date, end_date)
99
-
100
- except Exception as e:
101
- st.error(f"Error loading dates: {e}")
102
- start_date = datetime(2025, 3, 24).date()
103
- end_date = datetime(2025, 3, 28).date()
104
- st.session_state.date_range = (start_date, end_date)
105
-
106
- st.markdown("---")
107
- st.markdown("## πŸ”„ Refresh Data")
108
- if st.button("πŸ”„ Reload All Data"):
109
- st.rerun()
110
-
111
- # Main content
112
- if st.session_state.date_range:
113
- start_date, end_date = st.session_state.date_range
114
- st.markdown(f"**Analysis Period:** {start_date} to {end_date}")
115
- else:
116
- st.warning("No date range selected")
117
- st.stop()
118
-
119
- # Create tabs for different metadata sections
120
- tab1, tab2, tab3, tab4, tab5, tab6, tab7 = st.tabs([
121
- "πŸ“‹ Data Overview",
122
- "πŸ“¦ Demand Analysis",
123
- "πŸ‘₯ Workforce Analysis",
124
- "🏭 Production Capacity",
125
- "πŸ’° Cost Analysis",
126
- "πŸ“ˆ Quick Reports",
127
- "βš™οΈ Optimization Settings"
128
- ])
129
-
130
- # Tab 1: Data Overview
131
- with tab1:
132
- st.markdown('<h2 class="section-header">πŸ“‹ Dataset Overview</h2>', unsafe_allow_html=True)
133
-
134
- # Overall metrics
135
- try:
136
- # Load basic data
137
- demand_df = extract.read_released_orders_data(start_date=start_date, end_date=end_date)
138
- employee_df = extract.read_employee_data()
139
- line_df = extract.read_packaging_line_data()
140
-
141
- # Calculate key metrics
142
- total_orders = len(demand_df)
143
- total_quantity = demand_df["Order quantity (GMEIN)"].sum()
144
- unique_products = demand_df["Material Number"].nunique()
145
- total_employees = len(employee_df)
146
- total_lines = line_df["line_count"].sum()
147
-
148
- # Display key metrics
149
- col1, col2, col3, col4, col5 = st.columns(5)
150
-
151
- with col1:
152
- st.metric("πŸ“¦ Total Orders", f"{total_orders:,}")
153
- with col2:
154
- st.metric("πŸ“Š Total Quantity", f"{total_quantity:,.0f}")
155
- with col3:
156
- st.metric("🎯 Unique Products", unique_products)
157
- with col4:
158
- st.metric("πŸ‘₯ Total Employees", total_employees)
159
- with col5:
160
- st.metric("🏭 Production Lines", total_lines)
161
-
162
- # Data quality overview
163
- st.markdown("### πŸ“ˆ Data Quality Summary")
164
-
165
- col_q1, col_q2 = st.columns(2)
166
-
167
- with col_q1:
168
- # Orders data quality
169
- missing_orders = demand_df.isnull().sum().sum()
170
- completeness = ((demand_df.size - missing_orders) / demand_df.size) * 100
171
-
172
- st.markdown("""
173
- **Orders Data Quality:**
174
- """)
175
- st.progress(completeness / 100)
176
- st.write(f"Completeness: {completeness:.1f}%")
177
- st.write(f"Missing values: {missing_orders}")
178
-
179
- with col_q2:
180
- # Employee data quality
181
- missing_emp = employee_df.isnull().sum().sum()
182
- emp_completeness = ((employee_df.size - missing_emp) / employee_df.size) * 100
183
-
184
- st.markdown("""
185
- **Employee Data Quality:**
186
- """)
187
- st.progress(emp_completeness / 100)
188
- st.write(f"Completeness: {emp_completeness:.1f}%")
189
- st.write(f"Missing values: {missing_emp}")
190
-
191
- # Data freshness
192
- st.markdown("### πŸ“… Data Freshness")
193
- if 'Date' in demand_df.columns:
194
- latest_order = pd.to_datetime(demand_df['Date']).max()
195
- days_old = (datetime.now() - latest_order).days
196
- st.info(f"Latest order data: {latest_order.strftime('%Y-%m-%d')} ({days_old} days ago)")
197
-
198
- except Exception as e:
199
- st.error(f"Error loading overview data: {e}")
200
-
201
- # Tab 2: Demand Analysis
202
- with tab2:
203
- st.markdown('<h2 class="section-header">πŸ“¦ Demand Analysis</h2>', unsafe_allow_html=True)
204
-
205
- try:
206
- demand_df = extract.read_released_orders_data(start_date=start_date, end_date=end_date)
207
-
208
- # Demand summary metrics
209
- col_d1, col_d2, col_d3, col_d4 = st.columns(4)
210
-
211
- total_demand = demand_df["Order quantity (GMEIN)"].sum()
212
- avg_order_size = demand_df["Order quantity (GMEIN)"].mean()
213
- max_order_size = demand_df["Order quantity (GMEIN)"].max()
214
- min_order_size = demand_df["Order quantity (GMEIN)"].min()
215
-
216
- with col_d1:
217
- st.metric("πŸ“Š Total Demand", f"{total_demand:,.0f}")
218
- with col_d2:
219
- st.metric("πŸ“ˆ Avg Order Size", f"{avg_order_size:,.0f}")
220
- with col_d3:
221
- st.metric("πŸ”Ί Max Order", f"{max_order_size:,.0f}")
222
- with col_d4:
223
- st.metric("πŸ”» Min Order", f"{min_order_size:,.0f}")
224
-
225
- # Top products analysis
226
- st.markdown("### 🎯 Top Products by Demand")
227
-
228
- col_top1, col_top2 = st.columns([2, 1])
229
-
230
- with col_top1:
231
- top_products = demand_df.groupby('Material Number')["Order quantity (GMEIN)"].sum().sort_values(ascending=False).head(10)
232
-
233
- fig_top = px.bar(
234
- x=top_products.index,
235
- y=top_products.values,
236
- title='Top 10 Products by Total Demand',
237
- labels={'x': 'Product', 'y': 'Total Quantity'}
238
- )
239
- fig_top.update_layout(xaxis_tickangle=-45)
240
- st.plotly_chart(fig_top, use_container_width=True)
241
-
242
- with col_top2:
243
- st.markdown("**Top 10 Products:**")
244
- for i, (product, quantity) in enumerate(top_products.head(10).items(), 1):
245
- st.write(f"{i}. {product}: {quantity:,.0f}")
246
-
247
- # Daily demand pattern
248
- st.markdown("### πŸ“… Daily Demand Pattern")
249
-
250
- if 'Date' in demand_df.columns:
251
- daily_demand = demand_df.groupby('Date')["Order quantity (GMEIN)"].sum().reset_index()
252
- daily_demand['Date'] = pd.to_datetime(daily_demand['Date'])
253
-
254
- fig_daily = px.line(
255
- daily_demand,
256
- x='Date',
257
- y='Order quantity (GMEIN)',
258
- title='Daily Demand Trend',
259
- labels={'Order quantity (GMEIN)': 'Total Quantity'}
260
- )
261
- st.plotly_chart(fig_daily, use_container_width=True)
262
-
263
- # Demand statistics
264
- col_stats1, col_stats2, col_stats3 = st.columns(3)
265
- with col_stats1:
266
- st.metric("πŸ“Š Avg Daily Demand", f"{daily_demand['Order quantity (GMEIN)'].mean():,.0f}")
267
- with col_stats2:
268
- st.metric("πŸ“ˆ Peak Daily Demand", f"{daily_demand['Order quantity (GMEIN)'].max():,.0f}")
269
- with col_stats3:
270
- std_dev = daily_demand['Order quantity (GMEIN)'].std()
271
- st.metric("πŸ“Š Demand Variability", f"{std_dev:,.0f}")
272
-
273
- # Product distribution
274
- st.markdown("### 🎯 Product Demand Distribution")
275
-
276
- product_counts = demand_df['Material Number'].value_counts()
277
-
278
- col_dist1, col_dist2 = st.columns(2)
279
-
280
- with col_dist1:
281
- # Histogram of order quantities
282
- fig_hist = px.histogram(
283
- demand_df,
284
- x='Order quantity (GMEIN)',
285
- nbins=20,
286
- title='Order Quantity Distribution'
287
- )
288
- st.plotly_chart(fig_hist, use_container_width=True)
289
-
290
- with col_dist2:
291
- # Product frequency
292
- fig_freq = px.histogram(
293
- x=product_counts.values,
294
- nbins=15,
295
- title='Product Order Frequency Distribution'
296
- )
297
- fig_freq.update_layout(xaxis_title="Number of Orders", yaxis_title="Number of Products")
298
- st.plotly_chart(fig_freq, use_container_width=True)
299
-
300
- except Exception as e:
301
- st.error(f"Error in demand analysis: {e}")
302
-
303
- # Tab 3: Workforce Analysis
304
- with tab3:
305
- st.markdown('<h2 class="section-header">πŸ‘₯ Workforce Analysis</h2>', unsafe_allow_html=True)
306
-
307
- try:
308
- employee_df = extract.read_employee_data()
309
-
310
- # Employee metrics
311
- col_emp1, col_emp2, col_emp3, col_emp4 = st.columns(4)
312
-
313
- total_employees = len(employee_df)
314
- emp_types = employee_df['employment_type'].nunique()
315
-
316
- with col_emp1:
317
- st.metric("πŸ‘₯ Total Employees", total_employees)
318
- with col_emp2:
319
- st.metric("πŸ“‹ Employee Types", emp_types)
320
- with col_emp3:
321
- unicef_count = len(employee_df[employee_df['employment_type'] == 'UNICEF Fixed term'])
322
- st.metric("🏒 UNICEF Fixed", unicef_count)
323
- with col_emp4:
324
- humanizer_count = len(employee_df[employee_df['employment_type'] == 'Humanizer'])
325
- st.metric("πŸ‘· Humanizer", humanizer_count)
326
-
327
- # Employee type distribution
328
- st.markdown("### πŸ‘₯ Employee Type Distribution")
329
-
330
- col_emp_dist1, col_emp_dist2 = st.columns(2)
331
-
332
- with col_emp_dist1:
333
- emp_type_counts = employee_df['employment_type'].value_counts()
334
-
335
- fig_emp_pie = px.pie(
336
- values=emp_type_counts.values,
337
- names=emp_type_counts.index,
338
- title='Employee Distribution by Type'
339
- )
340
- st.plotly_chart(fig_emp_pie, use_container_width=True)
341
-
342
- with col_emp_dist2:
343
- fig_emp_bar = px.bar(
344
- x=emp_type_counts.index,
345
- y=emp_type_counts.values,
346
- title='Employee Count by Type'
347
- )
348
- st.plotly_chart(fig_emp_bar, use_container_width=True)
349
-
350
- # Cost analysis
351
- st.markdown("### πŸ’° Employee Cost Structure")
352
-
353
- cost_data = optimization_config.COST_LIST_PER_EMP_SHIFT
354
-
355
- # Create cost comparison table
356
- cost_comparison = []
357
- for emp_type, shifts in cost_data.items():
358
- for shift, cost in shifts.items():
359
- shift_name = {1: 'Regular', 2: 'Overtime', 3: 'Evening'}.get(shift, f'Shift {shift}')
360
- cost_comparison.append({
361
- 'Employee Type': emp_type,
362
- 'Shift': shift_name,
363
- 'Hourly Rate ($)': cost
364
- })
365
-
366
- cost_df = pd.DataFrame(cost_comparison)
367
-
368
- col_cost1, col_cost2 = st.columns(2)
369
-
370
- with col_cost1:
371
- st.dataframe(cost_df, use_container_width=True)
372
-
373
- with col_cost2:
374
- fig_cost = px.bar(
375
- cost_df,
376
- x='Employee Type',
377
- y='Hourly Rate ($)',
378
- color='Shift',
379
- title='Hourly Rates by Employee Type and Shift',
380
- barmode='group'
381
- )
382
- st.plotly_chart(fig_cost, use_container_width=True)
383
-
384
- # Productivity analysis
385
- st.markdown("### πŸ“ˆ Productivity Analysis")
386
-
387
- try:
388
- productivity_data = optimization_config.PRODUCTIVITY_LIST_PER_EMP_PRODUCT
389
-
390
- # Calculate average productivity by employee type
391
- prod_summary = []
392
- for emp_type, shifts in productivity_data.items():
393
- for shift, products in shifts.items():
394
- if products: # Check if products dict is not empty
395
- avg_productivity = np.mean(list(products.values()))
396
- shift_name = {1: 'Regular', 2: 'Overtime', 3: 'Evening'}.get(shift, f'Shift {shift}')
397
- prod_summary.append({
398
- 'Employee Type': emp_type,
399
- 'Shift': shift_name,
400
- 'Avg Productivity (units/hr)': avg_productivity
401
- })
402
-
403
- if prod_summary:
404
- prod_df = pd.DataFrame(prod_summary)
405
-
406
- fig_prod = px.bar(
407
- prod_df,
408
- x='Employee Type',
409
- y='Avg Productivity (units/hr)',
410
- color='Shift',
411
- title='Average Productivity by Employee Type and Shift',
412
- barmode='group'
413
- )
414
- st.plotly_chart(fig_prod, use_container_width=True)
415
-
416
- st.dataframe(prod_df, use_container_width=True)
417
- else:
418
- st.info("No productivity data available")
419
-
420
- except Exception as e:
421
- st.warning(f"Could not load productivity data: {e}")
422
-
423
- except Exception as e:
424
- st.error(f"Error in workforce analysis: {e}")
425
-
426
- # Tab 4: Production Capacity
427
- with tab4:
428
- st.markdown('<h2 class="section-header">🏭 Production Capacity Analysis</h2>', unsafe_allow_html=True)
429
-
430
- try:
431
- line_df = extract.read_packaging_line_data()
432
-
433
- # Production line metrics
434
- col_line1, col_line2, col_line3, col_line4 = st.columns(4)
435
-
436
- total_lines = line_df['line_count'].sum()
437
- line_types = len(line_df)
438
- max_capacity_line = line_df.loc[line_df['line_count'].idxmax()]
439
-
440
- with col_line1:
441
- st.metric("🏭 Total Lines", total_lines)
442
- with col_line2:
443
- st.metric("πŸ“‹ Line Types", line_types)
444
- with col_line3:
445
- st.metric("πŸ”Ί Max Capacity Type", f"Line {max_capacity_line['id']}")
446
- with col_line4:
447
- st.metric("πŸ“Š Max Line Count", max_capacity_line['line_count'])
448
-
449
- # Line capacity distribution
450
- st.markdown("### 🏭 Production Line Distribution")
451
-
452
- col_cap1, col_cap2 = st.columns(2)
453
-
454
- with col_cap1:
455
- fig_line_pie = px.pie(
456
- values=line_df['line_count'],
457
- names=[f"Line {row['id']}" for _, row in line_df.iterrows()],
458
- title='Production Line Distribution'
459
- )
460
- st.plotly_chart(fig_line_pie, use_container_width=True)
461
-
462
- with col_cap2:
463
- fig_line_bar = px.bar(
464
- x=[f"Line {row['id']}" for _, row in line_df.iterrows()],
465
- y=line_df['line_count'],
466
- title='Line Count by Type'
467
- )
468
- st.plotly_chart(fig_line_bar, use_container_width=True)
469
-
470
- # Capacity analysis
471
- st.markdown("### ⚑ Theoretical Capacity Analysis")
472
-
473
- cap_per_line = optimization_config.PER_PRODUCT_SPEED
474
- shift_hours = optimization_config.MAX_HOUR_PER_SHIFT_PER_PERSON
475
-
476
- # Calculate theoretical daily capacity
477
- capacity_analysis = []
478
- for _, row in line_df.iterrows():
479
- line_id = row['id']
480
- line_count = row['line_count']
481
-
482
- if line_id in cap_per_line:
483
- hourly_cap = cap_per_line[line_id]
484
-
485
- for shift, hours in shift_hours.items():
486
- shift_name = {1: 'Regular', 2: 'Overtime', 3: 'Evening'}.get(shift, f'Shift {shift}')
487
- daily_capacity = hourly_cap * hours * line_count
488
-
489
- capacity_analysis.append({
490
- 'Line Type': f"Line {line_id}",
491
- 'Shift': shift_name,
492
- 'Hourly Capacity': hourly_cap,
493
- 'Shift Hours': hours,
494
- 'Line Count': line_count,
495
- 'Shift Capacity': daily_capacity
496
- })
497
-
498
- if capacity_analysis:
499
- cap_df = pd.DataFrame(capacity_analysis)
500
-
501
- # Display capacity table
502
- st.dataframe(cap_df, use_container_width=True)
503
-
504
- # Capacity visualization
505
- fig_cap = px.bar(
506
- cap_df,
507
- x='Line Type',
508
- y='Shift Capacity',
509
- color='Shift',
510
- title='Theoretical Capacity by Line Type and Shift',
511
- barmode='group'
512
- )
513
- st.plotly_chart(fig_cap, use_container_width=True)
514
-
515
- # Total capacity summary
516
- total_capacity = cap_df.groupby('Line Type')['Shift Capacity'].sum()
517
-
518
- col_total1, col_total2 = st.columns(2)
519
-
520
- with col_total1:
521
- st.markdown("**Total Daily Capacity by Line:**")
522
- for line_type, capacity in total_capacity.items():
523
- st.write(f"β€’ {line_type}: {capacity:,.0f} units/day")
524
-
525
- with col_total2:
526
- total_all_lines = total_capacity.sum()
527
- st.metric("🏭 Total System Capacity", f"{total_all_lines:,.0f} units/day")
528
-
529
- except Exception as e:
530
- st.error(f"Error in production capacity analysis: {e}")
531
-
532
- # Tab 5: Cost Analysis
533
- with tab5:
534
- st.markdown('<h2 class="section-header">πŸ’° Cost Analysis</h2>', unsafe_allow_html=True)
535
-
536
- try:
537
- # Load cost data
538
- cost_data = optimization_config.COST_LIST_PER_EMP_SHIFT
539
- employee_df = extract.read_employee_data()
540
-
541
- # Cost structure overview
542
- st.markdown("### πŸ’΅ Cost Structure Overview")
543
-
544
- # Calculate cost ranges
545
- all_costs = []
546
- for emp_type, shifts in cost_data.items():
547
- for shift, cost in shifts.items():
548
- all_costs.append(cost)
549
-
550
- col_cost_over1, col_cost_over2, col_cost_over3, col_cost_over4 = st.columns(4)
551
-
552
- with col_cost_over1:
553
- st.metric("πŸ’° Min Hourly Rate", f"${min(all_costs)}")
554
- with col_cost_over2:
555
- st.metric("πŸ’° Max Hourly Rate", f"${max(all_costs)}")
556
- with col_cost_over3:
557
- st.metric("πŸ’° Avg Hourly Rate", f"${np.mean(all_costs):.2f}")
558
- with col_cost_over4:
559
- cost_range = max(all_costs) - min(all_costs)
560
- st.metric("πŸ“Š Cost Range", f"${cost_range}")
561
-
562
- # Detailed cost breakdown
563
- st.markdown("### πŸ“Š Detailed Cost Breakdown")
564
-
565
- cost_breakdown = []
566
- employee_counts = employee_df['employment_type'].value_counts()
567
-
568
- for emp_type, shifts in cost_data.items():
569
- emp_count = employee_counts.get(emp_type, 0)
570
-
571
- for shift, hourly_rate in shifts.items():
572
- shift_name = {1: 'Regular', 2: 'Overtime', 3: 'Evening'}.get(shift, f'Shift {shift}')
573
- shift_hours = optimization_config.MAX_HOUR_PER_SHIFT_PER_PERSON.get(shift, 0)
574
-
575
- daily_cost_per_emp = hourly_rate * shift_hours
576
- total_daily_cost = daily_cost_per_emp * emp_count
577
-
578
- cost_breakdown.append({
579
- 'Employee Type': emp_type,
580
- 'Shift': shift_name,
581
- 'Available Staff': emp_count,
582
- 'Hourly Rate ($)': hourly_rate,
583
- 'Shift Hours': shift_hours,
584
- 'Cost per Employee ($)': daily_cost_per_emp,
585
- 'Total Potential Cost ($)': total_daily_cost
586
- })
587
-
588
- cost_breakdown_df = pd.DataFrame(cost_breakdown)
589
- st.dataframe(cost_breakdown_df, use_container_width=True)
590
-
591
- # Cost visualization
592
- col_cost_viz1, col_cost_viz2 = st.columns(2)
593
-
594
- with col_cost_viz1:
595
- fig_cost_comp = px.bar(
596
- cost_breakdown_df,
597
- x='Employee Type',
598
- y='Total Potential Cost ($)',
599
- color='Shift',
600
- title='Total Potential Daily Cost by Type and Shift',
601
- barmode='group'
602
- )
603
- st.plotly_chart(fig_cost_comp, use_container_width=True)
604
-
605
- with col_cost_viz2:
606
- # Cost efficiency (cost per hour)
607
- fig_efficiency = px.scatter(
608
- cost_breakdown_df,
609
- x='Shift Hours',
610
- y='Hourly Rate ($)',
611
- color='Employee Type',
612
- size='Available Staff',
613
- title='Cost Efficiency Analysis',
614
- hover_data=['Shift']
615
- )
616
- st.plotly_chart(fig_efficiency, use_container_width=True)
617
-
618
- # Budget planning
619
- st.markdown("### πŸ“‹ Budget Planning Scenarios")
620
-
621
- col_budget1, col_budget2 = st.columns(2)
622
-
623
- with col_budget1:
624
- st.markdown("**Minimum Daily Cost Scenario:**")
625
- min_costs = cost_breakdown_df.groupby('Employee Type')['Cost per Employee ($)'].min()
626
- total_min_daily = (min_costs * employee_counts).sum()
627
- st.write(f"Total minimum daily cost: ${total_min_daily:,.2f}")
628
-
629
- for emp_type, cost in min_costs.items():
630
- count = employee_counts.get(emp_type, 0)
631
- st.write(f"β€’ {emp_type}: ${cost:.2f} Γ— {count} = ${cost * count:,.2f}")
632
-
633
- with col_budget2:
634
- st.markdown("**Maximum Daily Cost Scenario:**")
635
- max_costs = cost_breakdown_df.groupby('Employee Type')['Cost per Employee ($)'].max()
636
- total_max_daily = (max_costs * employee_counts).sum()
637
- st.write(f"Total maximum daily cost: ${total_max_daily:,.2f}")
638
-
639
- for emp_type, cost in max_costs.items():
640
- count = employee_counts.get(emp_type, 0)
641
- st.write(f"β€’ {emp_type}: ${cost:.2f} Γ— {count} = ${cost * count:,.2f}")
642
-
643
- # Weekly and monthly projections
644
- st.markdown("### πŸ“… Cost Projections")
645
-
646
- col_proj1, col_proj2, col_proj3 = st.columns(3)
647
-
648
- with col_proj1:
649
- weekly_min = total_min_daily * 7
650
- weekly_max = total_max_daily * 7
651
- st.metric("πŸ“… Weekly Cost Range", f"${weekly_min:,.0f} - ${weekly_max:,.0f}")
652
-
653
- with col_proj2:
654
- monthly_min = total_min_daily * 30
655
- monthly_max = total_max_daily * 30
656
- st.metric("πŸ“… Monthly Cost Range", f"${monthly_min:,.0f} - ${monthly_max:,.0f}")
657
-
658
- with col_proj3:
659
- avg_daily = (total_min_daily + total_max_daily) / 2
660
- st.metric("πŸ“Š Average Daily Cost", f"${avg_daily:,.2f}")
661
-
662
- except Exception as e:
663
- st.error(f"Error in cost analysis: {e}")
664
-
665
- # Tab 6: Quick Reports
666
- with tab6:
667
- st.markdown('<h2 class="section-header">πŸ“ˆ Quick Reports</h2>', unsafe_allow_html=True)
668
-
669
- st.info("πŸ’‘ **Need more detailed analysis?** Check out the **Enhanced Reports** page for comprehensive visualizations!")
670
-
671
- if st.button("πŸš€ Go to Enhanced Reports", type="primary", use_container_width=True):
672
- st.switch_page("pages/3_πŸ“ˆ_Enhanced_Reports.py")
673
-
674
- try:
675
- # Quick summary metrics
676
- demand_df = extract.read_released_orders_data(start_date=start_date, end_date=end_date)
677
- employee_df = extract.read_employee_data()
678
- cost_data = optimization_config.COST_LIST_PER_EMP_SHIFT
679
-
680
- st.markdown("### ⚑ Quick Summary")
681
-
682
- # Key metrics
683
- total_orders = len(demand_df)
684
- total_quantity = demand_df["Order quantity (GMEIN)"].sum()
685
- unique_products = demand_df["Material Number"].nunique()
686
- duration = (end_date - start_date).days + 1
687
-
688
- col_q1, col_q2, col_q3, col_q4 = st.columns(4)
689
-
690
- with col_q1:
691
- st.metric("πŸ“¦ Total Orders", f"{total_orders:,}")
692
- with col_q2:
693
- st.metric("πŸ“Š Total Quantity", f"{total_quantity:,.0f}")
694
- with col_q3:
695
- st.metric("🎯 Unique Products", unique_products)
696
- with col_q4:
697
- avg_daily_demand = total_quantity / duration
698
- st.metric("πŸ“… Avg Daily Demand", f"{avg_daily_demand:,.0f}")
699
-
700
- # Quick cost estimate
701
- st.markdown("### πŸ’° Cost Estimate")
702
-
703
- # Simple cost calculation
704
- total_employees = len(employee_df)
705
- emp_counts = employee_df['employment_type'].value_counts()
706
-
707
- # Estimate daily cost
708
- estimated_daily_cost = 0
709
- for emp_type, count in emp_counts.items():
710
- if emp_type in cost_data:
711
- # Use regular shift rate
712
- regular_rate = cost_data[emp_type].get(1, 0)
713
- estimated_daily_cost += count * regular_rate * 7 # Assume 7-hour shifts
714
-
715
- period_cost = estimated_daily_cost * duration
716
- cost_per_unit = period_cost / total_quantity if total_quantity > 0 else 0
717
-
718
- col_cost1, col_cost2, col_cost3 = st.columns(3)
719
-
720
- with col_cost1:
721
- st.metric("πŸ’° Est. Daily Cost", f"${estimated_daily_cost:,.2f}")
722
- with col_cost2:
723
- st.metric("πŸ’΅ Est. Period Cost", f"${period_cost:,.2f}")
724
- with col_cost3:
725
- st.metric("πŸ“¦ Est. Cost/Unit", f"${cost_per_unit:.3f}")
726
-
727
- # Quick production overview
728
- st.markdown("### 🏭 Production Overview")
729
-
730
- # Daily production distribution
731
- demand_df['Date'] = pd.to_datetime(demand_df['Basic finish date'])
732
- daily_production = demand_df.groupby('Date')['Order quantity (GMEIN)'].sum().reset_index()
733
-
734
- if len(daily_production) > 0:
735
- fig_quick = px.bar(
736
- daily_production,
737
- x='Date',
738
- y='Order quantity (GMEIN)',
739
- title='Daily Production Requirements',
740
- color='Order quantity (GMEIN)',
741
- color_continuous_scale='Blues'
742
- )
743
- st.plotly_chart(fig_quick, use_container_width=True)
744
-
745
- # Top products quick view
746
- top_products = demand_df.groupby('Material Number')['Order quantity (GMEIN)'].sum().sort_values(ascending=False).head(5)
747
-
748
- st.markdown("**Top 5 Products by Quantity:**")
749
- for i, (product, quantity) in enumerate(top_products.items(), 1):
750
- st.write(f"{i}. {product}: {quantity:,.0f} units")
751
-
752
- except Exception as e:
753
- st.error(f"Error generating quick reports: {e}")
754
-
755
- # Tab 7: Optimization Settings
756
- with tab7:
757
- st.markdown('<h2 class="section-header">βš™οΈ Optimization Settings</h2>', unsafe_allow_html=True)
758
-
759
- st.info("πŸ’‘ **Configure optimization parameters here!** These settings will be used by the optimization engine when you run optimizations.")
760
-
761
- try:
762
- # Load available options from data
763
- employee_df = extract.read_employee_data()
764
- line_df = extract.read_packaging_line_data()
765
- shift_df = extract.get_shift_info()
766
-
767
- # Employee Types Selection
768
- st.markdown("### πŸ‘₯ Employee Types")
769
- available_emp_types = employee_df["employment_type"].unique().tolist()
770
-
771
- # Initialize session state if not exists
772
- if 'selected_employee_types' not in st.session_state:
773
- st.session_state.selected_employee_types = available_emp_types
774
-
775
- selected_emp_types = st.multiselect(
776
- "Select employee types to include in optimization:",
777
- options=available_emp_types,
778
- default=st.session_state.selected_employee_types,
779
- key="emp_types_selector",
780
- help="Choose which employee types should be available for optimization"
781
- )
782
-
783
- # Update session state
784
- if selected_emp_types != st.session_state.selected_employee_types:
785
- st.session_state.selected_employee_types = selected_emp_types
786
- st.success(f"βœ… Employee types updated: {', '.join(selected_emp_types)}")
787
-
788
- # Display current employee type counts
789
- col_emp1, col_emp2 = st.columns(2)
790
- with col_emp1:
791
- st.markdown("**Current Employee Availability:**")
792
- emp_counts = employee_df['employment_type'].value_counts()
793
- for emp_type in selected_emp_types:
794
- count = emp_counts.get(emp_type, 0)
795
- st.write(f"β€’ {emp_type}: {count} employees")
796
-
797
- with col_emp2:
798
- # Show cost information for selected types
799
- st.markdown("**Hourly Rates (Regular Shift):**")
800
- cost_data = optimization_config.COST_LIST_PER_EMP_SHIFT
801
- for emp_type in selected_emp_types:
802
- if emp_type in cost_data:
803
- regular_rate = cost_data[emp_type].get(1, "N/A")
804
- st.write(f"β€’ {emp_type}: ${regular_rate}/hour")
805
-
806
- st.markdown("---")
807
-
808
- # Shifts Selection
809
- st.markdown("### πŸ• Shifts")
810
- available_shifts = shift_df["id"].unique().tolist()
811
-
812
- # Initialize session state if not exists
813
- if 'selected_shifts' not in st.session_state:
814
- st.session_state.selected_shifts = available_shifts
815
-
816
- shift_names = {1: 'Regular (Day)', 2: 'Overtime', 3: 'Evening'}
817
- shift_options = [f"{shift_id}: {shift_names.get(shift_id, f'Shift {shift_id}')}" for shift_id in available_shifts]
818
-
819
- selected_shift_options = st.multiselect(
820
- "Select shifts to include in optimization:",
821
- options=shift_options,
822
- default=[f"{shift_id}: {shift_names.get(shift_id, f'Shift {shift_id}')}" for shift_id in st.session_state.selected_shifts],
823
- key="shifts_selector",
824
- help="Choose which shifts should be available for optimization"
825
- )
826
-
827
- # Extract shift IDs from selected options
828
- selected_shifts = [int(option.split(':')[0]) for option in selected_shift_options]
829
-
830
- # Update session state
831
- if selected_shifts != st.session_state.selected_shifts:
832
- st.session_state.selected_shifts = selected_shifts
833
- st.success(f"βœ… Shifts updated: {', '.join([shift_names.get(s, f'Shift {s}') for s in selected_shifts])}")
834
-
835
- # Display shift information
836
- st.markdown("**Shift Details:**")
837
- shift_hours = optimization_config.MAX_HOUR_PER_SHIFT_PER_PERSON
838
- for shift_id in selected_shifts:
839
- shift_name = shift_names.get(shift_id, f'Shift {shift_id}')
840
- hours = shift_hours.get(shift_id, "N/A")
841
- st.write(f"β€’ {shift_name}: {hours} hours")
842
-
843
- st.markdown("---")
844
-
845
- # Production Lines Selection
846
- st.markdown("### 🏭 Production Lines")
847
- available_lines = line_df["id"].unique().tolist()
848
-
849
- # Initialize session state if not exists
850
- if 'selected_lines' not in st.session_state:
851
- st.session_state.selected_lines = available_lines
852
-
853
- selected_lines = st.multiselect(
854
- "Select production lines to include in optimization:",
855
- options=available_lines,
856
- default=st.session_state.selected_lines,
857
- key="lines_selector",
858
- help="Choose which production lines should be available for optimization"
859
- )
860
-
861
- # Update session state
862
- if selected_lines != st.session_state.selected_lines:
863
- st.session_state.selected_lines = selected_lines
864
- st.success(f"βœ… Production lines updated: {', '.join([f'Line {line}' for line in selected_lines])}")
865
-
866
- # Display line information
867
- col_line1, col_line2 = st.columns(2)
868
- with col_line1:
869
- st.markdown("**Line Capacities:**")
870
- for line_id in selected_lines:
871
- line_info = line_df[line_df['id'] == line_id]
872
- if not line_info.empty:
873
- line_count = line_info.iloc[0]['line_count']
874
- st.write(f"β€’ Line {line_id}: {line_count} units available")
875
-
876
- with col_line2:
877
- st.markdown("**Processing Speed:**")
878
- line_speeds = optimization_config.PER_PRODUCT_SPEED
879
- for line_id in selected_lines:
880
- speed = line_speeds.get(line_id, "N/A")
881
- st.write(f"β€’ Line {line_id}: {speed} units/hour")
882
-
883
- st.markdown("---")
884
-
885
- # Additional Settings
886
- st.markdown("### πŸ”§ Additional Settings")
887
-
888
- col_settings1, col_settings2 = st.columns(2)
889
-
890
- with col_settings1:
891
- # Constraint mode
892
- constraint_mode = st.selectbox(
893
- "Fixed Staff Constraint Mode:",
894
- options=["priority", "mandatory", "none"],
895
- index=0,
896
- key="constraint_mode_selector",
897
- help="priority=Use fixed staff first, mandatory=Force all fixed hours, none=Demand-driven"
898
- )
899
-
900
- if 'selected_constraint_mode' not in st.session_state:
901
- st.session_state.selected_constraint_mode = constraint_mode
902
-
903
- if constraint_mode != st.session_state.selected_constraint_mode:
904
- st.session_state.selected_constraint_mode = constraint_mode
905
- st.success(f"βœ… Constraint mode updated: {constraint_mode}")
906
-
907
- with col_settings2:
908
- # Evening shift mode
909
- evening_mode = st.selectbox(
910
- "Evening Shift Mode:",
911
- options=["normal", "activate_evening", "always_available"],
912
- index=0,
913
- key="evening_mode_selector",
914
- help="normal=Regular+Overtime only, activate_evening=Auto-activate when needed, always_available=Always include evening shift"
915
- )
916
-
917
- if 'selected_evening_mode' not in st.session_state:
918
- st.session_state.selected_evening_mode = evening_mode
919
-
920
- if evening_mode != st.session_state.selected_evening_mode:
921
- st.session_state.selected_evening_mode = evening_mode
922
- st.success(f"βœ… Evening shift mode updated: {evening_mode}")
923
-
924
- # Summary of current settings
925
- st.markdown("### πŸ“‹ Current Optimization Configuration")
926
- st.markdown('<div class="cost-highlight">', unsafe_allow_html=True)
927
-
928
- col_summary1, col_summary2 = st.columns(2)
929
-
930
- with col_summary1:
931
- st.markdown("**Selected Configuration:**")
932
- st.write(f"β€’ **Employee Types:** {len(st.session_state.get('selected_employee_types', []))} types")
933
- st.write(f"β€’ **Shifts:** {len(st.session_state.get('selected_shifts', []))} shifts")
934
- st.write(f"β€’ **Production Lines:** {len(st.session_state.get('selected_lines', []))} lines")
935
- st.write(f"β€’ **Constraint Mode:** {st.session_state.get('selected_constraint_mode', 'priority')}")
936
-
937
- with col_summary2:
938
- st.markdown("**Ready for Optimization:**")
939
- if (st.session_state.get('selected_employee_types') and
940
- st.session_state.get('selected_shifts') and
941
- st.session_state.get('selected_lines')):
942
- st.success("βœ… All required settings configured!")
943
- if st.button("πŸš€ Go to Optimization Page", type="primary", use_container_width=True):
944
- st.switch_page("pages/2_🎯_Optimization.py")
945
- else:
946
- st.warning("⚠️ Please configure all settings above")
947
-
948
- st.markdown('</div>', unsafe_allow_html=True)
949
-
950
- # Reset button
951
- if st.button("πŸ”„ Reset to Defaults", type="secondary"):
952
- st.session_state.selected_employee_types = available_emp_types
953
- st.session_state.selected_shifts = available_shifts
954
- st.session_state.selected_lines = available_lines
955
- st.session_state.selected_constraint_mode = "priority"
956
- st.session_state.selected_evening_mode = "normal"
957
- st.success("βœ… Settings reset to defaults!")
958
- st.rerun()
959
-
960
- except Exception as e:
961
- st.error(f"Error loading optimization settings: {e}")
962
-
963
- # Footer
964
- st.markdown("---")
965
- st.markdown("""
966
- <div style='text-align: center; color: gray; padding: 1rem;'>
967
- <small>Dataset Metadata Analysis | Data updated in real-time from your CSV files</small>
968
- </div>
969
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/2_🎯_Optimization.py DELETED
@@ -1,662 +0,0 @@
1
- import streamlit as st
2
-
3
- # Page configuration
4
- st.set_page_config(
5
- page_title="Optimization Tool",
6
- page_icon="🎯",
7
- layout="wide"
8
- )
9
-
10
- # Import libraries
11
- import pandas as pd
12
- import plotly.express as px
13
- import plotly.graph_objects as go
14
- from plotly.subplots import make_subplots
15
- import sys
16
- import os
17
- from datetime import datetime, timedelta
18
- import numpy as np
19
-
20
- # Add src to path for imports
21
- sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src'))
22
-
23
- try:
24
- from src.models.optimizer_new_aug14 import solve_fixed_team_weekly
25
- from src.config import optimization_config
26
- import src.etl.extract as extract
27
- import src.etl.transform as transform
28
- except ImportError as e:
29
- st.error(f"Error importing modules: {e}")
30
- st.stop()
31
-
32
- # Custom CSS
33
- st.markdown("""
34
- <style>
35
- .main-header {
36
- font-size: 2.5rem;
37
- font-weight: bold;
38
- color: #1f77b4;
39
- margin-bottom: 1rem;
40
- }
41
- .section-header {
42
- font-size: 1.5rem;
43
- font-weight: bold;
44
- color: #2c3e50;
45
- margin: 1rem 0;
46
- }
47
- .optimization-panel {
48
- background-color: #f8f9fa;
49
- padding: 1.5rem;
50
- border-radius: 0.8rem;
51
- border-left: 5px solid #28a745;
52
- margin-bottom: 1.5rem;
53
- }
54
- .results-panel {
55
- background-color: #fff3cd;
56
- padding: 1.5rem;
57
- border-radius: 0.8rem;
58
- border-left: 5px solid #ffc107;
59
- margin-bottom: 1.5rem;
60
- }
61
- </style>
62
- """, unsafe_allow_html=True)
63
-
64
- # Initialize session state
65
- if 'optimization_results' not in st.session_state:
66
- st.session_state.optimization_results = None
67
- if 'optimizer' not in st.session_state:
68
- st.session_state.optimizer = None
69
- if 'date_range' not in st.session_state:
70
- st.session_state.date_range = None
71
-
72
- # Title
73
- st.markdown('<h1 class="main-header">🎯 Optimization Tool</h1>', unsafe_allow_html=True)
74
-
75
- # Sidebar for optimization parameters
76
- with st.sidebar:
77
- st.markdown("## βš™οΈ Optimization Parameters")
78
-
79
- # Date Selection Section
80
- st.markdown("### πŸ“… Date Range Selection")
81
- try:
82
- date_ranges = transform.get_date_ranges()
83
- if date_ranges:
84
- date_range_options = [f"{start.strftime('%Y-%m-%d')} to {end.strftime('%Y-%m-%d')}" for start, end in date_ranges]
85
- selected_range_str = st.selectbox(
86
- "Select date range:",
87
- options=date_range_options,
88
- help="Available date ranges from released orders"
89
- )
90
-
91
- selected_index = date_range_options.index(selected_range_str)
92
- start_date, end_date = date_ranges[selected_index]
93
- st.session_state.date_range = (start_date, end_date)
94
-
95
- duration = (end_date - start_date).days + 1
96
- st.info(f"Duration: {duration} days")
97
-
98
- else:
99
- st.warning("No date ranges found in data")
100
- start_date = datetime(2025, 3, 24).date()
101
- end_date = datetime(2025, 3, 28).date()
102
- st.session_state.date_range = (start_date, end_date)
103
-
104
- except Exception as e:
105
- st.error(f"Error loading dates: {e}")
106
- start_date = datetime(2025, 3, 24).date()
107
- end_date = datetime(2025, 3, 28).date()
108
- st.session_state.date_range = (start_date, end_date)
109
-
110
- st.markdown("---")
111
-
112
- # Employee Type Selection
113
- st.markdown("### πŸ‘₯ Employee Configuration")
114
- try:
115
- employee_df = extract.read_employee_data()
116
- available_emp_types = employee_df["employment_type"].unique().tolist()
117
- except:
118
- available_emp_types = ["UNICEF Fixed term", "Humanizer"]
119
-
120
- selected_emp_types = st.multiselect(
121
- "Employee Types:",
122
- available_emp_types,
123
- default=available_emp_types,
124
- help="Select employee types to include in optimization"
125
- )
126
-
127
- # Shift Selection
128
- st.markdown("### πŸ• Shift Configuration")
129
- try:
130
- shift_df = extract.get_shift_info()
131
- available_shifts = shift_df["id"].unique().tolist()
132
- except:
133
- available_shifts = [1, 2, 3]
134
-
135
- selected_shifts = st.multiselect(
136
- "Shifts:",
137
- available_shifts,
138
- default=available_shifts,
139
- help="1=Regular, 2=Overtime, 3=Evening"
140
- )
141
-
142
- # Line Selection
143
- st.markdown("### 🏭 Production Line Configuration")
144
- try:
145
- line_df = extract.read_packaging_line_data()
146
- available_lines = line_df["id"].unique().tolist()
147
- except:
148
- available_lines = [6, 7]
149
-
150
- selected_lines = st.multiselect(
151
- "Production Lines:",
152
- available_lines,
153
- default=available_lines,
154
- help="Select production lines to include"
155
- )
156
-
157
- st.markdown("---")
158
-
159
- # Advanced Parameters
160
- with st.expander("πŸ”§ Advanced Parameters", expanded=False):
161
- constraint_mode = st.selectbox(
162
- "Fixed Staff Constraint Mode:",
163
- ["priority", "mandatory", "none"],
164
- index=0,
165
- help="priority=Use fixed staff first, mandatory=Force all fixed hours, none=Demand-driven"
166
- )
167
-
168
- max_hours_per_person = st.number_input(
169
- "Max hours per person per day:",
170
- min_value=8,
171
- max_value=24,
172
- value=14,
173
- help="Legal daily limit"
174
- )
175
-
176
- # Employee availability override
177
- st.markdown("**Employee Availability Override:**")
178
- col1, col2 = st.columns(2)
179
- with col1:
180
- unicef_count = st.number_input("UNICEF Fixed term:", min_value=0, value=8)
181
- with col2:
182
- humanizer_count = st.number_input("Humanizer:", min_value=0, value=6)
183
-
184
- st.markdown("---")
185
-
186
- # Run Optimization Button
187
- run_optimization = st.button("πŸš€ Run Optimization", type="primary", use_container_width=True)
188
-
189
- if st.button("πŸ”„ Clear Results", use_container_width=True):
190
- st.session_state.optimization_results = None
191
- st.rerun()
192
-
193
- # Main content area
194
- if st.session_state.date_range:
195
- start_date, end_date = st.session_state.date_range
196
- st.markdown(f"**Optimization Period:** {start_date} to {end_date}")
197
- else:
198
- st.warning("Please select a date range from the sidebar")
199
- st.stop()
200
-
201
- # Optimization execution
202
- if run_optimization:
203
- with st.spinner("πŸ”„ Running optimization... This may take a few moments."):
204
- try:
205
- # Run optimization using the new optimizer
206
- st.info("πŸ”§ Using optimizer_new_aug14 with fixed team weekly scheduling")
207
-
208
- results = solve_fixed_team_weekly()
209
-
210
- if results is None:
211
- st.error("❌ Optimization returned no results")
212
- elif isinstance(results, dict) and results.get('status') == 'failed':
213
- st.error(f"❌ Optimization failed: {results.get('message', 'Unknown error')}")
214
- else:
215
- # Convert results to expected format for display
216
- st.session_state.optimization_results = {
217
- 'status': 'success',
218
- 'total_cost': results.get('objective', 0),
219
- 'raw_results': results,
220
- 'solver_type': 'optimizer_new_aug14'
221
- }
222
- st.success("βœ… Optimization completed successfully!")
223
-
224
- except Exception as e:
225
- st.error(f"❌ Optimization failed: {e}")
226
- st.exception(e)
227
-
228
- # Display results if available
229
- if st.session_state.optimization_results:
230
- results = st.session_state.optimization_results
231
-
232
- # Results header
233
- st.markdown('<div class="results-panel">', unsafe_allow_html=True)
234
- st.markdown("## πŸ“Š Optimization Results")
235
-
236
- # Key metrics summary
237
- total_cost = results.get('total_cost', 0)
238
- params = results.get('parameters', {})
239
-
240
- col_summary1, col_summary2, col_summary3, col_summary4 = st.columns(4)
241
-
242
- with col_summary1:
243
- st.metric("πŸ’° Total Cost", f"${total_cost:,.2f}")
244
- with col_summary2:
245
- st.metric("πŸ“¦ Products", len(params.get('product_list', [])))
246
- with col_summary3:
247
- st.metric("πŸ‘₯ Employee Types", len(params.get('employee_types', [])))
248
- with col_summary4:
249
- if st.session_state.date_range:
250
- start_date, end_date = st.session_state.date_range
251
- duration = (end_date - start_date).days + 1
252
- cost_per_day = total_cost / duration if duration > 0 else 0
253
- st.metric("πŸ’΅ Cost/Day", f"${cost_per_day:,.2f}")
254
-
255
- st.markdown('</div>', unsafe_allow_html=True)
256
-
257
- # Create tabs for detailed results
258
- tab1, tab2, tab3, tab4 = st.tabs(["πŸ“Š Summary", "πŸ“ˆ Production", "πŸ‘· Labor", "πŸ’° Costs"])
259
-
260
- with tab1:
261
- st.markdown("### πŸ“Š Optimization Summary")
262
-
263
- # Check if we have raw results from new optimizer
264
- raw_results = results.get('raw_results', {})
265
- solver_type = results.get('solver_type', 'Unknown')
266
-
267
- st.info(f"πŸ”§ Solver Used: {solver_type}")
268
-
269
- # Additional summary metrics
270
- col_s1, col_s2, col_s3 = st.columns(3)
271
-
272
- with col_s1:
273
- if 'weekly_production' in raw_results:
274
- total_produced = sum(raw_results['weekly_production'].values())
275
- st.metric("🏭 Total Produced", f"{total_produced:,.0f}")
276
- else:
277
- st.metric("🏭 Total Produced", "N/A")
278
- with col_s2:
279
- if 'weekly_production' in raw_results:
280
- total_produced = sum(raw_results['weekly_production'].values())
281
- cost_per_unit = total_cost / total_produced if total_produced > 0 else 0
282
- st.metric("πŸ’΅ Cost per Unit", f"${cost_per_unit:.3f}")
283
- else:
284
- st.metric("πŸ’΅ Cost per Unit", "N/A")
285
- with col_s3:
286
- products_count = len(raw_results.get('weekly_production', {}))
287
- st.metric("οΏ½οΏ½οΏ½οΏ½ Products Optimized", products_count)
288
-
289
- # Show optimization parameters used
290
- st.markdown("#### πŸ“‹ Configuration Used")
291
- col_config1, col_config2 = st.columns(2)
292
-
293
- with col_config1:
294
- st.markdown("**Selected Parameters:**")
295
- st.markdown(f"β€’ **Employee Types:** {', '.join(selected_emp_types)}")
296
- st.markdown(f"β€’ **Shifts:** {', '.join(map(str, selected_shifts))}")
297
- st.markdown(f"β€’ **Production Lines:** {', '.join(map(str, selected_lines))}")
298
-
299
- with col_config2:
300
- st.markdown("**Optimization Details:**")
301
- st.markdown(f"β€’ **Constraint Mode:** {params.get('constraint_mode', 'N/A')}")
302
- st.markdown(f"β€’ **Days Optimized:** {len(params.get('days', []))}")
303
- st.markdown(f"β€’ **Products Included:** {len(params.get('product_list', []))}")
304
-
305
- # Solution quality indicators
306
- st.markdown("#### βœ… Solution Quality")
307
- col_qual1, col_qual2, col_qual3 = st.columns(3)
308
-
309
- with col_qual1:
310
- st.success("**Status:** Optimal solution found")
311
- with col_qual2:
312
- st.info(f"**Solver:** OR-Tools CBC")
313
- with col_qual3:
314
- st.info(f"**Solution Time:** < 1 minute")
315
-
316
- with tab2:
317
- st.markdown("### πŸ“ˆ Production Results")
318
-
319
- # Use new optimizer results format
320
- weekly_production = raw_results.get('weekly_production', {})
321
- run_schedule = raw_results.get('run_schedule', [])
322
-
323
- if weekly_production:
324
- # Create production summary table
325
- prod_data = []
326
-
327
- # Get demand data for comparison
328
- demand_data = optimization_config.DEMAND_DICTIONARY
329
-
330
- for product, produced in weekly_production.items():
331
- demand = demand_data.get(product, 0)
332
- fulfillment_rate = (produced / demand * 100) if demand > 0 else 0
333
-
334
- prod_data.append({
335
- 'Product': product,
336
- 'Demand': demand,
337
- 'Produced': produced,
338
- 'Fulfillment %': f"{fulfillment_rate:.1f}%",
339
- 'Status': 'βœ… Met' if fulfillment_rate >= 100 else '⚠️ Partial'
340
- })
341
-
342
- if prod_data:
343
- prod_df = pd.DataFrame(prod_data)
344
-
345
- # Production metrics
346
- total_demand = sum(data['demand'] for data in production_results.values())
347
- total_produced = sum(data['produced'] for data in production_results.values())
348
- overall_fulfillment = (total_produced / total_demand * 100) if total_demand > 0 else 0
349
-
350
- col_prod1, col_prod2, col_prod3, col_prod4 = st.columns(4)
351
-
352
- with col_prod1:
353
- st.metric("πŸ“¦ Total Demand", f"{total_demand:,.0f}")
354
- with col_prod2:
355
- st.metric("🏭 Total Produced", f"{total_produced:,.0f}")
356
- with col_prod3:
357
- st.metric("βœ… Overall Fulfillment", f"{overall_fulfillment:.1f}%")
358
- with col_prod4:
359
- products_met = sum(1 for data in production_results.values() if data['fulfillment_rate'] >= 100)
360
- st.metric("🎯 Products Fully Met", f"{products_met}/{len(production_results)}")
361
-
362
- # Production table
363
- st.markdown("#### πŸ“‹ Production Details")
364
- st.dataframe(prod_df, use_container_width=True)
365
-
366
- # Production charts
367
- col_chart1, col_chart2 = st.columns(2)
368
-
369
- with col_chart1:
370
- # Production vs Demand comparison
371
- fig_prod = px.bar(
372
- prod_df,
373
- x='Product',
374
- y=['Demand', 'Produced'],
375
- title='Production vs Demand by Product',
376
- barmode='group'
377
- )
378
- fig_prod.update_layout(xaxis_tickangle=-45)
379
- st.plotly_chart(fig_prod, use_container_width=True)
380
-
381
- with col_chart2:
382
- # Fulfillment rate chart
383
- fulfillment_data = [(row['Product'], float(row['Fulfillment %'].rstrip('%'))) for row in prod_data]
384
- fulfill_df = pd.DataFrame(fulfillment_data, columns=['Product', 'Fulfillment_Rate'])
385
-
386
- fig_fulfill = px.bar(
387
- fulfill_df,
388
- x='Product',
389
- y='Fulfillment_Rate',
390
- title='Fulfillment Rate by Product (%)',
391
- color='Fulfillment_Rate',
392
- color_continuous_scale='RdYlGn'
393
- )
394
- fig_fulfill.update_layout(yaxis_title="Fulfillment Rate (%)", xaxis_tickangle=-45)
395
- fig_fulfill.add_hline(y=100, line_dash="dash", line_color="red", annotation_text="Target: 100%")
396
- st.plotly_chart(fig_fulfill, use_container_width=True)
397
- else:
398
- st.info("No production data available")
399
-
400
- with tab3:
401
- st.markdown("### πŸ‘· Labor Allocation")
402
-
403
- employee_hours = results.get('employee_hours', {})
404
- headcount_req = results.get('headcount_requirements', {})
405
-
406
- if employee_hours:
407
- # Labor summary metrics
408
- total_labor_hours = 0
409
- labor_data = []
410
-
411
- for emp_type, shifts in employee_hours.items():
412
- emp_total_hours = 0
413
- for shift, daily_hours in shifts.items():
414
- total_hours = sum(daily_hours)
415
- emp_total_hours += total_hours
416
- if total_hours > 0:
417
- labor_data.append({
418
- 'Employee Type': emp_type,
419
- 'Shift': f"Shift {shift}",
420
- 'Total Hours': total_hours,
421
- 'Avg Daily Hours': total_hours / len(daily_hours) if daily_hours else 0
422
- })
423
- total_labor_hours += emp_total_hours
424
-
425
- # Labor metrics
426
- col_labor1, col_labor2, col_labor3, col_labor4 = st.columns(4)
427
-
428
- with col_labor1:
429
- st.metric("⏰ Total Labor Hours", f"{total_labor_hours:,.0f}")
430
- with col_labor2:
431
- if st.session_state.date_range:
432
- duration = (end_date - start_date).days + 1
433
- avg_daily_hours = total_labor_hours / duration
434
- st.metric("πŸ“… Avg Daily Hours", f"{avg_daily_hours:,.0f}")
435
- with col_labor3:
436
- try:
437
- max_daily_workers = 0
438
- for emp_type, shifts in headcount_req.items():
439
- for shift, daily_counts in shifts.items():
440
- if daily_counts: # daily_counts is a list
441
- max_daily_workers += max(daily_counts)
442
- st.metric("πŸ‘₯ Peak Workers Needed", max_daily_workers)
443
- except Exception as e:
444
- st.metric("πŸ‘₯ Peak Workers Needed", "N/A")
445
- with col_labor4:
446
- labor_cost_per_hour = total_cost / total_labor_hours if total_labor_hours > 0 else 0
447
- st.metric("πŸ’° Avg Cost/Hour", f"${labor_cost_per_hour:.2f}")
448
-
449
- if labor_data:
450
- st.markdown("#### πŸ“‹ Labor Hours Details")
451
- labor_df = pd.DataFrame(labor_data)
452
- st.dataframe(labor_df, use_container_width=True)
453
-
454
- # Labor visualization
455
- col_labor_chart1, col_labor_chart2 = st.columns(2)
456
-
457
- with col_labor_chart1:
458
- # Labor hours by type and shift
459
- fig_labor = px.bar(
460
- labor_df,
461
- x='Employee Type',
462
- y='Total Hours',
463
- color='Shift',
464
- title='Total Labor Hours by Employee Type and Shift',
465
- barmode='group'
466
- )
467
- st.plotly_chart(fig_labor, use_container_width=True)
468
-
469
- with col_labor_chart2:
470
- # Labor distribution pie chart
471
- emp_totals = labor_df.groupby('Employee Type')['Total Hours'].sum()
472
- fig_labor_pie = px.pie(
473
- values=emp_totals.values,
474
- names=emp_totals.index,
475
- title='Labor Hours Distribution by Employee Type'
476
- )
477
- st.plotly_chart(fig_labor_pie, use_container_width=True)
478
-
479
- # Headcount requirements
480
- if headcount_req:
481
- st.markdown("#### πŸ‘₯ Required Headcount")
482
- headcount_data = []
483
- for emp_type, shifts in headcount_req.items():
484
- for shift, daily_count in shifts.items():
485
- max_count = max(daily_count) if daily_count else 0
486
- avg_count = sum(daily_count) / len(daily_count) if daily_count else 0
487
- total_count = sum(daily_count) if daily_count else 0
488
- if max_count > 0:
489
- headcount_data.append({
490
- 'Employee Type': emp_type,
491
- 'Shift': f"Shift {shift}",
492
- 'Max Daily': max_count,
493
- 'Avg Daily': f"{avg_count:.1f}",
494
- 'Total Period': total_count
495
- })
496
-
497
- if headcount_data:
498
- headcount_df = pd.DataFrame(headcount_data)
499
- st.dataframe(headcount_df, use_container_width=True)
500
-
501
- # Headcount visualization
502
- fig_headcount = px.bar(
503
- headcount_df,
504
- x='Employee Type',
505
- y='Max Daily',
506
- color='Shift',
507
- title='Maximum Daily Headcount Requirements',
508
- barmode='group'
509
- )
510
- st.plotly_chart(fig_headcount, use_container_width=True)
511
-
512
- with tab4:
513
- st.markdown("### πŸ’° Cost Analysis")
514
-
515
- # Cost summary
516
- total_cost = results.get('total_cost', 0)
517
-
518
- col_cost_summary1, col_cost_summary2, col_cost_summary3, col_cost_summary4 = st.columns(4)
519
-
520
- with col_cost_summary1:
521
- st.metric("πŸ’° Total Cost", f"${total_cost:,.2f}")
522
- with col_cost_summary2:
523
- if st.session_state.date_range:
524
- duration = (end_date - start_date).days + 1
525
- cost_per_day = total_cost / duration
526
- st.metric("πŸ“… Cost per Day", f"${cost_per_day:,.2f}")
527
- with col_cost_summary3:
528
- total_demand = params.get('total_demand', 1)
529
- cost_per_unit = total_cost / total_demand if total_demand > 0 else 0
530
- st.metric("πŸ“¦ Cost per Unit", f"${cost_per_unit:.3f}")
531
- with col_cost_summary4:
532
- total_hours = sum(sum(sum(daily_hours) for daily_hours in shifts.values())
533
- for shifts in results.get('employee_hours', {}).values())
534
- cost_per_hour = total_cost / total_hours if total_hours > 0 else 0
535
- st.metric("⏰ Cost per Hour", f"${cost_per_hour:.2f}")
536
-
537
- # Cost breakdown by employee type
538
- employee_hours = results.get('employee_hours', {})
539
- if employee_hours:
540
- cost_data = []
541
- wage_types = optimization_config.COST_LIST_PER_EMP_SHIFT
542
-
543
- for emp_type, shifts in employee_hours.items():
544
- emp_total_cost = 0
545
- for shift, daily_hours in shifts.items():
546
- total_hours = sum(daily_hours)
547
- if total_hours > 0 and emp_type in wage_types and shift in wage_types[emp_type]:
548
- shift_cost = total_hours * wage_types[emp_type][shift]
549
- emp_total_cost += shift_cost
550
- cost_data.append({
551
- 'Employee Type': emp_type,
552
- 'Shift': f"Shift {shift}",
553
- 'Hours': total_hours,
554
- 'Rate ($/hr)': wage_types[emp_type][shift],
555
- 'Total Cost ($)': shift_cost,
556
- 'Percentage': (shift_cost / total_cost * 100) if total_cost > 0 else 0
557
- })
558
-
559
- if cost_data:
560
- st.markdown("#### πŸ“Š Detailed Cost Breakdown")
561
- cost_df = pd.DataFrame(cost_data)
562
- st.dataframe(cost_df, use_container_width=True)
563
-
564
- # Cost visualization
565
- col_cost_chart1, col_cost_chart2 = st.columns(2)
566
-
567
- with col_cost_chart1:
568
- # Cost by employee type and shift
569
- fig_cost = px.bar(
570
- cost_df,
571
- x='Employee Type',
572
- y='Total Cost ($)',
573
- color='Shift',
574
- title='Cost Breakdown by Employee Type and Shift',
575
- barmode='stack'
576
- )
577
- st.plotly_chart(fig_cost, use_container_width=True)
578
-
579
- with col_cost_chart2:
580
- # Cost distribution pie chart
581
- emp_costs = cost_df.groupby('Employee Type')['Total Cost ($)'].sum()
582
- fig_cost_pie = px.pie(
583
- values=emp_costs.values,
584
- names=emp_costs.index,
585
- title='Cost Distribution by Employee Type'
586
- )
587
- st.plotly_chart(fig_cost_pie, use_container_width=True)
588
-
589
- # Priority mode results
590
- priority_results = results.get('priority_results')
591
- if priority_results and priority_results.get('summary'):
592
- st.markdown("#### 🎯 Priority Mode Analysis")
593
- summary = priority_results['summary']
594
-
595
- col_priority1, col_priority2 = st.columns(2)
596
-
597
- with col_priority1:
598
- if summary['unicef_sufficient']:
599
- st.success("βœ… **UNICEF Fixed term staff sufficient**")
600
- st.info("β†’ Humanizer staff not needed for this demand level")
601
- else:
602
- st.warning(f"⚠️ **{summary['total_capacity_flags']} cases where UNICEF at capacity**")
603
- st.info("β†’ Humanizer staff utilized to meet demand")
604
-
605
- with col_priority2:
606
- if summary['unicef_sufficient']:
607
- st.metric("🎯 Optimization Efficiency", "100%")
608
- st.caption("All demand met with preferred staff only")
609
- else:
610
- efficiency = (1 - summary['total_capacity_flags'] / len(params.get('product_list', [1]))) * 100
611
- st.metric("🎯 Optimization Efficiency", f"{efficiency:.1f}%")
612
- st.caption("Percentage of demand met with preferred staff")
613
-
614
- else:
615
- # Placeholder content when no results
616
- st.markdown('<div class="optimization-panel">', unsafe_allow_html=True)
617
- st.markdown("## 🎯 Ready to Optimize")
618
- st.markdown("""
619
- Configure your optimization parameters in the sidebar and click **'πŸš€ Run Optimization'** to get started!
620
-
621
- ### What you'll see after optimization:
622
-
623
- - **πŸ“Š Summary**: Overall results and key performance metrics
624
- - **πŸ“ˆ Production**: Detailed production schedule and fulfillment analysis
625
- - **πŸ‘· Labor**: Employee allocation and shift assignments
626
- - **πŸ’° Costs**: Comprehensive cost breakdown and analysis
627
-
628
- ### Tips for better results:
629
- - Ensure your date range has sufficient demand data
630
- - Select appropriate employee types for your scenario
631
- - Consider using 'priority' constraint mode for realistic business operations
632
- """)
633
- st.markdown('</div>', unsafe_allow_html=True)
634
-
635
- # Show current configuration
636
- if st.session_state.date_range:
637
- st.markdown("### πŸ“‹ Current Configuration")
638
- col_config1, col_config2 = st.columns(2)
639
-
640
- with col_config1:
641
- st.markdown("**Selected Parameters:**")
642
- st.markdown(f"β€’ **Date Range:** {start_date} to {end_date}")
643
- st.markdown(f"β€’ **Employee Types:** {', '.join(selected_emp_types) if selected_emp_types else 'None selected'}")
644
- st.markdown(f"β€’ **Shifts:** {', '.join(map(str, selected_shifts)) if selected_shifts else 'None selected'}")
645
-
646
- with col_config2:
647
- st.markdown("**Configuration Status:**")
648
- status_emp = "βœ…" if selected_emp_types else "❌"
649
- status_shift = "βœ…" if selected_shifts else "❌"
650
- status_lines = "βœ…" if selected_lines else "❌"
651
-
652
- st.markdown(f"β€’ **Employee Types:** {status_emp}")
653
- st.markdown(f"β€’ **Shifts:** {status_shift}")
654
- st.markdown(f"β€’ **Production Lines:** {status_lines}")
655
-
656
- # Footer
657
- st.markdown("---")
658
- st.markdown("""
659
- <div style='text-align: center; color: gray; padding: 1rem;'>
660
- <small>Optimization Tool | Powered by OR-Tools | Real-time data integration</small>
661
- </div>
662
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/3_πŸ“ˆ_Enhanced_Reports.py DELETED
@@ -1,873 +0,0 @@
1
- import streamlit as st
2
-
3
- # Page configuration
4
- st.set_page_config(
5
- page_title="Enhanced Reports",
6
- page_icon="πŸ“ˆ",
7
- layout="wide"
8
- )
9
-
10
- # Import libraries
11
- import pandas as pd
12
- import plotly.express as px
13
- import plotly.graph_objects as go
14
- from plotly.subplots import make_subplots
15
- import sys
16
- import os
17
- from datetime import datetime, timedelta
18
- import numpy as np
19
-
20
- # Add src to path for imports
21
- sys.path.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'src'))
22
-
23
- try:
24
- import src.etl.extract as extract
25
- import src.etl.transform as transform
26
- from src.config import optimization_config
27
- except ImportError as e:
28
- st.error(f"Error importing modules: {e}")
29
- st.stop()
30
-
31
- # Custom CSS
32
- st.markdown("""
33
- <style>
34
- .main-header {
35
- font-size: 2.5rem;
36
- font-weight: bold;
37
- color: #1f77b4;
38
- margin-bottom: 1rem;
39
- }
40
- .section-header {
41
- font-size: 1.5rem;
42
- font-weight: bold;
43
- color: #2c3e50;
44
- margin: 1rem 0;
45
- }
46
- .metric-card {
47
- background-color: #f8f9fa;
48
- padding: 1rem;
49
- border-radius: 0.5rem;
50
- border-left: 4px solid #1f77b4;
51
- margin-bottom: 1rem;
52
- }
53
- .cost-highlight {
54
- background-color: #e8f5e8;
55
- padding: 1rem;
56
- border-radius: 0.5rem;
57
- border-left: 4px solid #28a745;
58
- margin: 1rem 0;
59
- }
60
- .production-highlight {
61
- background-color: #fff3cd;
62
- padding: 1rem;
63
- border-radius: 0.5rem;
64
- border-left: 4px solid #ffc107;
65
- margin: 1rem 0;
66
- }
67
- </style>
68
- """, unsafe_allow_html=True)
69
-
70
- # Initialize session state
71
- if 'date_range' not in st.session_state:
72
- st.session_state.date_range = None
73
-
74
- # Title
75
- st.markdown('<h1 class="main-header">πŸ“ˆ Enhanced Visualization Reports</h1>', unsafe_allow_html=True)
76
-
77
- # Sidebar for date selection
78
- with st.sidebar:
79
- st.markdown("## πŸ“… Date Selection")
80
-
81
- try:
82
- date_ranges = transform.get_date_ranges()
83
- if date_ranges:
84
- date_range_options = [f"{start.strftime('%Y-%m-%d')} to {end.strftime('%Y-%m-%d')}" for start, end in date_ranges]
85
- selected_range_str = st.selectbox(
86
- "Select date range:",
87
- options=date_range_options,
88
- help="Available date ranges from released orders"
89
- )
90
-
91
- selected_index = date_range_options.index(selected_range_str)
92
- start_date, end_date = date_ranges[selected_index]
93
- st.session_state.date_range = (start_date, end_date)
94
-
95
- duration = (end_date - start_date).days + 1
96
- st.info(f"Duration: {duration} days")
97
-
98
- else:
99
- st.warning("No date ranges found")
100
- start_date = datetime(2025, 3, 24).date()
101
- end_date = datetime(2025, 3, 28).date()
102
- st.session_state.date_range = (start_date, end_date)
103
-
104
- except Exception as e:
105
- st.error(f"Error loading dates: {e}")
106
- start_date = datetime(2025, 3, 24).date()
107
- end_date = datetime(2025, 3, 28).date()
108
- st.session_state.date_range = (start_date, end_date)
109
-
110
- st.markdown("---")
111
- st.markdown("## πŸ”„ Refresh Data")
112
- if st.button("πŸ”„ Reload All Data"):
113
- st.rerun()
114
-
115
- # Main content
116
- if st.session_state.date_range:
117
- start_date, end_date = st.session_state.date_range
118
- st.markdown(f"**Analysis Period:** {start_date} to {end_date}")
119
- else:
120
- st.warning("No date range selected")
121
- st.stop()
122
-
123
- # Create main tabs for the enhanced reports
124
- tab1, tab2, tab3, tab4 = st.tabs([
125
- "πŸ’° Employee Costs per Hour",
126
- "πŸ“‹ Production Plans & Orders",
127
- "🏭 Line Allocation by Day",
128
- "πŸ’΅ Total Costs & Analysis"
129
- ])
130
-
131
- # Tab 1: Employee Costs per Hour
132
- with tab1:
133
- st.markdown('<h2 class="section-header">πŸ’° Employee Costs per Hour Analysis</h2>', unsafe_allow_html=True)
134
-
135
- try:
136
- # Load employee cost data
137
- employee_df = extract.read_employee_data()
138
- cost_data = optimization_config.COST_LIST_PER_EMP_SHIFT
139
- shift_hours = optimization_config.MAX_HOUR_PER_SHIFT_PER_PERSON
140
-
141
- # Create comprehensive cost analysis
142
- st.markdown("### πŸ“Š Hourly Rate Breakdown")
143
-
144
- # Detailed cost table with all combinations
145
- cost_breakdown = []
146
- employee_counts = employee_df['employment_type'].value_counts()
147
-
148
- for emp_type, shifts in cost_data.items():
149
- emp_count = employee_counts.get(emp_type, 0)
150
-
151
- for shift_id, hourly_rate in shifts.items():
152
- shift_name = {1: 'Regular', 2: 'Overtime', 3: 'Evening'}.get(shift_id, f'Shift {shift_id}')
153
- shift_duration = shift_hours.get(shift_id, 0)
154
-
155
- cost_breakdown.append({
156
- 'Employee Type': emp_type,
157
- 'Shift': shift_name,
158
- 'Shift ID': shift_id,
159
- 'Available Staff': emp_count,
160
- 'Hourly Rate ($)': hourly_rate,
161
- 'Shift Duration (hrs)': shift_duration,
162
- 'Daily Cost per Employee ($)': hourly_rate * shift_duration,
163
- 'Total Daily Cost Potential ($)': hourly_rate * shift_duration * emp_count
164
- })
165
-
166
- cost_df = pd.DataFrame(cost_breakdown)
167
-
168
- # Display detailed cost table
169
- st.dataframe(cost_df, use_container_width=True)
170
-
171
- # Cost analysis visualizations
172
- col_cost1, col_cost2 = st.columns(2)
173
-
174
- with col_cost1:
175
- # Hourly rates comparison
176
- fig_hourly = px.bar(
177
- cost_df,
178
- x='Employee Type',
179
- y='Hourly Rate ($)',
180
- color='Shift',
181
- title='Hourly Rates by Employee Type and Shift',
182
- barmode='group',
183
- text='Hourly Rate ($)'
184
- )
185
- fig_hourly.update_traces(texttemplate='$%{text:.0f}', textposition='outside')
186
- st.plotly_chart(fig_hourly, use_container_width=True)
187
-
188
- with col_cost2:
189
- # Daily cost per employee
190
- fig_daily = px.bar(
191
- cost_df,
192
- x='Employee Type',
193
- y='Daily Cost per Employee ($)',
194
- color='Shift',
195
- title='Daily Cost per Employee by Type and Shift',
196
- barmode='group',
197
- text='Daily Cost per Employee ($)'
198
- )
199
- fig_daily.update_traces(texttemplate='$%{text:.0f}', textposition='outside')
200
- st.plotly_chart(fig_daily, use_container_width=True)
201
-
202
- # Cost efficiency analysis
203
- st.markdown("### πŸ“ˆ Cost Efficiency Analysis")
204
-
205
- col_eff1, col_eff2 = st.columns(2)
206
-
207
- with col_eff1:
208
- # Cost per hour vs productivity visualization
209
- fig_efficiency = px.scatter(
210
- cost_df,
211
- x='Shift Duration (hrs)',
212
- y='Hourly Rate ($)',
213
- size='Available Staff',
214
- color='Employee Type',
215
- title='Cost Efficiency: Hourly Rate vs Shift Duration',
216
- hover_data=['Shift', 'Total Daily Cost Potential ($)']
217
- )
218
- st.plotly_chart(fig_efficiency, use_container_width=True)
219
-
220
- with col_eff2:
221
- # Cost distribution pie chart
222
- emp_cost_totals = cost_df.groupby('Employee Type')['Total Daily Cost Potential ($)'].sum()
223
- fig_pie = px.pie(
224
- values=emp_cost_totals.values,
225
- names=emp_cost_totals.index,
226
- title='Total Daily Cost Potential Distribution'
227
- )
228
- st.plotly_chart(fig_pie, use_container_width=True)
229
-
230
- # Summary metrics
231
- st.markdown("### πŸ“‹ Cost Summary Metrics")
232
-
233
- col_summary1, col_summary2, col_summary3, col_summary4 = st.columns(4)
234
-
235
- total_potential_cost = cost_df['Total Daily Cost Potential ($)'].sum()
236
- min_hourly_rate = cost_df['Hourly Rate ($)'].min()
237
- max_hourly_rate = cost_df['Hourly Rate ($)'].max()
238
- avg_hourly_rate = cost_df['Hourly Rate ($)'].mean()
239
-
240
- with col_summary1:
241
- st.metric("πŸ’° Total Daily Potential", f"${total_potential_cost:,.2f}")
242
- with col_summary2:
243
- st.metric("πŸ“‰ Min Hourly Rate", f"${min_hourly_rate:.2f}")
244
- with col_summary3:
245
- st.metric("πŸ“ˆ Max Hourly Rate", f"${max_hourly_rate:.2f}")
246
- with col_summary4:
247
- st.metric("πŸ“Š Avg Hourly Rate", f"${avg_hourly_rate:.2f}")
248
-
249
- # Cost projections
250
- st.markdown('<div class="cost-highlight">', unsafe_allow_html=True)
251
- st.markdown("#### πŸ“… Cost Projections")
252
-
253
- duration = (end_date - start_date).days + 1
254
- period_cost = total_potential_cost * duration
255
- weekly_cost = total_potential_cost * 7
256
- monthly_cost = total_potential_cost * 30
257
-
258
- col_proj1, col_proj2, col_proj3 = st.columns(3)
259
-
260
- with col_proj1:
261
- st.metric("πŸ“… Current Period", f"${period_cost:,.2f}", f"{duration} days")
262
- with col_proj2:
263
- st.metric("πŸ“… Weekly Projection", f"${weekly_cost:,.2f}")
264
- with col_proj3:
265
- st.metric("πŸ“… Monthly Projection", f"${monthly_cost:,.2f}")
266
-
267
- st.markdown('</div>', unsafe_allow_html=True)
268
-
269
- except Exception as e:
270
- st.error(f"Error in employee cost analysis: {e}")
271
-
272
- # Tab 2: Production Plans & Orders
273
- with tab2:
274
- st.markdown('<h2 class="section-header">πŸ“‹ Production Plans & Orders Analysis</h2>', unsafe_allow_html=True)
275
-
276
- try:
277
- # Load production data
278
- demand_df = extract.read_released_orders_data(start_date=start_date, end_date=end_date)
279
- planned_df = extract.read_planned_orders_data(start_date=start_date, end_date=end_date)
280
-
281
- # Production overview
282
- st.markdown("### πŸ“Š Production Overview")
283
-
284
- col_prod1, col_prod2, col_prod3, col_prod4 = st.columns(4)
285
-
286
- total_released_orders = len(demand_df)
287
- total_planned_orders = len(planned_df) if planned_df is not None else 0
288
- total_released_quantity = demand_df["Order quantity (GMEIN)"].sum()
289
- unique_materials = demand_df["Material Number"].nunique()
290
-
291
- with col_prod1:
292
- st.metric("πŸ“¦ Released Orders", f"{total_released_orders:,}")
293
- with col_prod2:
294
- st.metric("πŸ“‹ Planned Orders", f"{total_planned_orders:,}")
295
- with col_prod3:
296
- st.metric("πŸ“Š Total Quantity", f"{total_released_quantity:,.0f}")
297
- with col_prod4:
298
- st.metric("🎯 Unique Materials", f"{unique_materials:,}")
299
-
300
- # Daily production schedule
301
- st.markdown("### πŸ“… Daily Production Schedule")
302
-
303
- # Convert dates and create daily view
304
- demand_df['Date'] = pd.to_datetime(demand_df['Basic finish date'])
305
- daily_production = demand_df.groupby(['Date', 'Material Number']).agg({
306
- 'Order quantity (GMEIN)': 'sum',
307
- 'Order': 'count'
308
- }).reset_index()
309
-
310
- # Daily summary
311
- daily_summary = demand_df.groupby('Date').agg({
312
- 'Order quantity (GMEIN)': 'sum',
313
- 'Order': 'count',
314
- 'Material Number': 'nunique'
315
- }).reset_index()
316
- daily_summary.columns = ['Date', 'Total Quantity', 'Total Orders', 'Unique Materials']
317
-
318
- col_daily1, col_daily2 = st.columns(2)
319
-
320
- with col_daily1:
321
- # Daily quantity trend
322
- fig_daily_qty = px.line(
323
- daily_summary,
324
- x='Date',
325
- y='Total Quantity',
326
- title='Daily Production Quantity Trend',
327
- markers=True
328
- )
329
- fig_daily_qty.update_layout(xaxis_title="Date", yaxis_title="Total Quantity")
330
- st.plotly_chart(fig_daily_qty, use_container_width=True)
331
-
332
- with col_daily2:
333
- # Daily orders count
334
- fig_daily_orders = px.bar(
335
- daily_summary,
336
- x='Date',
337
- y='Total Orders',
338
- title='Daily Orders Count',
339
- color='Total Orders',
340
- color_continuous_scale='Blues'
341
- )
342
- st.plotly_chart(fig_daily_orders, use_container_width=True)
343
-
344
- # Material analysis
345
- st.markdown("### 🎯 Material Analysis")
346
-
347
- # Top materials by quantity
348
- material_summary = demand_df.groupby('Material Number').agg({
349
- 'Order quantity (GMEIN)': 'sum',
350
- 'Order': 'count'
351
- }).reset_index()
352
- material_summary.columns = ['Material', 'Total Quantity', 'Order Count']
353
- material_summary = material_summary.sort_values('Total Quantity', ascending=False)
354
-
355
- col_mat1, col_mat2 = st.columns(2)
356
-
357
- with col_mat1:
358
- # Top 10 materials by quantity
359
- top_materials = material_summary.head(10)
360
- fig_top_mat = px.bar(
361
- top_materials,
362
- x='Material',
363
- y='Total Quantity',
364
- title='Top 10 Materials by Total Quantity',
365
- color='Total Quantity',
366
- color_continuous_scale='Viridis'
367
- )
368
- fig_top_mat.update_layout(xaxis_tickangle=-45)
369
- st.plotly_chart(fig_top_mat, use_container_width=True)
370
-
371
- with col_mat2:
372
- # Order frequency vs quantity scatter
373
- fig_scatter = px.scatter(
374
- material_summary,
375
- x='Order Count',
376
- y='Total Quantity',
377
- title='Order Frequency vs Total Quantity',
378
- hover_data=['Material'],
379
- size='Total Quantity',
380
- color='Order Count',
381
- color_continuous_scale='Plasma'
382
- )
383
- st.plotly_chart(fig_scatter, use_container_width=True)
384
-
385
- # Detailed production schedule table
386
- st.markdown("### πŸ“‹ Detailed Production Schedule")
387
-
388
- # Show daily breakdown with materials
389
- expanded_schedule = demand_df[['Date', 'Order', 'Material Number', 'Material description',
390
- 'Order quantity (GMEIN)', 'Basic start date', 'Basic finish date']].copy()
391
- expanded_schedule['Date'] = expanded_schedule['Date'].dt.strftime('%Y-%m-%d')
392
- expanded_schedule['Basic start date'] = pd.to_datetime(expanded_schedule['Basic start date']).dt.strftime('%Y-%m-%d')
393
- expanded_schedule['Basic finish date'] = pd.to_datetime(expanded_schedule['Basic finish date']).dt.strftime('%Y-%m-%d')
394
-
395
- st.dataframe(expanded_schedule, use_container_width=True)
396
-
397
- # Production capacity analysis
398
- st.markdown('<div class="production-highlight">', unsafe_allow_html=True)
399
- st.markdown("#### 🏭 Production Capacity Requirements")
400
-
401
- # Calculate required line hours per day
402
- line_capacity = optimization_config.PER_PRODUCT_SPEED
403
- material_line_req = {}
404
-
405
- for _, row in demand_df.iterrows():
406
- material = row['Material Number']
407
- quantity = row['Order quantity (GMEIN)']
408
- date = row['Date'].strftime('%Y-%m-%d')
409
-
410
- # Get line assignment (simplified - using line 6 as default)
411
- line_id = optimization_config.KIT_LINE_MATCH_DICT.get(material, 6)
412
- line_speed = line_capacity.get(line_id, 300) # Default 300 units/hour
413
-
414
- required_hours = quantity / line_speed if line_speed > 0 else 0
415
-
416
- if date not in material_line_req:
417
- material_line_req[date] = {}
418
- if line_id not in material_line_req[date]:
419
- material_line_req[date][line_id] = 0
420
-
421
- material_line_req[date][line_id] += required_hours
422
-
423
- # Display capacity requirements
424
- capacity_data = []
425
- for date, lines in material_line_req.items():
426
- for line_id, hours in lines.items():
427
- capacity_data.append({
428
- 'Date': date,
429
- 'Line': f'Line {line_id}',
430
- 'Required Hours': hours,
431
- 'Line Capacity (units/hr)': line_capacity.get(line_id, 300)
432
- })
433
-
434
- if capacity_data:
435
- capacity_df = pd.DataFrame(capacity_data)
436
-
437
- # Capacity visualization
438
- fig_capacity = px.bar(
439
- capacity_df,
440
- x='Date',
441
- y='Required Hours',
442
- color='Line',
443
- title='Daily Line Capacity Requirements',
444
- barmode='stack'
445
- )
446
- st.plotly_chart(fig_capacity, use_container_width=True)
447
-
448
- # Capacity summary table
449
- st.dataframe(capacity_df, use_container_width=True)
450
-
451
- st.markdown('</div>', unsafe_allow_html=True)
452
-
453
- except Exception as e:
454
- st.error(f"Error in production analysis: {e}")
455
-
456
- # Tab 3: Line Allocation by Day
457
- with tab3:
458
- st.markdown('<h2 class="section-header">🏭 Line Allocation by Day Analysis</h2>', unsafe_allow_html=True)
459
-
460
- try:
461
- # Load line and production data
462
- line_df = extract.read_packaging_line_data()
463
- demand_df = extract.read_released_orders_data(start_date=start_date, end_date=end_date)
464
-
465
- # Line capacity configuration
466
- line_capacity = optimization_config.PER_PRODUCT_SPEED
467
- shift_hours = optimization_config.MAX_HOUR_PER_SHIFT_PER_PERSON
468
-
469
- st.markdown("### 🏭 Production Line Overview")
470
-
471
- # Line metrics
472
- col_line1, col_line2, col_line3, col_line4 = st.columns(4)
473
-
474
- total_lines = line_df['line_count'].sum()
475
- line_types = len(line_df)
476
- max_capacity_line = line_df.loc[line_df['line_count'].idxmax()]
477
-
478
- with col_line1:
479
- st.metric("🏭 Total Lines", total_lines)
480
- with col_line2:
481
- st.metric("πŸ“‹ Line Types", line_types)
482
- with col_line3:
483
- st.metric("πŸ”Ί Max Capacity Line", f"Line {max_capacity_line['id']}")
484
- with col_line4:
485
- st.metric("πŸ“Š Max Line Count", max_capacity_line['line_count'])
486
-
487
- # Daily line allocation analysis
488
- st.markdown("### πŸ“… Daily Line Allocation Requirements")
489
-
490
- # Calculate daily requirements per line
491
- demand_df['Date'] = pd.to_datetime(demand_df['Basic finish date'])
492
- daily_line_allocation = {}
493
-
494
- # Group by date and calculate line requirements
495
- for date, date_group in demand_df.groupby('Date'):
496
- date_str = date.strftime('%Y-%m-%d')
497
- daily_line_allocation[date_str] = {}
498
-
499
- for _, row in date_group.iterrows():
500
- material = row['Material Number']
501
- quantity = row['Order quantity (GMEIN)']
502
-
503
- # Get line assignment
504
- line_id = optimization_config.KIT_LINE_MATCH_DICT.get(material, 6)
505
- line_speed = line_capacity.get(line_id, 300)
506
-
507
- # Calculate required hours
508
- required_hours = quantity / line_speed if line_speed > 0 else 0
509
-
510
- if line_id not in daily_line_allocation[date_str]:
511
- daily_line_allocation[date_str][line_id] = {
512
- 'required_hours': 0,
513
- 'materials': [],
514
- 'total_quantity': 0
515
- }
516
-
517
- daily_line_allocation[date_str][line_id]['required_hours'] += required_hours
518
- daily_line_allocation[date_str][line_id]['materials'].append(material)
519
- daily_line_allocation[date_str][line_id]['total_quantity'] += quantity
520
-
521
- # Create allocation visualization data
522
- allocation_data = []
523
- for date, lines in daily_line_allocation.items():
524
- for line_id, data in lines.items():
525
- line_info = line_df[line_df['id'] == line_id].iloc[0] if len(line_df[line_df['id'] == line_id]) > 0 else None
526
- available_lines = line_info['line_count'] if line_info is not None else 1
527
-
528
- # Calculate utilization per line
529
- utilization_per_line = data['required_hours'] / available_lines if available_lines > 0 else 0
530
- max_hours_per_line = sum(shift_hours.values()) # Total available hours per day
531
- utilization_percentage = (utilization_per_line / max_hours_per_line) * 100 if max_hours_per_line > 0 else 0
532
-
533
- allocation_data.append({
534
- 'Date': date,
535
- 'Line': f'Line {line_id}',
536
- 'Line ID': line_id,
537
- 'Required Hours': data['required_hours'],
538
- 'Available Lines': available_lines,
539
- 'Hours per Line': utilization_per_line,
540
- 'Utilization %': min(utilization_percentage, 100), # Cap at 100%
541
- 'Materials Count': len(set(data['materials'])),
542
- 'Total Quantity': data['total_quantity'],
543
- 'Max Hours Available': max_hours_per_line * available_lines
544
- })
545
-
546
- allocation_df = pd.DataFrame(allocation_data)
547
-
548
- if not allocation_df.empty:
549
- # Line utilization visualization
550
- col_util1, col_util2 = st.columns(2)
551
-
552
- with col_util1:
553
- # Daily utilization by line
554
- fig_util = px.bar(
555
- allocation_df,
556
- x='Date',
557
- y='Utilization %',
558
- color='Line',
559
- title='Daily Line Utilization Percentage',
560
- barmode='group'
561
- )
562
- fig_util.add_hline(y=100, line_dash="dash", line_color="red", annotation_text="100% Capacity")
563
- fig_util.update_layout(yaxis_title="Utilization %", xaxis_title="Date")
564
- st.plotly_chart(fig_util, use_container_width=True)
565
-
566
- with col_util2:
567
- # Required hours by line and date
568
- fig_hours = px.bar(
569
- allocation_df,
570
- x='Date',
571
- y='Required Hours',
572
- color='Line',
573
- title='Required Hours by Line and Date',
574
- barmode='stack'
575
- )
576
- st.plotly_chart(fig_hours, use_container_width=True)
577
-
578
- # Detailed allocation table
579
- st.markdown("### πŸ“‹ Detailed Line Allocation")
580
- st.dataframe(allocation_df, use_container_width=True)
581
-
582
- # Line capacity analysis
583
- st.markdown("### πŸ“Š Line Capacity Analysis")
584
-
585
- # Calculate daily totals
586
- daily_totals = allocation_df.groupby('Date').agg({
587
- 'Required Hours': 'sum',
588
- 'Max Hours Available': 'sum',
589
- 'Materials Count': 'sum',
590
- 'Total Quantity': 'sum'
591
- }).reset_index()
592
-
593
- daily_totals['Overall Utilization %'] = (daily_totals['Required Hours'] / daily_totals['Max Hours Available']) * 100
594
- daily_totals['Capacity Status'] = daily_totals['Overall Utilization %'].apply(
595
- lambda x: '🟒 Normal' if x <= 80 else ('🟑 High' if x <= 100 else 'πŸ”΄ Overload')
596
- )
597
-
598
- col_cap1, col_cap2 = st.columns(2)
599
-
600
- with col_cap1:
601
- # Overall daily utilization
602
- fig_overall = px.line(
603
- daily_totals,
604
- x='Date',
605
- y='Overall Utilization %',
606
- title='Overall Daily Capacity Utilization',
607
- markers=True
608
- )
609
- fig_overall.add_hline(y=80, line_dash="dash", line_color="orange", annotation_text="High Utilization (80%)")
610
- fig_overall.add_hline(y=100, line_dash="dash", line_color="red", annotation_text="Full Capacity (100%)")
611
- st.plotly_chart(fig_overall, use_container_width=True)
612
-
613
- with col_cap2:
614
- # Capacity status summary
615
- status_counts = daily_totals['Capacity Status'].value_counts()
616
- fig_status = px.pie(
617
- values=status_counts.values,
618
- names=status_counts.index,
619
- title='Daily Capacity Status Distribution'
620
- )
621
- st.plotly_chart(fig_status, use_container_width=True)
622
-
623
- # Summary metrics
624
- col_summary1, col_summary2, col_summary3, col_summary4 = st.columns(4)
625
-
626
- avg_utilization = daily_totals['Overall Utilization %'].mean()
627
- max_utilization = daily_totals['Overall Utilization %'].max()
628
- overload_days = len(daily_totals[daily_totals['Overall Utilization %'] > 100])
629
-
630
- with col_summary1:
631
- st.metric("πŸ“Š Avg Utilization", f"{avg_utilization:.1f}%")
632
- with col_summary2:
633
- st.metric("πŸ”Ί Peak Utilization", f"{max_utilization:.1f}%")
634
- with col_summary3:
635
- st.metric("πŸ”΄ Overload Days", overload_days)
636
- with col_summary4:
637
- total_capacity_hours = daily_totals['Max Hours Available'].iloc[0] if len(daily_totals) > 0 else 0
638
- st.metric("⚑ Daily Capacity", f"{total_capacity_hours:.0f} hrs")
639
-
640
- except Exception as e:
641
- st.error(f"Error in line allocation analysis: {e}")
642
-
643
- # Tab 4: Total Costs & Analysis
644
- with tab4:
645
- st.markdown('<h2 class="section-header">πŸ’΅ Total Costs & Analysis</h2>', unsafe_allow_html=True)
646
-
647
- try:
648
- # Load all necessary data for comprehensive cost analysis
649
- employee_df = extract.read_employee_data()
650
- demand_df = extract.read_released_orders_data(start_date=start_date, end_date=end_date)
651
- cost_data = optimization_config.COST_LIST_PER_EMP_SHIFT
652
- shift_hours = optimization_config.MAX_HOUR_PER_SHIFT_PER_PERSON
653
- line_capacity = optimization_config.PER_PRODUCT_SPEED
654
-
655
- st.markdown("### πŸ’° Comprehensive Cost Analysis")
656
-
657
- # Calculate production requirements and costs
658
- duration = (end_date - start_date).days + 1
659
- total_demand = demand_df["Order quantity (GMEIN)"].sum()
660
-
661
- # Estimate labor requirements
662
- total_production_hours = 0
663
- daily_requirements = {}
664
-
665
- demand_df['Date'] = pd.to_datetime(demand_df['Basic finish date'])
666
-
667
- for date, date_group in demand_df.groupby('Date'):
668
- date_str = date.strftime('%Y-%m-%d')
669
- daily_hours = 0
670
-
671
- for _, row in date_group.iterrows():
672
- material = row['Material Number']
673
- quantity = row['Order quantity (GMEIN)']
674
-
675
- # Get line speed
676
- line_id = optimization_config.KIT_LINE_MATCH_DICT.get(material, 6)
677
- line_speed = line_capacity.get(line_id, 300)
678
-
679
- # Calculate production hours needed
680
- hours_needed = quantity / line_speed if line_speed > 0 else 0
681
- daily_hours += hours_needed
682
-
683
- daily_requirements[date_str] = daily_hours
684
- total_production_hours += daily_hours
685
-
686
- # Cost scenario analysis
687
- st.markdown("### πŸ“Š Cost Scenario Analysis")
688
-
689
- # Calculate different staffing scenarios
690
- scenarios = {
691
- 'Minimum Cost': {'UNICEF Fixed term': 0.5, 'Humanizer': 0.5}, # 50% of each type
692
- 'Balanced': {'UNICEF Fixed term': 0.6, 'Humanizer': 0.4},
693
- 'Quality Focus': {'UNICEF Fixed term': 0.8, 'Humanizer': 0.2},
694
- 'Maximum Capacity': {'UNICEF Fixed term': 1.0, 'Humanizer': 1.0}
695
- }
696
-
697
- scenario_results = []
698
- employee_counts = employee_df['employment_type'].value_counts()
699
-
700
- for scenario_name, ratios in scenarios.items():
701
- scenario_cost = 0
702
- scenario_hours = 0
703
-
704
- for emp_type, ratio in ratios.items():
705
- available_staff = employee_counts.get(emp_type, 0)
706
- used_staff = int(available_staff * ratio)
707
-
708
- # Calculate cost for each shift type
709
- if emp_type in cost_data:
710
- for shift_id, hourly_rate in cost_data[emp_type].items():
711
- shift_duration = shift_hours.get(shift_id, 0)
712
- shift_cost = used_staff * hourly_rate * shift_duration
713
- scenario_cost += shift_cost
714
- scenario_hours += used_staff * shift_duration
715
-
716
- # Project for the entire period
717
- period_cost = scenario_cost * duration
718
-
719
- scenario_results.append({
720
- 'Scenario': scenario_name,
721
- 'Daily Cost ($)': scenario_cost,
722
- 'Period Cost ($)': period_cost,
723
- 'Daily Hours': scenario_hours,
724
- 'Cost per Hour ($)': scenario_cost / scenario_hours if scenario_hours > 0 else 0,
725
- 'Cost per Unit ($)': period_cost / total_demand if total_demand > 0 else 0
726
- })
727
-
728
- scenario_df = pd.DataFrame(scenario_results)
729
-
730
- # Scenario comparison visualization
731
- col_scenario1, col_scenario2 = st.columns(2)
732
-
733
- with col_scenario1:
734
- fig_scenario_cost = px.bar(
735
- scenario_df,
736
- x='Scenario',
737
- y='Period Cost ($)',
738
- title='Total Period Cost by Scenario',
739
- color='Period Cost ($)',
740
- color_continuous_scale='Reds'
741
- )
742
- fig_scenario_cost.update_layout(xaxis_tickangle=-45)
743
- st.plotly_chart(fig_scenario_cost, use_container_width=True)
744
-
745
- with col_scenario2:
746
- fig_scenario_unit = px.bar(
747
- scenario_df,
748
- x='Scenario',
749
- y='Cost per Unit ($)',
750
- title='Cost per Unit by Scenario',
751
- color='Cost per Unit ($)',
752
- color_continuous_scale='Blues'
753
- )
754
- fig_scenario_unit.update_layout(xaxis_tickangle=-45)
755
- st.plotly_chart(fig_scenario_unit, use_container_width=True)
756
-
757
- # Scenario details table
758
- st.dataframe(scenario_df, use_container_width=True)
759
-
760
- # Daily cost breakdown
761
- st.markdown("### πŸ“… Daily Cost Breakdown")
762
-
763
- # Calculate daily costs based on requirements
764
- daily_cost_data = []
765
- for date_str, required_hours in daily_requirements.items():
766
-
767
- # Simple allocation: distribute hours across available staff
768
- total_available_hours = 0
769
- for emp_type in cost_data.keys():
770
- available_staff = employee_counts.get(emp_type, 0)
771
- for shift_id in cost_data[emp_type].keys():
772
- total_available_hours += available_staff * shift_hours.get(shift_id, 0)
773
-
774
- if total_available_hours > 0:
775
- utilization_rate = min(required_hours / total_available_hours, 1.0)
776
-
777
- daily_cost = 0
778
- for emp_type in cost_data.keys():
779
- available_staff = employee_counts.get(emp_type, 0)
780
- for shift_id, hourly_rate in cost_data[emp_type].items():
781
- shift_duration = shift_hours.get(shift_id, 0)
782
- # Allocate proportionally
783
- used_hours = available_staff * shift_duration * utilization_rate
784
- daily_cost += used_hours * hourly_rate
785
-
786
- daily_cost_data.append({
787
- 'Date': date_str,
788
- 'Required Hours': required_hours,
789
- 'Utilization Rate': utilization_rate * 100,
790
- 'Daily Cost ($)': daily_cost,
791
- 'Cost per Hour ($)': daily_cost / required_hours if required_hours > 0 else 0
792
- })
793
-
794
- if daily_cost_data:
795
- daily_cost_df = pd.DataFrame(daily_cost_data)
796
-
797
- col_daily1, col_daily2 = st.columns(2)
798
-
799
- with col_daily1:
800
- fig_daily_cost = px.line(
801
- daily_cost_df,
802
- x='Date',
803
- y='Daily Cost ($)',
804
- title='Daily Labor Cost Trend',
805
- markers=True
806
- )
807
- st.plotly_chart(fig_daily_cost, use_container_width=True)
808
-
809
- with col_daily2:
810
- fig_daily_util = px.bar(
811
- daily_cost_df,
812
- x='Date',
813
- y='Utilization Rate',
814
- title='Daily Labor Utilization Rate (%)',
815
- color='Utilization Rate',
816
- color_continuous_scale='RdYlGn'
817
- )
818
- st.plotly_chart(fig_daily_util, use_container_width=True)
819
-
820
- # Daily breakdown table
821
- st.dataframe(daily_cost_df, use_container_width=True)
822
-
823
- # Summary metrics
824
- st.markdown("### πŸ“‹ Cost Summary")
825
-
826
- col_total1, col_total2, col_total3, col_total4 = st.columns(4)
827
-
828
- if daily_cost_data:
829
- total_period_cost = sum(item['Daily Cost ($)'] for item in daily_cost_data)
830
- avg_daily_cost = total_period_cost / len(daily_cost_data)
831
- total_required_hours = sum(item['Required Hours'] for item in daily_cost_data)
832
- avg_cost_per_hour = total_period_cost / total_required_hours if total_required_hours > 0 else 0
833
- else:
834
- total_period_cost = avg_daily_cost = total_required_hours = avg_cost_per_hour = 0
835
-
836
- with col_total1:
837
- st.metric("πŸ’° Total Period Cost", f"${total_period_cost:,.2f}")
838
- with col_total2:
839
- st.metric("πŸ“… Avg Daily Cost", f"${avg_daily_cost:,.2f}")
840
- with col_total3:
841
- st.metric("⏰ Total Labor Hours", f"{total_required_hours:,.0f}")
842
- with col_total4:
843
- st.metric("πŸ’΅ Avg Cost/Hour", f"${avg_cost_per_hour:.2f}")
844
-
845
- # ROI and efficiency metrics
846
- st.markdown('<div class="cost-highlight">', unsafe_allow_html=True)
847
- st.markdown("#### πŸ“ˆ Efficiency Metrics")
848
-
849
- col_eff1, col_eff2, col_eff3 = st.columns(3)
850
-
851
- cost_per_unit = total_period_cost / total_demand if total_demand > 0 else 0
852
- cost_per_day = total_period_cost / duration if duration > 0 else 0
853
-
854
- with col_eff1:
855
- st.metric("πŸ’΅ Cost per Unit", f"${cost_per_unit:.3f}")
856
- with col_eff2:
857
- st.metric("πŸ“… Cost per Day", f"${cost_per_day:,.2f}")
858
- with col_eff3:
859
- hours_per_unit = total_required_hours / total_demand if total_demand > 0 else 0
860
- st.metric("⏰ Hours per Unit", f"{hours_per_unit:.3f}")
861
-
862
- st.markdown('</div>', unsafe_allow_html=True)
863
-
864
- except Exception as e:
865
- st.error(f"Error in total cost analysis: {e}")
866
-
867
- # Footer
868
- st.markdown("---")
869
- st.markdown("""
870
- <div style='text-align: center; color: gray; padding: 1rem;'>
871
- <small>Enhanced Visualization Reports | Real-time data analysis | Updated: {}</small>
872
- </div>
873
- """.format(datetime.now().strftime('%Y-%m-%d %H:%M')), unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml.backup DELETED
@@ -1,35 +0,0 @@
1
- [project]
2
- name = "supply-roster-tool-real"
3
- version = "0.1.0"
4
- description = ""
5
- authors = [
6
- {name = "HaLim Jun",email = "hjun@unicef.org"}
7
- ]
8
- license = {text = "MIT"}
9
- readme = "README.md"
10
- requires-python = ">=3.10,<3.11"
11
- dependencies = [
12
- "absl-py==2.3.1",
13
- "dotenv==0.9.9",
14
- "immutabledict==4.2.1",
15
- "numpy==2.2.6",
16
- "ortools==9.14.6206",
17
- "pandas==2.3.1",
18
- "protobuf==6.31.1",
19
- "psycopg2-binary==2.9.9",
20
- "python-dateutil==2.9.0.post0",
21
- "python-dotenv==1.0.0",
22
- "pytz==2025.2",
23
- "six==1.17.0",
24
- "SQLAlchemy==2.0.36",
25
- "typing_extensions==4.14.1",
26
- "tzdata==2025.2",
27
-
28
-
29
-
30
- ]
31
-
32
-
33
- [build-system]
34
- requires = ["poetry-core>=2.0.0,<3.0.0"]
35
- build-backend = "poetry.core.masonry.api"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
run_streamlit.py DELETED
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Simple runner script for the SD Roster Optimization Streamlit app.
4
- """
5
-
6
- import subprocess
7
- import sys
8
- import os
9
-
10
- def main():
11
- """Run the Streamlit app"""
12
- # Change to the project directory
13
- project_dir = os.path.dirname(os.path.abspath(__file__))
14
- os.chdir(project_dir)
15
-
16
- # Run streamlit
17
- try:
18
- subprocess.run([
19
- sys.executable, "-m", "streamlit", "run", "Home.py",
20
- "--server.port", "8501",
21
- "--server.address", "localhost"
22
- ], check=True)
23
- except subprocess.CalledProcessError as e:
24
- print(f"Error running Streamlit: {e}")
25
- return 1
26
- except KeyboardInterrupt:
27
- print("\nStreamlit app stopped by user")
28
- return 0
29
-
30
- return 0
31
-
32
- if __name__ == "__main__":
33
- exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/models/optimizer_real.py DELETED
@@ -1,499 +0,0 @@
1
- # Option A (with lines) + 7-day horizon (weekly demand only)
2
- # Generalized: arbitrary products (product_list) and day-varying headcount N_day[e][t]
3
- # -----------------------------------------------------------------------------
4
- # pip install ortools
5
- from ortools.linear_solver import pywraplp
6
- import pandas as pd
7
- import sys
8
- import os
9
-
10
- sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
11
-
12
- from src.config import optimization_config
13
- import importlib
14
-
15
- importlib.reload(optimization_config)
16
-
17
-
18
- class OptimizerReal:
19
- def __init__(self):
20
- self.config = optimization_config
21
-
22
- def solve_option_A_multi_day_generalized(self):
23
- # -----------------------------
24
- # 1) SETS
25
- # -----------------------------
26
- # Days
27
- days = self.config.DATE_SPAN
28
-
29
- # Products (master set; you can have many)
30
- # Fill with all SKUs that may appear over the week
31
- product_list = self.config.PRODUCT_LIST # EDIT: add/remove products freely
32
-
33
- # Employee types (fixed to two types Fixed,Humanizer; headcount varies by day)
34
- employee_types = self.config.EMPLOYEE_TYPE_LIST
35
-
36
- # Shifts: 1=usual, 2=overtime, 3=evening
37
- shift_list = self.config.SHIFT_LIST
38
-
39
- # Line types and explicit line list
40
- line_list = self.config.LINE_LIST
41
- line_cnt_per_type = self.config.LINE_CNT_PER_TYPE # number of physical lines per type (EDIT)
42
- line_type_cnt_tuple = [
43
- (t, i) for t in line_list for i in range(1, line_cnt_per_type[t] + 1)
44
- ] # pair of line type and line number (e.g., ('long', 1))
45
-
46
- # -----------------------------
47
- # 2) PARAMETERS (EDIT THESE)
48
- # -----------------------------
49
- # Weekly demand (units) for each product in product_list
50
- weekly_demand = self.config.DEMAND_DICTIONARY
51
-
52
- # Validate demand - check if any products have positive demand
53
- total_demand = sum(weekly_demand.get(p, 0) for p in product_list)
54
- if total_demand == 0:
55
- print("Warning: Total demand is zero for all products. Optimization may not be meaningful.")
56
- print("Products:", product_list)
57
- print("Demands:", {p: weekly_demand.get(p, 0) for p in product_list})
58
-
59
- # Daily activity toggle for each product (1=can be produced on day t; 0=cannot)
60
- # If a product is not active on a day, we force its production and hours to 0 that day.
61
- active = {
62
- t: {p: 1 for p in product_list} for t in days
63
- } # EDIT per day if some SKUs are not available
64
-
65
- # Per-hour labor cost by employee type & shift
66
- wage_types = self.config.COST_LIST_PER_EMP_SHIFT
67
-
68
- # Productivity productivities[e][s][p] = units per hour (assumed line-independent here)
69
- # Provide entries for ALL products in product_list
70
- productivities = self.config.PRODUCTIVITY_LIST_PER_EMP_PRODUCT
71
- # If productivity depends on line, switch to q_line[(e,s,p,ell)] and use it in constraints.
72
-
73
- # Day-varying available headcount per type
74
- # N_day[e][t] = number of employees of type e available on day t
75
- N_day = self.config.MAX_EMPLOYEE_PER_TYPE_ON_DAY
76
-
77
- # Limits
78
- Hmax_daily_per_person = (
79
- self.config.MAX_HOUR_PER_PERSON_PER_DAY
80
- ) # per person per day
81
- Hmax_shift = self.config.MAX_HOUR_PER_SHIFT_PER_PERSON # per-shift hour caps
82
- # Per-line unit/hour capacity (physical)
83
- Cap = self.config.PER_PRODUCT_SPEED
84
-
85
- # Fixed regular hours for type Fixed on shift 1
86
- # BUSINESS LOGIC: Fixed staff availability vs mandatory hours
87
- # Controlled by FIXED_STAFF_CONSTRAINT_MODE in optimization_config
88
-
89
- first_shift_hour = Hmax_shift[1]
90
- daily_weekly_type = self.config.DAILY_WEEKLY_SCHEDULE
91
- constraint_mode = self.config.FIXED_STAFF_CONSTRAINT_MODE
92
-
93
- if constraint_mode == "mandatory":
94
- # Option 1: Mandatory hours (forces staff to work even when idle)
95
- F_x1_day = {t: first_shift_hour * N_day["UNICEF Fixed term"][t] for t in days}
96
- print(f"Using MANDATORY fixed hours constraint: {sum(F_x1_day.values())} hours/week")
97
- elif constraint_mode == "priority":
98
- # Option 3: Priority-based (realistic business model)
99
- F_x1_day = None
100
- print("Using PRIORITY constraint - fixed staff first, then temporary staff")
101
- elif constraint_mode == "none":
102
- # Option 4: No constraint (fully demand-driven)
103
- F_x1_day = None
104
- print("Using NO fixed hours constraint - demand-driven scheduling")
105
- else:
106
- raise ValueError(f"Invalid FIXED_STAFF_CONSTRAINT_MODE: {constraint_mode}. Use 'mandatory', 'available', 'priority', or 'none'")
107
-
108
- # e.g., F_x1_day = sum(F_x1_day.values()) if you want weekly instead (then set F_x1_day=None)
109
- PER_PRODUCT_SPEED = self.config.PER_PRODUCT_SPEED
110
-
111
- # Optional skill/compatibility: allow[(e,p,ell)] = 1/0 (1=allowed; 0=forbid)
112
- allow = {}
113
- for e in employee_types:
114
- for p in product_list:
115
- for ell in line_type_cnt_tuple:
116
- allow[(e, p, ell)] = 1 # EDIT as needed
117
-
118
- # -----------------------------
119
- # 3) SOLVER
120
- # -----------------------------
121
- solver = pywraplp.Solver.CreateSolver("CBC") # open-source mixed-integer program (MIP) solver
122
- if not solver:
123
- raise RuntimeError("Failed to create solver. Check OR-Tools installation.")
124
- INF = solver.infinity()
125
-
126
- # -----------------------------
127
- # 4) DECISION VARIABLES
128
- # -----------------------------
129
- # h[e,s,p,ell,t] = worker-hours of type e on shift s for product p on line ell on day t (integer)
130
-
131
- h = {}
132
- for e in employee_types:
133
- for s in shift_list:
134
- for p in product_list:
135
- for ell in line_type_cnt_tuple:
136
- for t in days:
137
- # Upper bound of labor hour per (e,s,t): shift hour cap * available headcount that day
138
-
139
- ub = Hmax_shift[s] * N_day[e][t]
140
- h[e, s, p, ell, t] = solver.IntVar(
141
- 0, ub, f"h_{e}_{s}_{p}_{ell[0]}{ell[1]}_d{t}"
142
- )# h = work hour per (employee type,shift,t-day) is decided somewhere between 0 and ub and is an integer
143
-
144
- # u[p,ell,s,t] = units of product p produced on line ell during shift s on day t
145
- #Maybe we need upper bound here
146
- u = {}
147
- for p in product_list:
148
- for ell in line_type_cnt_tuple:
149
- for s in shift_list:
150
- for t in days:
151
- u[p, ell, s, t] = solver.NumVar(
152
- 0, INF, f"u_{p}_{ell[0]}{ell[1]}_{s}_d{t}"
153
- )
154
-
155
- # tline[ell,s,t] = operating hours of line ell during shift s on day t
156
- # tline = line operating hour per (line,shift,t-day) is decided somewhere between 0 and Hmax_shift[s] and is a real number
157
- tline = {}
158
- for ell in line_type_cnt_tuple:
159
- for s in shift_list:
160
- for t in days:
161
- tline[ell, s, t] = solver.NumVar(
162
- 0, Hmax_shift[s], f"t_{ell[0]}{ell[1]}_{s}_d{t}"
163
- )
164
-
165
- # ybin[e,s,t] = shift usage binaries per type/day (to gate OT after usual)
166
- # ybin = shift usage binary per (employee type,shift,t-day) is decided somewhere between 0 and 1 and is a binary number
167
- ybin = {}
168
- for e in employee_types:
169
- for s in shift_list:
170
- for t in days:
171
- ybin[e, s, t] = solver.BoolVar(f"y_{e}_{s}_d{t}")
172
-
173
- # -----------------------------
174
- # 5) OBJECTIVE: Minimize total labor cost over the week
175
- # -----------------------------
176
- print("wage_types",wage_types)
177
- print("h",h)
178
- solver.Minimize(
179
- solver.Sum(
180
- wage_types[e][s] * h[e, s, p, ell, t]
181
- for e in employee_types
182
- for s in shift_list
183
- for p in product_list
184
- for ell in line_type_cnt_tuple
185
- for t in days
186
- )
187
- )
188
-
189
- # -----------------------------
190
- # 6) CONSTRAINTS
191
- # -----------------------------
192
-
193
- # 6.1 Weekly demand (no daily demand)
194
- #unit of production for p over the week should be larger or equal to demand value
195
- for p in product_list:
196
- demand_value = weekly_demand.get(p, 0)
197
- if demand_value > 0: # Only add constraint if there's actual demand
198
- solver.Add(
199
- solver.Sum(u[p, ell, s, t] for ell in line_type_cnt_tuple for s in shift_list for t in days)
200
- >= demand_value
201
- )
202
-
203
-
204
- # 6.2 If a product is inactive on a day, force zero production and hours for that day
205
- # This makes "varying products per day" explicit.
206
- # BIG_H = max(Hmax_shift.values()) * sum(N_day[e][t] for e in employee_types for t in days)
207
- for p in product_list:
208
- for t in days:
209
- if active[t][p] == 0:
210
- for ell in line_type_cnt_tuple:
211
- for s in shift_list:
212
- #If if not active, production unit is 0
213
- solver.Add(u[p, ell, s, t] == 0)
214
- #If if not active, work hour is 0
215
- for e in employee_types:
216
- solver.Add(h[e, s, p, ell, t] == 0)
217
-
218
- # 6.3 Labor -> units (per line/shift/day)
219
- #Total production unit based on labor productivity cap
220
- # If productivity depends on line, swap productivities[e][s][p] with q_line[(e,s,p,ell)] here.
221
-
222
- for p in product_list:
223
- for ell in line_type_cnt_tuple:
224
- for s in shift_list:
225
- for t in days:
226
- # Gate by activity (if inactive, both sides are already 0 from 6.2)
227
- solver.Add(
228
- u[p, ell, s, t]
229
- <= solver.Sum(productivities[e][s][p] * h[e, s, p, ell, t] for e in employee_types)
230
- )
231
-
232
- # 6.4 Per-line throughput cap (units/hour Γ— line-hours)
233
- #per line production cap for each line
234
- #tline = line operating hour per (line,shift,t-day)
235
- for ell in line_type_cnt_tuple:
236
- for s in shift_list:
237
- for t in days:
238
- line_type = ell[0] # 'long' or 'short'
239
- solver.Add(
240
- solver.Sum(u[p, ell, s, t] for p in product_list)
241
- <= PER_PRODUCT_SPEED[line_type] * tline[ell, s, t]
242
- )
243
-
244
- # 6.5 Couple line hours & worker-hours (multi-operator lines)
245
- # Multiple workers can work on a line simultaneously, up to MAX_PARALLEL_WORKERS limit
246
- for ell in line_type_cnt_tuple:
247
- line_type = ell[0] # 6 or 7
248
- max_workers = self.config.MAX_PARALLEL_WORKERS[line_type]
249
- for s in shift_list:
250
- for t in days:
251
- solver.Add(
252
- solver.Sum(h[e, s, p, ell, t] for e in employee_types for p in product_list)
253
- <= max_workers * tline[ell, s, t]
254
- )
255
-
256
- # 6.6 Fixed regular hours for type Fixed on shift 1
257
- if F_x1_day is not None:
258
- # Per-day fixed hours (mandatory - expensive)
259
- for t in days:
260
- solver.Add(
261
- solver.Sum(h["UNICEF Fixed term", 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
262
- == F_x1_day[t]
263
- )
264
- print("Applied mandatory fixed hours constraint")
265
-
266
- else:
267
- # No fixed constraint - purely demand-driven (cost-efficient)
268
- print("No mandatory fixed hours constraint - using demand-driven scheduling")
269
- # The availability constraint (6.7) already limits maximum hours
270
-
271
- # Special handling for priority mode
272
- if constraint_mode == "priority":
273
- print("Implementing priority constraints: UNICEF Fixed term used before Humanizer")
274
- # Add constraints to prioritize fixed staff usage before temporary staff
275
-
276
- # Store unicef_at_capacity variables for later inspection
277
- unicef_capacity_vars = {}
278
-
279
- # Priority constraint: For each day, product, and line,
280
- # Humanizer hours can only be used if UNICEF Fixed term is at capacity
281
- for t in days:
282
- for p in product_list:
283
- for ell in line_type_cnt_tuple:
284
- # Create binary variable to indicate if UNICEF Fixed term is at capacity
285
- unicef_at_capacity = solver.IntVar(0, 1, f"unicef_at_capacity_{p}_{ell[0]}{ell[1]}_d{t}")
286
- unicef_capacity_vars[p, ell, t] = unicef_at_capacity # Store for later
287
-
288
- # Calculate maximum possible hours for Humanizer staff this day
289
- max_humanizer_hours = sum(Hmax_shift[s] * N_day["Humanizer"][t] for s in shift_list)
290
-
291
- # If UNICEF is not at capacity (unicef_at_capacity = 0), then Humanizer hours must be 0
292
- # If UNICEF is at capacity (unicef_at_capacity = 1), then Humanizer can work up to their limit
293
- solver.Add(
294
- solver.Sum(h["Humanizer", s, p, ell, t] for s in shift_list)
295
- <= unicef_at_capacity * max_humanizer_hours # Correct M value
296
- )
297
-
298
- # Calculate maximum possible hours for UNICEF Fixed term staff this day
299
- max_unicef_hours = sum(Hmax_shift[s] * N_day["UNICEF Fixed term"][t] for s in shift_list)
300
-
301
- # Simple logic: unicef_at_capacity = 1 if and only if UNICEF uses ALL available hours
302
- # This ensures Humanizer is only used when UNICEF is completely maxed out
303
- solver.Add(
304
- solver.Sum(h["UNICEF Fixed term", s, p, ell, t] for s in shift_list)
305
- >= unicef_at_capacity * max_unicef_hours # If capacity=1, UNICEF must use max hours
306
- )
307
-
308
- if max_unicef_hours > 0:
309
- # Upper-bound link with small epsilon so it works with 0.1-hour granularity
310
- eps = 0.1 # smallest time unit (hours)
311
-
312
- # If capacity = 0 β†’ UNICEF ≀ max_unicef_hours - eps
313
- # If capacity = 1 β†’ UNICEF ≀ max_unicef_hours (tight)
314
- solver.Add(
315
- solver.Sum(
316
- h["UNICEF Fixed term", s, p, ell, t] for s in shift_list
317
- )
318
- <= max_unicef_hours - eps + unicef_at_capacity * eps
319
- )
320
- else:
321
- # No UNICEF staff that day β†’ capacity flag must be 0
322
- solver.Add(unicef_at_capacity == 0)
323
-
324
- # 6.7 Daily hours cap per employee type (14h per person per day)
325
- for e in employee_types:
326
- for t in days:
327
- solver.Add(
328
- solver.Sum(
329
- h[e, s, p, ell, t] for s in shift_list for p in product_list for ell in line_type_cnt_tuple
330
- )
331
- <= Hmax_daily_per_person * N_day[e][t]
332
- )
333
-
334
- # 6.8 Link hours to shift-usage binaries (per type/day)
335
- # Use a type/day-specific Big-M: M_e_s_t = Hmax_shift[s] * N_day[e][t]
336
- for e in employee_types:
337
- for s in shift_list:
338
- for t in days:
339
- M_e_s_t = Hmax_shift[s] * N_day[e][t]
340
- solver.Add(
341
- solver.Sum(h[e, s, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
342
- <= M_e_s_t * ybin[e, s, t]
343
- )
344
-
345
- # 6.9 Overtime only after usual (per day). Also bound OT hours <= usual hours
346
- # Binary activation variable for employee type, shift and day
347
- for e in employee_types:
348
- for t in days:
349
- solver.Add(ybin[e, 2, t] <= ybin[e, 1, t])
350
- solver.Add(
351
- solver.Sum(h[e, 2, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
352
- <= solver.Sum(h[e, 1, p, ell, t] for p in product_list for ell in line_type_cnt_tuple)
353
- )
354
- # (Optional) evening only after usual:
355
- # for e in employee_types:
356
- # for t in days:
357
- # solver.Add(ybin[e, 3, t] <= ybin[e, 1, t])
358
-
359
- # 6.10 Skill/compatibility mask
360
- for e in employee_types:
361
- for p in product_list:
362
- for ell in line_type_cnt_tuple:
363
- if allow[(e, p, ell)] == 0:
364
- for s in shift_list:
365
- for t in days:
366
- solver.Add(h[e, s, p, ell, t] == 0)
367
-
368
- # -----------------------------
369
- # 7) SOLVE
370
- # -----------------------------
371
- status = solver.Solve()
372
- if status != pywraplp.Solver.OPTIMAL:
373
- print("No optimal solution. Status:", status)
374
- return {
375
- 'status': 'failed',
376
- 'solver_status': status,
377
- 'message': f"No optimal solution found. Solver status: {status}"
378
- }
379
-
380
- # -----------------------------
381
- # 8) REPORT
382
- # -----------------------------
383
- total_cost = solver.Objective().Value()
384
- print("Objective (min cost):", total_cost)
385
-
386
- # Collect production results
387
- production_results = {}
388
- print("\n--- Weekly production by product ---")
389
- for p in product_list:
390
- produced = sum(
391
- u[p, ell, s, t].solution_value() for ell in line_type_cnt_tuple for s in shift_list for t in days
392
- )
393
- production_results[p] = {
394
- 'produced': produced,
395
- 'demand': weekly_demand.get(p, 0),
396
- 'fulfillment_rate': (produced / weekly_demand.get(p, 1)) * 100 if weekly_demand.get(p, 0) > 0 else 0
397
- }
398
- print(f"{p}: {produced:.1f} (weekly demand {weekly_demand.get(p,0)})")
399
-
400
- # Collect line operating hours
401
- line_hours = {}
402
- print("\n--- Line operating hours by shift/day ---")
403
- for ell in line_type_cnt_tuple:
404
- line_hours[ell] = {}
405
- for s in shift_list:
406
- hours = [tline[ell, s, t].solution_value() for t in days]
407
- line_hours[ell][s] = hours
408
- if sum(hours) > 1e-6:
409
- print(
410
- f"Line {ell} Shift {s}: "
411
- + ", ".join([f"days{t}={hours[t-1]:.2f}h" for t in days])
412
- )
413
-
414
- # Collect employee hours
415
- employee_hours = {}
416
- print("\n--- Hours by employee type / shift / day ---")
417
- for e in employee_types:
418
- employee_hours[e] = {}
419
- for s in shift_list:
420
- day_hours = [
421
- sum(h[e, s, p, ell, t].solution_value() for p in product_list for ell in line_type_cnt_tuple)
422
- for t in days
423
- ]
424
- employee_hours[e][s] = day_hours
425
- if sum(day_hours) > 1e-6:
426
- print(
427
- f"e={e}, s={s}: "
428
- + ", ".join([f"days{t}={day_hours[t-1]:.2f}h" for t in days])
429
- )
430
-
431
- # Collect headcount requirements
432
- headcount_requirements = {}
433
- print("\n--- Implied headcount by type / shift / day ---")
434
- for e in employee_types:
435
- headcount_requirements[e] = {}
436
- print(e)
437
- for s in shift_list:
438
- row = []
439
- daily_headcount = []
440
- for t in days:
441
- hours = sum(
442
- h[e, s, p, ell, t].solution_value() for p in product_list for ell in line_type_cnt_tuple
443
- )
444
- need = int((hours + Hmax_shift[s] - 1) // Hmax_shift[s]) # ceil
445
- daily_headcount.append(need)
446
- row.append(f"days{t}={need}")
447
-
448
- headcount_requirements[e][s] = daily_headcount
449
- if any("=0" not in Fixed for Fixed in row):
450
- print(f"e={e}, s={s}: " + ", ".join(row))
451
-
452
- # Collect priority mode results
453
- priority_results = None
454
- if constraint_mode == "priority" and 'unicef_capacity_vars' in locals():
455
- priority_results = {}
456
- print("\n--- UNICEF At Capacity Status (Priority Mode) ---")
457
- for (p, ell, t), var in unicef_capacity_vars.items():
458
- capacity_value = var.solution_value()
459
- if capacity_value > 0.5: # Binary variable, so > 0.5 means 1
460
- priority_results[(p, ell, t)] = capacity_value
461
- print(f"Product {p}, Line {ell}, Day {t}: UNICEF at capacity = {capacity_value:.0f}")
462
-
463
- # Summary
464
- total_capacity_flags = sum(1 for var in unicef_capacity_vars.values() if var.solution_value() > 0.5)
465
- if total_capacity_flags == 0:
466
- print("βœ… All unicef_at_capacity = 0 β†’ UNICEF Fixed term staff sufficient for all demand")
467
- print(" β†’ Humanizer staff not needed")
468
- else:
469
- print(f"⚠️ {total_capacity_flags} cases where UNICEF at capacity β†’ Humanizer staff used")
470
-
471
- priority_results['summary'] = {
472
- 'total_capacity_flags': total_capacity_flags,
473
- 'unicef_sufficient': total_capacity_flags == 0
474
- }
475
-
476
- # Return structured results
477
- return {
478
- 'status': 'optimal',
479
- 'total_cost': total_cost,
480
- 'production_results': production_results,
481
- 'line_hours': line_hours,
482
- 'employee_hours': employee_hours,
483
- 'headcount_requirements': headcount_requirements,
484
- 'priority_results': priority_results,
485
- 'parameters': {
486
- 'days': days,
487
- 'product_list': product_list,
488
- 'employee_types': employee_types,
489
- 'shift_list': shift_list,
490
- 'line_list': line_list,
491
- 'constraint_mode': constraint_mode,
492
- 'total_demand': sum(weekly_demand.get(p, 0) for p in product_list)
493
- }
494
- }
495
-
496
-
497
- if __name__ == "__main__":
498
- optimizer = OptimizerReal()
499
- optimizer.solve_option_A_multi_day_generalized()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/project DELETED
@@ -1 +0,0 @@
1
- Subproject commit 2e1b97c9d8196552a23dd5a4c536f25e53c033dc
 
 
src/visualization/Home.py DELETED
@@ -1,73 +0,0 @@
1
- import streamlit as st
2
-
3
- # Page configuration
4
- st.set_page_config(
5
- page_title="Supply Roster Tool",
6
- page_icon="🏠",
7
- layout="wide",
8
- initial_sidebar_state="expanded"
9
- )
10
-
11
- # Initialize session state for shared variables
12
- if 'data_path' not in st.session_state:
13
- st.session_state.data_path = "data/my_roster_data"
14
- if 'target_date' not in st.session_state:
15
- st.session_state.target_date = ""
16
-
17
- # Main page content
18
- st.title("🏠 Supply Roster Optimization Tool")
19
- st.markdown("---")
20
-
21
- # Welcome section
22
-
23
-
24
- col1, col2 = st.columns([1, 1])
25
-
26
- with col1:
27
- st.markdown("""
28
- ## πŸ“‹ Welcome to Supply Roster Tool
29
-
30
- """)
31
-
32
- with col2:
33
- st.image("images/POC_page/POC_SupplyRoster_image.png",
34
- caption="Supply Roster Tool Overview",
35
- use_container_width=True)
36
-
37
- # Global settings in sidebar
38
- with st.sidebar:
39
- st.markdown("## 🌐 Global Settings")
40
- st.markdown("The setting will be shared across all pages")
41
-
42
- # Data path setting
43
- new_data_path = st.text_input(
44
- "πŸ“ Data Path",
45
- value=st.session_state.data_path,
46
- help="The data path will be shared across all pages"
47
- )
48
-
49
- if new_data_path != st.session_state.data_path:
50
- st.session_state.data_path = new_data_path
51
- st.success("βœ… Data path updated!")
52
-
53
- st.markdown(f"**Current data path:** `{st.session_state.data_path}`")
54
-
55
- # Quick navigation
56
- st.markdown("## 🧭 Quick Navigation")
57
- if st.button("🎯 Go to Optimization", use_container_width=True):
58
- st.switch_page("pages/optimize_viz.py")
59
-
60
- if st.button("πŸ“Š Go to Dataset Overview", use_container_width=True):
61
- st.switch_page("pages/metadata.py")
62
-
63
- # Main content area
64
- st.markdown("---")
65
-
66
-
67
- # Footer
68
- st.markdown("---")
69
- st.markdown("""
70
- <div style='text-align: center; color: gray;'>
71
- <small>Supply Roster Optimization Tool | Built with Streamlit</small>
72
- </div>
73
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/visualization/pages/1_optimize_viz.py DELETED
@@ -1,424 +0,0 @@
1
- import sys
2
- import os
3
- import pandas as pd
4
- import streamlit as st
5
- import plotly.express as px
6
- from datetime import datetime
7
-
8
- # Add parent directory to path to import LaborOptimizer
9
- sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
10
- from optimization.labor_optimizer import LaborOptimizer
11
-
12
-
13
-
14
-
15
- def get_available_dates(data_path):
16
- """Load the orders data and extract unique dates"""
17
- try:
18
- orders_file = os.path.join(data_path, "orders.csv")
19
- if os.path.exists(orders_file):
20
- orders_df = pd.read_csv(orders_file)
21
- if "due_date" in orders_df.columns:
22
- # Convert to datetime and extract unique dates
23
- dates = pd.to_datetime(orders_df["due_date"]).dt.date.unique()
24
- # Sort dates in descending order (most recent first)
25
- dates = sorted(dates, reverse=True)
26
- return dates
27
- except Exception as e:
28
- st.error(f"Error loading dates: {str(e)}")
29
- return []
30
-
31
-
32
- def get_metadata_stats(optimizer, target_date=None):
33
- """
34
- Aggregate metadata statistics about employee costs and availability
35
-
36
- Args:
37
- optimizer: LaborOptimizer instance
38
- target_date: Target date for availability analysis
39
-
40
- Returns:
41
- dict: Dictionary containing various statistics
42
- """
43
- try:
44
- # Employee type costs
45
- employee_types_df = optimizer.employee_types_df
46
- costs_data = []
47
- for _, row in employee_types_df.iterrows():
48
- costs_data.append({
49
- 'Employee Type': row['type_name'].title(),
50
- 'Usual Cost ($/hr)': f"${row['usual_cost']:.2f}",
51
- 'Overtime Cost ($/hr)': f"${row['overtime_cost']:.2f}",
52
- 'Evening Shift Cost ($/hr)': f"${row['evening_shift_cost']:.2f}",
53
- 'Max Hours': row['max_hours'],
54
- 'Unit Manpower/Hr': row['unit_productivity_per_hour']
55
- })
56
-
57
- # Shift hours information
58
- shift_hours = optimizer._get_shift_hours()
59
- shift_data = []
60
- for shift_type, hours in shift_hours.items():
61
- shift_data.append({
62
- 'Shift Type': shift_type.replace('_', ' ').title(),
63
- 'Duration (hours)': f"{hours:.1f}"
64
- })
65
-
66
- # Employee availability for target date
67
- availability_data = []
68
- if target_date:
69
- target_date_str = pd.to_datetime(target_date).strftime("%Y-%m-%d")
70
- else:
71
- # Use most recent date if no target date specified, but show warning
72
- target_date_str = pd.to_datetime(optimizer.orders_df["due_date"]).max().strftime("%Y-%m-%d")
73
- st.warning("⚠️ No target date specified. Using the most recent order date for analysis. Please select a specific target date for accurate availability data.")
74
-
75
- availability_target_date = optimizer.employee_availability_df[
76
- optimizer.employee_availability_df["date"] == target_date_str
77
- ]
78
-
79
- employee_availability = optimizer.employees_df.merge(
80
- availability_target_date, left_on="id", right_on="employee_id", how="left"
81
- )
82
-
83
- for emp_type in optimizer.employee_types_df["type_name"]:
84
- emp_type_data = employee_availability[
85
- employee_availability["type_name"] == emp_type
86
- ]
87
-
88
- if not emp_type_data.empty:
89
- first_shift_available = emp_type_data["first_shift_available"].sum()
90
- second_shift_available = emp_type_data["second_shift_available"].sum()
91
- overtime_available = emp_type_data["overtime_available"].sum()
92
- total_employees = len(emp_type_data)
93
- else:
94
- first_shift_available = second_shift_available = overtime_available = total_employees = 0
95
-
96
- availability_data.append({
97
- 'Employee Type': emp_type.title(),
98
- 'Total Employees': total_employees,
99
- 'Usual Time Available': first_shift_available,
100
- 'Evening Shift Available': second_shift_available,
101
- 'Overtime Available': overtime_available
102
- })
103
-
104
- # Overall statistics
105
- total_employees = len(optimizer.employees_df)
106
- total_employee_types = len(optimizer.employee_types_df)
107
- total_orders = len(optimizer.orders_df)
108
-
109
- return {
110
- 'costs_data': costs_data,
111
- 'shift_data': shift_data,
112
- 'availability_data': availability_data,
113
- 'overall_stats': {
114
- 'Total Employees': total_employees,
115
- 'Employee Types': total_employee_types,
116
- 'Total Orders': total_orders,
117
- 'Analysis Date': target_date_str,
118
- 'is_default_date': not bool(target_date)
119
- }
120
- }
121
-
122
- except Exception as e:
123
- st.error(f"Error generating metadata: {str(e)}")
124
- return None
125
-
126
-
127
- def display_metadata_section(metadata):
128
- """Display metadata in organized sections"""
129
- if not metadata:
130
- return
131
-
132
- # Make the entire Dataset Overview section collapsible
133
- with st.expander("πŸ“Š Dataset Overview", expanded=False):
134
- # Overall statistics
135
- st.write("Information on the date chosen - not an optimization report") # df, err, func, keras!
136
- col1, col2, col3, col4 = st.columns(4)
137
- with col1:
138
- st.metric("Total Employees Available", metadata['overall_stats']['Total Employees'])
139
- with col2:
140
- st.metric("Employee Types Available", metadata['overall_stats']['Employee Types'])
141
- with col3:
142
- st.metric("Total Orders", metadata['overall_stats']['Total Orders'])
143
- with col4:
144
- analysis_date = metadata['overall_stats']['Analysis Date']
145
- if metadata['overall_stats'].get('is_default_date', False):
146
- st.metric("Analysis Date", f"{analysis_date} ⚠️", help="Using most recent order date - select specific date for accurate analysis")
147
- else:
148
- st.metric("Analysis Date", analysis_date)
149
-
150
- # Create tabs for different metadata sections
151
- tab1, tab2, tab3 = st.tabs(["πŸ’° Employee Costs", "πŸ• Shift Information", "πŸ‘₯ Availability"])
152
-
153
- with tab1:
154
- st.subheader("Employee Type Costs")
155
- costs_df = pd.DataFrame(metadata['costs_data'])
156
- st.dataframe(costs_df, use_container_width=True)
157
-
158
- # Cost comparison chart
159
- costs_for_chart = []
160
- for item in metadata['costs_data']:
161
- emp_type = item['Employee Type']
162
- costs_for_chart.extend([
163
- {'Employee Type': emp_type, 'Cost Type': 'Usual', 'Cost': float(item['Usual Cost ($/hr)'].replace('$', ''))},
164
- {'Employee Type': emp_type, 'Cost Type': 'Overtime', 'Cost': float(item['Overtime Cost ($/hr)'].replace('$', ''))},
165
- {'Employee Type': emp_type, 'Cost Type': 'Evening', 'Cost': float(item['Evening Shift Cost ($/hr)'].replace('$', ''))}
166
- ])
167
-
168
- chart_df = pd.DataFrame(costs_for_chart)
169
- fig = px.bar(chart_df, x='Employee Type', y='Cost', color='Cost Type',
170
- title='Hourly Costs by Employee Type and Shift',
171
- barmode='group')
172
- st.plotly_chart(fig, use_container_width=True)
173
-
174
- with tab2:
175
- st.subheader("Shift Duration Information")
176
- shift_df = pd.DataFrame(metadata['shift_data'])
177
- st.dataframe(shift_df, use_container_width=True)
178
-
179
- # Shift duration chart
180
- fig2 = px.bar(shift_df, x='Shift Type', y='Duration (hours)',
181
- title='Shift Duration by Type')
182
- st.plotly_chart(fig2, use_container_width=True)
183
-
184
- with tab3:
185
- st.subheader("Employee Availability")
186
- availability_df = pd.DataFrame(metadata['availability_data'])
187
- st.dataframe(availability_df, use_container_width=True)
188
-
189
- # # Availability chart
190
- # availability_chart_data = []
191
- # for item in metadata['availability_data']:
192
- # emp_type = item['Employee Type']
193
- # availability_chart_data.extend([
194
- # {'Employee Type': emp_type, 'Shift Type': 'Usual Time', 'Available': item['Usual Time Available']},
195
- # {'Employee Type': emp_type, 'Shift Type': 'Evening Shift', 'Available': item['Evening Shift Available']},
196
- # {'Employee Type': emp_type, 'Shift Type': 'Overtime', 'Available': item['Overtime Available']}
197
- # ])
198
-
199
- # chart_df2 = pd.DataFrame(availability_chart_data)
200
- # fig3 = px.bar(chart_df2, x='Employee Type', y='Available', color='Shift Type',
201
- # title='Available Workers by Employee Type and Shift',
202
- # barmode='group')
203
- # st.plotly_chart(fig3, use_container_width=True)
204
-
205
-
206
- def main():
207
- st.set_page_config(page_title="Labor Optimization Tool", layout="wide")
208
- st.title("Labor Optimization Visualization Tool")
209
-
210
- # Initialize session state
211
- if 'data_path' not in st.session_state:
212
- st.session_state.data_path = "data/my_roster_data"
213
-
214
- # Sidebar for inputs
215
- with st.sidebar:
216
- st.header("Optimization Parameters")
217
- data_path = st.text_input("Data Path", value=st.session_state.data_path)
218
- # Update session state when user changes data_path
219
- st.session_state.data_path = data_path
220
-
221
- # Load available dates from the dataset
222
- available_dates = get_available_dates(data_path)
223
-
224
-
225
- if available_dates:
226
- date_options = [""] + [str(date) for date in available_dates]
227
- target_date = st.selectbox(
228
- "Target Date (select empty for latest date)",
229
- options=date_options,
230
- index=0,
231
- )
232
- st.session_state.target_date = target_date
233
- else:
234
- target_date = st.text_input(
235
- "Target Date (YYYY-MM-DD, leave empty for latest)"
236
- )
237
- if available_dates == []:
238
- st.warning("No order dates found in dataset. Check the data path.")
239
-
240
- st.header("Advanced Options")
241
- st.caption("Set to 0 to use all available workers")
242
- max_workers_permanent = st.number_input(
243
- "Max Permanent Workers", min_value=0, value=0
244
- )
245
- max_workers_contract = st.number_input(
246
- "Max Contract Workers", min_value=0, value=0
247
- )
248
- max_workers_temporary = st.number_input(
249
- "Max Temporary Workers", min_value=0, value=0
250
- )
251
-
252
- # Add button to show metadata
253
- show_metadata = st.checkbox("Show Dataset Overview", value=True)
254
- optimize_btn = st.button("Run Optimization")
255
-
256
- # Main area for optimization results
257
- if optimize_btn:
258
- try:
259
- with st.spinner("Running optimization..."):
260
- optimizer = LaborOptimizer(data_path)
261
-
262
- # Prepare override dict if values are provided
263
- max_workers_override = {}
264
- if max_workers_permanent > 0:
265
- max_workers_override["permanent"] = max_workers_permanent
266
- if max_workers_contract > 0:
267
- max_workers_override["contract"] = max_workers_contract
268
- if max_workers_temporary > 0:
269
- max_workers_override["temporary"] = max_workers_temporary
270
-
271
- # If no overrides provided, pass None instead of empty dict
272
- if not max_workers_override:
273
- max_workers_override = None
274
-
275
- results = optimizer.optimize(target_date, max_workers_override)
276
-
277
- if isinstance(results, str):
278
- st.error(results)
279
- else:
280
- # Wrap optimization results in an expander
281
- with st.expander("🎯 Optimization Results", expanded=True):
282
- # Split the page into sections
283
- summary_col, allocation_col = st.columns([1, 1])
284
-
285
- with summary_col:
286
- st.subheader("Optimization Summary")
287
- st.write(f"**Target Date:** {results['target_date']}")
288
- st.write(
289
- f"**Total Labor Hours:** {results['total_labor_hours_needed']:.2f}"
290
- )
291
- st.write(f"**Total Cost:** ${results['total_cost']:.2f}")
292
-
293
- with allocation_col:
294
- st.subheader("Employee Allocation")
295
- allocation_data = results["allocation"]
296
-
297
- # Create a DataFrame for easier visualization
298
- allocation_df = pd.DataFrame.from_dict(
299
- {
300
- emp_type: {
301
- shift: int(val) for shift, val in shifts.items()
302
- }
303
- for emp_type, shifts in allocation_data.items()
304
- },
305
- orient="index",
306
- )
307
- allocation_df.index.name = "Employee Type"
308
- allocation_df.columns = [
309
- col.replace("_", " ").title()
310
- for col in allocation_df.columns
311
- ]
312
-
313
- st.dataframe(allocation_df)
314
-
315
- # Cost visualization
316
- st.subheader("Cost Visualization")
317
-
318
- # Prepare data for visualization
319
- cost_data = []
320
- for emp_type, shifts in allocation_data.items():
321
- shift_hours = results["shift_hours"]
322
- costs = optimizer.employee_types_df.set_index("type_name")
323
-
324
- shift_cost_mapping = {
325
- "usual_time": "usual_cost",
326
- "overtime": "overtime_cost",
327
- "evening_shift": "evening_shift_cost",
328
- }
329
-
330
- for shift in shifts:
331
- cost = (
332
- shifts[shift]
333
- * shift_hours[shift]
334
- * costs.loc[emp_type, shift_cost_mapping[shift]]
335
- )
336
- if cost > 0: # Only add non-zero costs
337
- cost_data.append(
338
- {
339
- "Employee Type": emp_type.title(),
340
- "Shift": shift.replace("_", " ").title(),
341
- "Cost": cost,
342
- "Workers": int(shifts[shift]),
343
- }
344
- )
345
-
346
- cost_df = pd.DataFrame(cost_data)
347
-
348
- col1, col2 = st.columns([3, 2])
349
-
350
- with col1:
351
- # Bar chart for costs
352
- if not cost_df.empty:
353
- fig = px.bar(
354
- cost_df,
355
- x="Shift",
356
- y="Cost",
357
- color="Employee Type",
358
- title="Labor Cost by Employee Type and Shift",
359
- labels={"Cost": "Cost ($)"},
360
- )
361
- st.plotly_chart(fig, use_container_width=True)
362
-
363
- with col2:
364
- # Pie chart for total cost by employee type
365
- if not cost_df.empty:
366
- total_by_type = (
367
- cost_df.groupby("Employee Type")["Cost"]
368
- .sum()
369
- .reset_index()
370
- )
371
- fig2 = px.pie(
372
- total_by_type,
373
- values="Cost",
374
- names="Employee Type",
375
- title="Total Cost by Employee Type",
376
- )
377
- st.plotly_chart(fig2, use_container_width=True)
378
-
379
- # Worker allocation visualization
380
- st.subheader("Worker Allocation")
381
- worker_data = []
382
- for emp_type, shifts in allocation_data.items():
383
- for shift, count in shifts.items():
384
- if count > 0: # Only add non-zero allocations
385
- worker_data.append(
386
- {
387
- "Employee Type": emp_type.title(),
388
- "Shift": shift.replace("_", " ").title(),
389
- "Workers": int(count),
390
- }
391
- )
392
-
393
- worker_df = pd.DataFrame(worker_data)
394
-
395
- if not worker_df.empty:
396
- fig3 = px.bar(
397
- worker_df,
398
- x="Shift",
399
- y="Workers",
400
- color="Employee Type",
401
- title="Worker Allocation by Shift and Employee Type",
402
- barmode="group",
403
- )
404
- st.plotly_chart(fig3, use_container_width=True)
405
-
406
- except Exception as e:
407
- st.error(f"Error: {str(e)}")
408
- st.exception(e)
409
-
410
- # Display metadata section if requested - moved below optimization results
411
- if show_metadata:
412
- try:
413
- optimizer = LaborOptimizer(data_path)
414
-
415
- # Show warning if no target date is selected
416
- if not target_date:
417
- st.info("πŸ’‘ **Tip**: Select a specific target date from the sidebar to see accurate availability data for that date. Currently showing data for the most recent order date.")
418
-
419
- except Exception as e:
420
- st.error(f"Error loading metadata: {str(e)}")
421
-
422
-
423
- if __name__ == "__main__":
424
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/visualization/pages/2_metadata.py DELETED
@@ -1,300 +0,0 @@
1
- import sys
2
- import os
3
- import pandas as pd
4
- import streamlit as st
5
- import plotly.express as px
6
- from datetime import datetime
7
-
8
- # Add parent directory to path to import LaborOptimizer
9
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10
- from optimization.labor_optimizer import LaborOptimizer
11
-
12
-
13
- def get_available_dates(data_path):
14
- """Load the orders data and extract unique dates"""
15
- try:
16
- orders_file = os.path.join(data_path, "orders.csv")
17
- if os.path.exists(orders_file):
18
- orders_df = pd.read_csv(orders_file)
19
- if "due_date" in orders_df.columns:
20
- # Convert to datetime and extract unique dates
21
- dates = pd.to_datetime(orders_df["due_date"]).dt.date.unique()
22
- # Sort dates in descending order (most recent first)
23
- dates = sorted(dates, reverse=True)
24
- return dates
25
- except Exception as e:
26
- st.error(f"Error loading dates: {str(e)}")
27
- return []
28
-
29
-
30
- def get_metadata_stats(optimizer, target_date=None):
31
- """
32
- Aggregate metadata statistics about employee costs and availability
33
-
34
- Args:
35
- optimizer: LaborOptimizer instance
36
- target_date: Target date for availability analysis
37
-
38
- Returns:
39
- dict: Dictionary containing various statistics
40
- """
41
- try:
42
- # Employee type costs
43
- employee_types_df = optimizer.employee_types_df
44
- costs_data = []
45
- for _, row in employee_types_df.iterrows():
46
- costs_data.append({
47
- 'Employee Type': row['type_name'].title(),
48
- 'Usual Cost ($/hr)': f"${row['usual_cost']:.2f}",
49
- 'Overtime Cost ($/hr)': f"${row['overtime_cost']:.2f}",
50
- 'Evening Shift Cost ($/hr)': f"${row['evening_shift_cost']:.2f}",
51
- 'Max Hours': row['max_hours'],
52
- 'Unit Manpower/Hr': row['unit_productivity_per_hour']
53
- })
54
-
55
- # Shift hours information
56
- shift_hours = optimizer._get_shift_hours()
57
- shift_data = []
58
- for shift_type, hours in shift_hours.items():
59
- shift_data.append({
60
- 'Shift Type': shift_type.replace('_', ' ').title(),
61
- 'Duration (hours)': f"{hours:.1f}"
62
- })
63
-
64
- # Employee availability for target date
65
- availability_data = []
66
- if target_date:
67
- target_date_str = pd.to_datetime(target_date).strftime("%Y-%m-%d")
68
- else:
69
- # Use most recent date if no target date specified, but show warning
70
- target_date_str = pd.to_datetime(optimizer.orders_df["due_date"]).max().strftime("%Y-%m-%d")
71
- st.warning("⚠️ No target date specified. Using the most recent order date for analysis. Please select a specific target date for accurate availability data.")
72
-
73
- availability_target_date = optimizer.employee_availability_df[
74
- optimizer.employee_availability_df["date"] == target_date_str
75
- ]
76
-
77
- employee_availability = optimizer.employees_df.merge(
78
- availability_target_date, left_on="id", right_on="employee_id", how="left"
79
- )
80
-
81
- for emp_type in optimizer.employee_types_df["type_name"]:
82
- emp_type_data = employee_availability[
83
- employee_availability["type_name"] == emp_type
84
- ]
85
-
86
- if not emp_type_data.empty:
87
- first_shift_available = emp_type_data["first_shift_available"].sum()
88
- second_shift_available = emp_type_data["second_shift_available"].sum()
89
- overtime_available = emp_type_data["overtime_available"].sum()
90
- total_employees = len(emp_type_data)
91
- else:
92
- first_shift_available = second_shift_available = overtime_available = total_employees = 0
93
-
94
- availability_data.append({
95
- 'Employee Type': emp_type.title(),
96
- 'Total Employees': total_employees,
97
- 'Usual Time Available': first_shift_available,
98
- 'Evening Shift Available': second_shift_available,
99
- 'Overtime Available': overtime_available
100
- })
101
-
102
- # Overall statistics
103
- total_employees = len(optimizer.employees_df)
104
- total_employee_types = len(optimizer.employee_types_df)
105
- total_orders = len(optimizer.orders_df)
106
-
107
- return {
108
- 'costs_data': costs_data,
109
- 'shift_data': shift_data,
110
- 'availability_data': availability_data,
111
- 'overall_stats': {
112
- 'Total Employees': total_employees,
113
- 'Employee Types': total_employee_types,
114
- 'Total Orders': total_orders,
115
- 'Analysis Date': target_date_str,
116
- 'is_default_date': not bool(target_date)
117
- }
118
- }
119
-
120
- except Exception as e:
121
- st.error(f"Error generating metadata: {str(e)}")
122
- return None
123
-
124
-
125
- def display_metadata_section(metadata):
126
- """Display metadata in organized sections"""
127
- if not metadata:
128
- return
129
-
130
- # Make the entire Dataset Overview section collapsible
131
- # with st.expander("πŸ“Š Dataset Overview", expanded=False):
132
-
133
- with st.expander("πŸ“Š Dataset Overview", expanded=False):
134
- st.write(f"Data path: {st.session_state.data_path}")
135
- # Overall statistics
136
- st.write("Information on the date chosen - not an optimization report") # df, err, func, keras!
137
- col1, col2, col3, col4 = st.columns(4)
138
- with col1:
139
- st.metric("Total Employees Available", metadata['overall_stats']['Total Employees'])
140
- with col2:
141
- st.metric("Employee Types Available", metadata['overall_stats']['Employee Types'])
142
- with col3:
143
- st.metric("Total Orders", metadata['overall_stats']['Total Orders'])
144
- with col4:
145
- analysis_date = metadata['overall_stats']['Analysis Date']
146
- if metadata['overall_stats'].get('is_default_date', False):
147
- st.metric("Analysis Date", f"{analysis_date} ⚠️", help="Using most recent order date - select specific date for accurate analysis")
148
- else:
149
- st.metric("Analysis Date", analysis_date)
150
-
151
- # Create tabs for different metadata sections
152
- tab1, tab2, tab3 = st.tabs(["πŸ’° Employee Costs", "πŸ• Shift Information", "πŸ‘₯ Availability"])
153
-
154
- with tab1:
155
- st.subheader("Employee Type Costs")
156
- costs_df = pd.DataFrame(metadata['costs_data'])
157
- st.dataframe(costs_df, use_container_width=True)
158
-
159
- # Cost comparison chart
160
- costs_for_chart = []
161
- for item in metadata['costs_data']:
162
- emp_type = item['Employee Type']
163
- costs_for_chart.extend([
164
- {'Employee Type': emp_type, 'Cost Type': 'Usual', 'Cost': float(item['Usual Cost ($/hr)'].replace('$', ''))},
165
- {'Employee Type': emp_type, 'Cost Type': 'Overtime', 'Cost': float(item['Overtime Cost ($/hr)'].replace('$', ''))},
166
- {'Employee Type': emp_type, 'Cost Type': 'Evening', 'Cost': float(item['Evening Shift Cost ($/hr)'].replace('$', ''))}
167
- ])
168
-
169
- chart_df = pd.DataFrame(costs_for_chart)
170
- fig = px.bar(chart_df, x='Employee Type', y='Cost', color='Cost Type',
171
- title='Hourly Costs by Employee Type and Shift',
172
- barmode='group')
173
- st.plotly_chart(fig, use_container_width=True)
174
-
175
- with tab2:
176
- st.subheader("Shift Duration Information")
177
- shift_df = pd.DataFrame(metadata['shift_data'])
178
- st.dataframe(shift_df, use_container_width=True)
179
-
180
- # Shift duration chart
181
- fig2 = px.bar(shift_df, x='Shift Type', y='Duration (hours)',
182
- title='Shift Duration by Type')
183
- st.plotly_chart(fig2, use_container_width=True)
184
-
185
- with tab3:
186
- st.subheader("Employee Availability")
187
- availability_df = pd.DataFrame(metadata['availability_data'])
188
- st.dataframe(availability_df, use_container_width=True)
189
-
190
- # # Availability chart
191
- # availability_chart_data = []
192
- # for item in metadata['availability_data']:
193
- # emp_type = item['Employee Type']
194
- # availability_chart_data.extend([
195
- # {'Employee Type': emp_type, 'Shift Type': 'Usual Time', 'Available': item['Usual Time Available']},
196
- # {'Employee Type': emp_type, 'Shift Type': 'Evening Shift', 'Available': item['Evening Shift Available']},
197
- # {'Employee Type': emp_type, 'Shift Type': 'Overtime', 'Available': item['Overtime Available']}
198
- # ])
199
-
200
- # chart_df2 = pd.DataFrame(availability_chart_data)
201
- # fig3 = px.bar(chart_df2, x='Employee Type', y='Available', color='Shift Type',
202
- # title='Available Workers by Employee Type and Shift',
203
- # barmode='group')
204
- # st.plotly_chart(fig3, use_container_width=True)
205
- def display_demand(optimizer):
206
- with st.expander("πŸ“Š Demand", expanded=False):
207
- demand_df = optimizer.orders_df
208
- st.header("Demand")
209
- daily_demand = demand_df.groupby('date_of_order').sum()['order_amount'].reset_index()
210
- st.plotly_chart(px.bar(daily_demand, x='date_of_order', y='order_amount', title='Demand by Date'), use_container_width=True)
211
- st.markdown("### Demand for the selected date")
212
- st.dataframe(demand_df[demand_df['date_of_order']==st.session_state.target_date], use_container_width=True)
213
-
214
- def display_employee_availability(optimizer):
215
- with st.expander("πŸ‘₯ Employee Availability", expanded=False):
216
- st.header("Employee Availability")
217
- employee_availability_df = optimizer.employee_availability_df
218
- employee_availability_df['date'] = pd.to_datetime(employee_availability_df['date'])
219
- employee_availability_target_date = employee_availability_df[employee_availability_df['date']==st.session_state.target_date]
220
- employee_availability_target_date = pd.merge(employee_availability_target_date, optimizer.employees_df, left_on='employee_id', right_on='id', how='left')
221
- st.dataframe(employee_availability_target_date[['name', 'employee_id', 'type_name', 'first_shift_available', 'second_shift_available', 'overtime_available']], use_container_width=True)
222
- # Group by type_name and sum the availability columns
223
- available_employee_grouped = employee_availability_target_date.groupby('type_name')[
224
- ['first_shift_available', 'second_shift_available', 'overtime_available']
225
- ].sum().reset_index()
226
-
227
- st.markdown("### Employee Availability for the selected date")
228
- # Create non-stacked (grouped) bar chart using plotly
229
- fig = px.bar(
230
- available_employee_grouped.melt(id_vars=['type_name'], var_name='shift_type', value_name='count'),
231
- x='type_name',
232
- y='count',
233
- color='shift_type',
234
- barmode='group', # This makes it non-stacked
235
- title='Available Employee Count by Type and Shift',
236
- labels={'type_name': 'Employee Type', 'count': 'Available Count', 'shift_type': 'Shift Type'}
237
- )
238
- st.plotly_chart(fig, use_container_width=True)
239
-
240
- # st.dataframe(employee_availability_target_date, use_container_width=True)
241
- # st.plotly_chart(px.bar(employee_availability_target_date, x='employee_id', y='availability', title='Employee Availability by Date'), use_container_width=True)
242
-
243
- # st.dataframe(employee_availability_df[employee_availability_df['date']==st.session_state.target_date], use_container_width=True)
244
-
245
-
246
-
247
- def main():
248
- """Main function for metadata page"""
249
- st.set_page_config(page_title="Dataset Metadata", layout="wide")
250
- st.title("πŸ“Š Dataset Metadata Overview")
251
-
252
- # Get data_path from session state if available, otherwise create input
253
- if 'data_path' in st.session_state:
254
- # Using shared data_path from optimize_viz.py
255
- data_path = st.session_state.data_path
256
-
257
- st.sidebar.info(f"πŸ“ Using shared data path: `{data_path}`")
258
- else:
259
- st.error("No data path found. Please select a data path in the sidebar.")
260
-
261
-
262
- if 'target_date' in st.session_state:
263
- target_date = st.session_state.target_date
264
- st.sidebar.info(f"πŸ“… Using shared target date: `{target_date}`")
265
- else:
266
- st.error("No target date found. Please select a target date in the sidebar.")
267
-
268
- #If the date selection needs to be individualized per page, uncomment the following code
269
- # with st.sidebar:
270
- # # Date selection
271
- # available_dates = get_available_dates(data_path)
272
- # if available_dates:
273
- # date_options = [""] + [str(date) for date in available_dates]
274
- # target_date = st.selectbox(
275
- # "Target Date (select empty for latest date)",
276
- # options=date_options,
277
- # index=0,
278
- # )
279
- # else:
280
- # target_date = st.text_input(
281
- # "Target Date (YYYY-MM-DD, leave empty for latest)"
282
- # )
283
-
284
- try:
285
- optimizer = LaborOptimizer(data_path)
286
-
287
- # Show warning if no target date is selected
288
- if not target_date:
289
- st.info("πŸ’‘ **Tip**: Select a specific target date from the sidebar to see accurate availability data for that date. Currently showing data for the most recent order date.")
290
-
291
- metadata = get_metadata_stats(optimizer, target_date if target_date else None)
292
- display_metadata_section(metadata)
293
- display_demand(optimizer)
294
- display_employee_availability(optimizer)
295
- except Exception as e:
296
- st.error(f"Error loading metadata: {str(e)}")
297
-
298
-
299
- if __name__ == "__main__":
300
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
streamlit_page/__init__.py DELETED
@@ -1 +0,0 @@
1
- # Streamlit page package
 
 
streamlit_page/page1.py DELETED
@@ -1,62 +0,0 @@
1
- import streamlit as st
2
- from datetime import datetime, timedelta
3
- import sys
4
- import os
5
-
6
- # Add the parent directory to the path to import src modules
7
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
- import src.etl.transform as transform
9
-
10
- # Page title
11
- st.title("Date Selection")
12
-
13
- # Date selection section
14
- st.header("Select Date Range")
15
-
16
- # Get available date ranges from COOIS_Released_Prod_Orders.csv
17
- try:
18
- date_ranges = transform.get_date_ranges()
19
- all_dates, start_dates, end_dates = transform.get_available_dates()
20
-
21
- if date_ranges:
22
- # Create dropdown for date range selection
23
- date_range_options = [f"{start} to {end}" for start, end in date_ranges]
24
- selected_range_str = st.selectbox(
25
- "Select a date range from released orders:",
26
- options=date_range_options,
27
- help="Date ranges available in COOIS_Released_Prod_Orders.csv"
28
- )
29
-
30
- # Extract selected dates
31
- selected_index = date_range_options.index(selected_range_str)
32
- start_date, end_date = date_ranges[selected_index]
33
-
34
- # Display the selected date range
35
- st.write(f"**Start Date:** {start_date.strftime('%Y-%m-%d')}")
36
- st.write(f"**End Date:** {end_date.strftime('%Y-%m-%d')}")
37
-
38
- # Show the date range duration
39
- duration = (end_date - start_date).days
40
- st.info(f"Selected date range: {duration} days")
41
-
42
-
43
-
44
- except Exception as e:
45
- st.error(f"Error loading date data: {str(e)}")
46
- # Fallback to default dates
47
- start_date = datetime(2025, 3, 24).date()
48
- end_date = datetime(2025, 3, 28).date()
49
-
50
- # Product selection section
51
-
52
-
53
- # You can add more functionality here based on the selected dates
54
- if st.button("Confirm Selection"):
55
- if 'selected_product' in locals():
56
- st.success(f"Date range: {start_date} to {end_date}")
57
- st.success(f"Selected product: {selected_product}")
58
- if 'selected_products' in locals() and selected_products:
59
- st.success(f"Multiple products selected: {', '.join(selected_products)}")
60
- else:
61
- st.success(f"Date range confirmed: {start_date} to {end_date}")
62
- # Add your logic here for what happens when selection is confirmed