| import streamlit as st |
| import streamlit.components.v1 as components |
| import pandas as pd |
| import matplotlib.pyplot as plt |
| import plotly.figure_factory as ff |
| import os |
| from datetime import datetime, timedelta |
| import json |
| import requests |
| import base64 |
| import logging |
| from model import predict_delay, get_weather_condition |
| from utils import validate_inputs, generate_heatmap |
| from reportlab.lib.pagesizes import letter |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image |
| from reportlab.lib.styles import getSampleStyleSheet |
| from reportlab.lib.units import inch |
| from io import BytesIO |
| from simple_salesforce import Salesforce |
|
|
| |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
| logger = logging.getLogger(__name__) |
|
|
| |
| st.set_page_config(page_title="Delay 🚀", layout="wide") |
|
|
| |
| try: |
| sf_instance_url = os.environ.get("SF_INSTANCE_URL") |
| if not sf_instance_url: |
| raise ValueError("SF_INSTANCE_URL environment variable not set") |
| if "lightning.force.com" in sf_instance_url: |
| logger.warning("SF_INSTANCE_URL contains lightning.force.com; consider using my.salesforce.com for reliable PDF downloads") |
| sf = Salesforce( |
| username=os.environ.get("SF_USERNAME"), |
| password=os.environ.get("SF_PASSWORD"), |
| security_token=os.environ.get("SF_SECURITY_TOKEN"), |
| instance_url=sf_instance_url |
| ) |
| except Exception as e: |
| st.error(f"Failed to connect to Salesforce: {str(e)}") |
| logger.error(f"Salesforce connection failed: {str(e)}") |
| sf = None |
|
|
| |
| WEATHER_API_KEY = os.environ.get("WEATHER_API_KEY") |
| WEATHER_API_URL = "http://api.openweathermap.org/data/2.5/forecast" |
|
|
| |
| st.title("Project Delay Predictor 🚀") |
|
|
| |
| task_options = { |
| "Planning": ["Define Scope", "Resource Allocation", "Permit Acquisition"], |
| "Design": ["Architectural Drafting", "Engineering Analysis", "Design Review"], |
| "Construction": ["Foundation Work", "Structural Build", "Utility Installation"] |
| } |
|
|
| |
| if 'phase' not in st.session_state: |
| st.session_state.phase = "" |
| if 'task' not in st.session_state: |
| st.session_state.task = "" |
| if 'weather_data' not in st.session_state: |
| st.session_state.weather_data = None |
|
|
| |
| def fetch_weather_data(project_location, date): |
| if not WEATHER_API_KEY: |
| logger.error("WEATHER_API_KEY not set") |
| return None, {"error": "Weather API key not set. Please provide a valid API key."} |
| try: |
| params = { |
| "q": project_location, |
| "appid": WEATHER_API_KEY, |
| "units": "metric" |
| } |
| response = requests.get(WEATHER_API_URL, params=params) |
| response.raise_for_status() |
| data = response.json() |
| |
| |
| target_date = datetime.strptime(date, "%Y-%m-%d") |
| closest_forecast = None |
| min_time_diff = float('inf') |
| |
| for forecast in data['list']: |
| forecast_time = datetime.fromtimestamp(forecast['dt']) |
| time_diff = abs((forecast_time - target_date).total_seconds()) |
| if time_diff < min_time_diff: |
| min_time_diff = time_diff |
| closest_forecast = forecast |
| |
| if not closest_forecast: |
| return None, {"error": "No forecast available for the specified date."} |
| |
| |
| weather_main = forecast['weather'][0]['main'].lower() |
| impact_score = 50 |
| if 'clear' in weather_main: |
| impact_score = 10 |
| elif 'clouds' in weather_main: |
| impact_score = 30 if forecast['clouds']['all'] < 50 else 50 |
| elif 'rain' in weather_main: |
| impact_score = 70 if forecast['rain'].get('3h', 0) < 2.5 else 85 |
| elif 'storm' in weather_main or 'thunderstorm' in weather_main: |
| impact_score = 90 |
| |
| weather_condition = get_weather_condition(impact_score) |
| return { |
| "weather_impact_score": impact_score, |
| "weather_condition": weather_condition, |
| "temperature": forecast['main']['temp'], |
| "humidity": forecast['main']['humidity'] |
| }, None |
| except Exception as e: |
| logger.error(f"Failed to fetch weather data: {str(e)}") |
| return None, {"error": f"Failed to fetch weather data for {project_location}: {str(e)}"} |
|
|
| |
| def format_high_risk_phases(high_risk_phases): |
| formatted = [] |
| for phase in high_risk_phases: |
| flag = "🚩" if phase['risk'] > 75 else "" |
| alert = "[Alert]" if phase['risk'] > 75 else "" |
| formatted.append(f"{flag} {phase['phase']}: {phase['task']} (Risk: {phase['risk']:.1f}%) {alert}") |
| return formatted |
|
|
| |
| def generate_gantt_chart(input_data, prediction): |
| try: |
| phase = input_data["phase"] |
| task = input_data["task"] |
| expected_duration = input_data["task_expected_duration"] |
| actual_duration = input_data["task_actual_duration"] |
| forecast_date = datetime.strptime(input_data["weather_forecast_date"], "%Y-%m-%d") |
| delay_risk = prediction["delay_probability"] |
|
|
| |
| start_date = forecast_date - timedelta(days=max(expected_duration, actual_duration)) |
| expected_end = start_date + timedelta(days=expected_duration) |
| actual_end = start_date + timedelta(days=actual_duration) if actual_duration > 0 else expected_end |
|
|
| |
| df = [ |
| dict(Task=f"{phase}: {task} (Expected)", Start=start_date.strftime("%Y-%m-%d"), Finish=expected_end.strftime("%Y-%m-%d"), Resource="Expected", Risk=delay_risk), |
| dict(Task=f"{phase}: {task} (Actual)", Start=start_date.strftime("%Y-%m-%d"), Finish=actual_end.strftime("%Y-%m-%d"), Resource="Actual", Risk=delay_risk) |
| ] |
|
|
| |
| colors = { |
| "Expected": "rgb(0, 255, 0)" if delay_risk <= 50 else "rgb(255, 255, 0)" if delay_risk <= 75 else "rgb(255, 0, 0)", |
| "Actual": "rgb(0, 200, 0)" if delay_risk <= 50 else "rgb(200, 200, 0)" if delay_risk <= 75 else "rgb(200, 0, 0)" |
| } |
|
|
| |
| fig = ff.create_gantt( |
| df, |
| colors=colors, |
| index_col="Resource", |
| title=f"Gantt Chart for {phase}: {task}", |
| show_colorbar=True, |
| bar_width=0.4, |
| showgrid_x=True, |
| showgrid_y=True |
| ) |
| fig.update_layout( |
| xaxis_title="Timeline", |
| yaxis_title="Task", |
| height=300, |
| margin=dict(l=150) |
| ) |
| return fig |
| except Exception as e: |
| logger.error(f"Failed to generate Gantt chart: {str(e)}") |
| return None |
|
|
| |
| def generate_pdf(input_data, prediction, heatmap_fig, gantt_fig): |
| buffer = BytesIO() |
| doc = SimpleDocTemplate(buffer, pagesize=letter) |
| styles = getSampleStyleSheet() |
| story = [] |
|
|
| |
| story.append(Paragraph("Project Delay Prediction Report", styles['Title'])) |
| story.append(Spacer(1, 12)) |
|
|
| |
| story.append(Paragraph("Input Data", styles['Heading2'])) |
| input_fields = [ |
| f"Project Name: {input_data['project_name']}", |
| f"Phase: {input_data['phase']}", |
| f"Task: {input_data['task']}", |
| f"Current Progress: {input_data['current_progress']}%", |
| f"Task Expected Duration: {input_data['task_expected_duration']} days", |
| f"Task Actual Duration: {input_data['task_actual_duration']} days", |
| f"Workforce Gap: {input_data['workforce_gap']}%", |
| f"Workforce Skill Level: {input_data['workforce_skill_level']}", |
| f"Workforce Shift Hours: {input_data['workforce_shift_hours']}", |
| f"Weather Impact Score: {input_data['weather_impact_score']}", |
| f"Weather Condition: {input_data['weather_condition']}", |
| f"Weather Forecast Date: {input_data['weather_forecast_date']}", |
| f"Project Location: {input_data['project_location']}" |
| ] |
| for field in input_fields: |
| story.append(Paragraph(field, styles['Normal'])) |
| story.append(Spacer(1, 12)) |
|
|
| |
| story.append(Paragraph("Prediction Results", styles['Heading2'])) |
| high_risk_text = "<br/>".join(format_high_risk_phases(prediction['high_risk_phases'])) |
| |
| |
| two_week_alert = next((insight for insight in prediction['ai_insights'].split("; ") if "2-Week Risk Alert" in insight), None) |
| if two_week_alert: |
| story.append(Paragraph("2-Week Risk Alert", styles['Heading3'])) |
| story.append(Paragraph(two_week_alert, styles['Normal'])) |
| story.append(Spacer(1, 12)) |
| |
| prediction_fields = [ |
| f"Delay Probability: {prediction['delay_probability']:.2f}%", |
| f"High Risk Phases:<br/>{high_risk_text}", |
| f"AI Insights: {prediction['ai_insights']}", |
| f"Weather Condition: {prediction['weather_condition']}" |
| ] |
| for field in prediction_fields: |
| story.append(Paragraph(field, styles['Normal'])) |
| story.append(Spacer(1, 12)) |
|
|
| |
| story.append(Paragraph("Delay Risk Heatmap", styles['Heading2'])) |
| img_buffer = BytesIO() |
| heatmap_fig.savefig(img_buffer, format='png', bbox_inches='tight') |
| img_buffer.seek(0) |
| story.append(Image(img_buffer, width=6*inch, height=2*inch)) |
| story.append(Spacer(1, 12)) |
|
|
| |
| if gantt_fig: |
| story.append(Paragraph("Gantt Chart", styles['Heading2'])) |
| gantt_buffer = BytesIO() |
| try: |
| gantt_fig.write_image(gantt_buffer, format='PNG') |
| gantt_buffer.seek(0) |
| story.append(Image(gantt_buffer, width=6*inch, height=3*inch)) |
| except Exception as e: |
| logger.error(f"Failed to include Gantt chart in PDF: {str(e)}") |
| story.append(Paragraph("Gantt Chart unavailable due to rendering issues.", styles['Normal'])) |
| story.append(Spacer(1, 12)) |
|
|
| doc.build(story) |
| buffer.seek(0) |
| return buffer |
|
|
| |
| def save_to_salesforce(input_data, prediction, pdf_buffer): |
| if sf is None: |
| return "Salesforce connection not established." |
| try: |
| |
| status = "Flagged" if prediction["delay_probability"] > 75 else "Running" |
|
|
| |
| sf_data = { |
| "Project_Name__c": input_data["project_name"], |
| "Phase__c": input_data["phase"], |
| "Task__c": input_data["task"], |
| "Current_Progress__c": input_data["current_progress"], |
| "Task_Expected_Duration__c": input_data["task_expected_duration"], |
| "Task_Actual_Duration__c": input_data["task_actual_duration"], |
| "Workforce_Gap__c": input_data["workforce_gap"], |
| "Workforce_Skill_Level__c": input_data["workforce_skill_level"], |
| "Workforce_Shift_Hours__c": input_data["workforce_shift_hours"], |
| "Weather_Impact_Score__c": input_data["weather_impact_score"], |
| "Weather_Condition__c": input_data["weather_condition"], |
| "Weather_Forecast_Date__c": input_data["weather_forecast_date"], |
| "Project_Location__c": input_data["project_location"], |
| "Delay_Probability__c": prediction["delay_probability"], |
| "AI_Insights__c": prediction["ai_insights"], |
| "High_Risk_Phases__c": "; ".join(format_high_risk_phases(prediction["high_risk_phases"])), |
| "Status__c": status |
| } |
| logger.info(f"Attempting to save to Salesforce Delay_Predictor__c: {sf_data}") |
| |
| |
| result = sf.Delay_Predictor__c.create(sf_data) |
| if not result["success"]: |
| logger.error(f"Salesforce save failed: {result['errors']}") |
| return f"Salesforce save failed: {result['errors']}" |
|
|
| |
| record_id = result["id"] |
| logger.info(f"Created Salesforce record ID: {record_id}") |
|
|
| |
| pdf_data = pdf_buffer.getvalue() |
| pdf_base64 = base64.b64encode(pdf_data).decode('utf-8') |
| content_version = { |
| "Title": f"Delay_Prediction_Report_{input_data['project_name']}_{datetime.now().strftime('%Y%m%d_%H%M%S')}", |
| "PathOnClient": "project_delay_report.pdf", |
| "VersionData": pdf_base64, |
| "FirstPublishLocationId": record_id |
| } |
| cv_result = sf.ContentVersion.create(content_version) |
| if not cv_result["success"]: |
| logger.error(f"Failed to upload PDF to Salesforce: {cv_result['errors']}") |
| return f"Failed to upload PDF to Salesforce: {cv_result['errors']}" |
|
|
| |
| content_version_id = cv_result["id"] |
|
|
| |
| query = f"SELECT ContentDocumentId FROM ContentVersion WHERE Id = '{content_version_id}'" |
| query_result = sf.query(query) |
| if query_result["totalSize"] == 0: |
| logger.error(f"Failed to retrieve ContentDocumentId for ContentVersion {content_version_id}") |
| return "Failed to retrieve ContentDocumentId for the ContentVersion" |
| content_document_id = query_result["records"][0]["ContentDocumentId"] |
|
|
| |
| pdf_url = f"{sf_instance_url}/sfc/servlet.shepherd/document/download/{content_document_id}" |
| logger.info(f"Generated PDF URL: {pdf_url}") |
|
|
| |
| update_result = sf.Delay_Predictor__c.update(record_id, {"PDF_Report__c": pdf_url}) |
| if update_result != 204: |
| logger.error(f"Failed to update PDF_Report__c with URL: {pdf_url}") |
| return f"Failed to update PDF_Report__c field: {update_result}" |
|
|
| return None |
| except Exception as e: |
| logger.error(f"Error saving to Salesforce: {str(e)}") |
| return f"Error saving to Salesforce: {str(e)}" |
|
|
| |
| st.markdown("### Project Details") |
| col1, col2 = st.columns([1, 1]) |
|
|
| with col1: |
| project_name = st.text_input("Project Name", help="Enter the name of the project") |
| phase = st.selectbox( |
| "Phase", |
| [""] + ["Planning", "Design", "Construction"], |
| index=0 if st.session_state.phase == "" else ["", "Planning", "Design", "Construction"].index(st.session_state.phase), |
| key="phase_select", |
| help="Select the project phase" |
| ) |
| |
| |
| if phase != st.session_state.phase: |
| st.session_state.phase = phase |
| st.session_state.task = "" |
| logger.info(f"Phase changed to {phase}, resetting task") |
| |
| task_options_list = [""] + task_options.get(phase, []) if phase else [""] |
| logger.info(f"Task options for phase '{phase}': {task_options_list}") |
| task = st.selectbox( |
| "Task", |
| task_options_list, |
| index=0 if st.session_state.task == "" else task_options_list.index(st.session_state.task) if st.session_state.task in task_options_list else 0, |
| key="task_select", |
| help="Select the task corresponding to the phase" |
| ) |
| st.session_state.task = task |
| current_progress = st.number_input("Current Progress (%)", min_value=0.0, max_value=100.0, step=1.0, value=0.0, help="Enter the current progress percentage") |
| task_expected_duration = st.number_input("Task Expected Duration (days)", min_value=0, step=1, value=0, help="Enter the expected duration in days") |
| task_actual_duration = st.number_input("Task Actual Duration (days)", min_value=0, step=1, value=0, help="Enter the actual duration in days") |
|
|
| with col2: |
| workforce_gap = st.number_input("Workforce Gap (%)", min_value=0.0, max_value=100.0, step=1.0, value=0.0, help="Enter the workforce gap percentage") |
| workforce_skill_level = st.selectbox("Workforce Skill Level", ["", "Low", "Medium", "High"], index=0, help="Select the workforce skill level") |
| workforce_shift_hours = st.number_input("Workforce Shift Hours", min_value=0, step=1, value=0, help="Enter the shift hours") |
| st.write(f"**Selected Shift Hours**: {workforce_shift_hours}") |
| project_location = st.text_input("Project Location (City)", placeholder="e.g., New York", help="Enter the city for weather data") |
| weather_forecast_date = st.date_input("Weather Forecast Date", min_value=datetime(2025, 1, 1), value=None, help="Select the forecast date") |
|
|
| |
| predict_button = st.button("Fetch Weather and Predict Delay") |
|
|
| |
| if predict_button: |
| logger.info("Processing prediction request") |
| input_data = { |
| "project_name": project_name, |
| "phase": phase, |
| "task": task, |
| "current_progress": current_progress, |
| "task_expected_duration": task_expected_duration, |
| "task_actual_duration": task_actual_duration, |
| "workforce_gap": workforce_gap, |
| "workforce_skill_level": workforce_skill_level, |
| "workforce_shift_hours": workforce_shift_hours, |
| "weather_impact_score": 0, |
| "weather_condition": "", |
| "weather_forecast_date": weather_forecast_date.strftime("%Y-%m-%d") if weather_forecast_date else "", |
| "project_location": project_location |
| } |
| |
| |
| error = validate_inputs(input_data) |
| if error and not error.startswith("Please select or fill in weather"): |
| st.error(error) |
| logger.error(f"Validation error: {error}") |
| else: |
| |
| if project_location and weather_forecast_date: |
| weather_data, weather_error = fetch_weather_data(project_location, input_data["weather_forecast_date"]) |
| if weather_error: |
| st.error(weather_error.get("error", "Unknown weather error")) |
| logger.error(weather_error.get("error", "Unknown weather error")) |
| input_data["weather_impact_score"] = 50 |
| input_data["weather_condition"] = "Unknown" |
| else: |
| input_data["weather_impact_score"] = weather_data["weather_impact_score"] |
| input_data["weather_condition"] = weather_data["weather_condition"] |
| st.write(f"**Weather Data for {project_location} on {input_data['weather_forecast_date']}**:") |
| st.write(f"- Condition: {weather_data['weather_condition']}") |
| st.write(f"- Impact Score: {weather_data['weather_impact_score']}") |
| st.write(f"- Temperature: {weather_data['temperature']}°C") |
| st.write(f"- Humidity: {weather_data['humidity']}%") |
| st.session_state.weather_data = weather_data |
| else: |
| st.error("Please provide a project location and weather forecast date.") |
| logger.error("Project location or weather forecast date missing") |
| input_data["weather_impact_score"] = 50 |
| input_data["weather_condition"] = "Unknown" |
| |
| |
| error = validate_inputs(input_data) |
| if error: |
| st.error(error) |
| logger.error(f"Validation error: {error}") |
| else: |
| with st.spinner("Generating predictions and AI insights..."): |
| try: |
| prediction = predict_delay(input_data) |
| except Exception as e: |
| st.error(f"Prediction failed: {str(e)}") |
| logger.error(f"Prediction failed: {str(e)}") |
| prediction = {"error": str(e)} |
| |
| if "error" in prediction: |
| st.error(prediction["error"]) |
| else: |
| st.subheader("Prediction Results") |
| st.write(f"**Delay Probability**: {prediction['delay_probability']:.2f}%") |
| st.write("**High Risk Phases**:") |
| for line in format_high_risk_phases(prediction['high_risk_phases']): |
| st.write(line) |
| st.write(f"**AI Insights**: {prediction['ai_insights']}") |
| st.write(f"**Weather Condition**: {prediction['weather_condition']}") |
| |
| |
| chart_config = generate_heatmap(prediction['delay_probability'], f"{phase}: {task}") |
| chart_id = f"chart-{hash(str(chart_config))}" |
| chart_html = f""" |
| <canvas id="{chart_id}" style="max-height: 200px; max-width: 600px;"></canvas> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <script> |
| try {{ |
| const ctx = document.getElementById('{chart_id}').getContext('2d'); |
| new Chart(ctx, {json.dumps(chart_config)}); |
| }} catch (e) {{ |
| console.error('Chart.js failed: ' + e); |
| }} |
| </script> |
| """ |
| try: |
| components.html(chart_html, height=250) |
| logger.info("Chart.js heatmap rendered") |
| except Exception as e: |
| logger.error(f"Chart.js rendering failed: {str(e)}") |
| st.error("Failed to render heatmap; please check your browser settings.") |
| |
| |
| fig, ax = plt.subplots(figsize=(8, 2)) |
| color = 'red' if prediction['delay_probability'] > 75 else 'yellow' if prediction['delay_probability'] > 50 else 'green' |
| ax.barh([f"{phase}: {task}"], [prediction['delay_probability']], color=color, edgecolor='black') |
| ax.set_xlim(0, 100) |
| ax.set_xlabel("Delay Probability (%)") |
| ax.set_title("Delay Risk Heatmap") |
| plt.tight_layout() |
| |
| |
| gantt_fig = generate_gantt_chart(input_data, prediction) |
| if gantt_fig: |
| st.plotly_chart(gantt_fig, use_container_width=True) |
| logger.info("Gantt chart rendered") |
| |
| pdf_buffer = generate_pdf(input_data, prediction, fig, gantt_fig) |
| plt.close(fig) |
| st.download_button( |
| label="Download Prediction Report (PDF)", |
| data=pdf_buffer, |
| file_name="project_delay_report.pdf", |
| mime="application/pdf" |
| ) |
| |
| |
| sf_error = save_to_salesforce(input_data, prediction, pdf_buffer) |
| if sf_error: |
| st.error(sf_error) |
| logger.error(f"Salesforce error: {sf_error}") |
| else: |
| st.success("Prediction data and PDF successfully saved to Salesforce!") |
| logger.info("Data and PDF saved to Salesforce") |
| |
| st.session_state.prediction = prediction |
| st.session_state.input_data = input_data |