# Effort Estimator - final UI updates per user's last requests # If needed in Colab uncomment: # !pip install --quiet gradio openpyxl pandas import pandas as pd import gradio as gr import warnings warnings.filterwarnings('ignore') # ---------------- Load data ---------------- data_path = "Effort Estimation Sample Data v0.1.xlsx" sheet1 = pd.read_excel(data_path, sheet_name="Efforts Breakdown") sheet2 = pd.read_excel(data_path, sheet_name="Complexity Matrix") # ---------------- Constants & UI colors ---------------- phase_display_names = { "Design effort (days)": "Design", "Build effort (days)": "Build", "Testing effort (days)": "Testing", "Post Go Live (days)": "Go Live" } EY_COLORS = { "black": "#0b0b0b", "white": "#FFFFFF", "turbo": "#FFE600", "sonic_silver": "#797878", "sizzling_sunrise": "#FFDB00" } competencies = sorted(list(sheet1["Competency"].dropna().unique())) FIXED_COMPLEXITIES = ["L", "M", "H"] MAX_SUBMODULES = 30 # ---------------- Helper functions ---------------- def get_modules(competency): if not competency: return [] return list(sheet1[sheet1["Competency"] == competency]["Module"].dropna().unique()) def get_submodules(competency, modules): if not competency or not modules: return [] all_pairs = [] for module in modules: submodules = list(sheet1[ (sheet1["Competency"] == competency) & (sheet1["Module"] == module) ]["Sub - Module"].dropna().unique()) for sm in submodules: all_pairs.append((module, sm)) return all_pairs def get_complexities_for_submodule(competency, module, submodule): return FIXED_COMPLEXITIES def get_phases_for_submodule(competency, module, submodule): if not competency or not module or not submodule: return [] filtered = sheet1[ (sheet1["Competency"] == competency) & (sheet1["Module"] == module) & (sheet1["Sub - Module"] == submodule) ] if filtered.empty: return [] phases = [] for col, disp in phase_display_names.items(): if col in filtered.columns: col_vals = filtered[col].dropna() if not col_vals.empty: try: numeric_mask = col_vals.astype(float) > 0 if numeric_mask.any(): phases.append(disp) except: phases.append(disp) return phases def create_submodule_details_html(competency, modules, module_submodule_pairs): if not competency or not modules or not module_submodule_pairs: return "" html = f"""

📋 Sub-Module Details (Available)

""" grouped = {} for module, sm in module_submodule_pairs: grouped.setdefault(module, []).append(sm) for module, sms in grouped.items(): html += f"
Module: {module}
" for sm in sms: complexities = get_complexities_for_submodule(competency, module, sm) phases = get_phases_for_submodule(competency, module, sm) html += f"
" html += f"{sm}
Complexities: {', '.join(complexities)} | Phases: {', '.join(phases) if phases else 'None'}" html += "
" html += "
" html += "
" return html def calculate_effort_per_submodule(competency, selected_modules, selected_sm_identifiers, submodule_counts, per_submodule_complexities, per_submodule_phases, buffer_percent): if not competency: return "❌ Please select a Competency." if not selected_modules: return "❌ Please select at least one Module." if not selected_sm_identifiers: return "❌ Please select at least one Sub-Module." overall_complexities = set() overall_phases = set() for smid in selected_sm_identifiers: cs = per_submodule_complexities.get(smid, []) ps = per_submodule_phases.get(smid, []) overall_complexities.update(cs) overall_phases.update(ps) if not overall_complexities: return "❌ Please select at least one Complexity Level for the chosen sub-modules." if not overall_phases: return "❌ Please select at least one Phase for the chosen sub-modules." total_effort = 0.0 breakdown_rows = [] for smid in selected_sm_identifiers: try: module, submodule = smid.split("||", 1) except: continue count = int(submodule_counts.get(smid, 1)) complexities = per_submodule_complexities.get(smid, []) phases = per_submodule_phases.get(smid, []) for complexity in complexities: filtered_data = sheet1[ (sheet1["Competency"] == competency) & (sheet1["Module"] == module) & (sheet1["Sub - Module"] == submodule) ] if filtered_data.empty: continue weight_rows = sheet2[ (sheet2["Competency"] == competency) & (sheet2["Complexity"] == complexity) ] weight = float(weight_rows["Weightage"].values[0]) if not weight_rows.empty else 1.0 combination_effort = 0.0 phase_details = [] for phase in phases: phase_col = next((col for col, disp in phase_display_names.items() if disp == phase), None) if phase_col and phase_col in filtered_data.columns: phase_effort = float(filtered_data[phase_col].sum()) * weight phase_total = phase_effort * count combination_effort += phase_total phase_details.append(f"{phase}: {phase_effort:.1f}x{count} = {phase_total:.1f} days") if combination_effort > 0: total_effort += combination_effort breakdown_rows.append({ 'module': module, 'submodule': submodule, 'complexity': complexity, 'count': count, 'phases': ', '.join(phases), 'effort': round(combination_effort, 2), 'phase_details': ' | '.join(phase_details) }) if total_effort == 0: return "❌ No effort data found for the selected combinations." final_effort = total_effort * (1 + buffer_percent / 100) # build results HTML # summary block uses white text on dark background to be visible summary_html = f"""
Competency: {competency}
Selected Modules/Sub-Modules: {', '.join(selected_sm_identifiers)}
Buffer Percentage: {buffer_percent}%
""" table_html = f"""

Effort Breakdown

""" # all rows except header: white background for clarity for row in breakdown_rows: table_html += f""" """ table_html += "
Module Sub Module Complexity Count Phases Effort (days)
{row['module']} {row['submodule']} {row['complexity']} {row['count']} {row['phases']} {row['effort']}
" totals_html = f"""
Total Efforts (before buffer):{total_effort:.2f} days
Buffer Applied ({buffer_percent}%):+ {total_effort * buffer_percent/100:.2f} days
Total Efforts (with buffer):{final_effort:.2f} days
""" # detailed phase breakdown area - white text on dark background phase_html = "
" phase_html += "

Detailed Phase Breakdown:

" for row in breakdown_rows: prefix = f"{row['module']} - {row['submodule']} ({row['complexity']}):" phase_html += ( f"

" f"{prefix} " f"{row['phase_details']}

" ) phase_html += "
" full_html = f"""
{summary_html}
{table_html}
{totals_html}
{phase_html}
""" return full_html # ----- LOGIN CREDENTIALS ----- VALID_USERNAME = "admin" VALID_PASSWORD = "password123" def login(username, password): if username == VALID_USERNAME and password == VALID_PASSWORD: return True, "" else: return False, "Invalid username or password" # ---------------- Gradio UI ---------------- with gr.Blocks( theme=gr.themes.Default(primary_hue="yellow", neutral_hue="gray"), title="Effort Estimator Tool", css=f""" .gradio-container {{ background: linear-gradient(135deg, {EY_COLORS['black']} 0%, #111111 100%); font-family: Arial, sans-serif; min-height: 100vh; color: {EY_COLORS['white']}; }} .ey-header {{ text-align: center; padding: 18px; color: {EY_COLORS['white']}; display:flex; align-items:center; justify-content:space-between; gap:12px; }} .ey-header .title-block {{ text-align:left; }} .ey-header img.logo {{ height: 54px; margin-right: 12px; vertical-align: middle; }} .ey-button {{ background: {EY_COLORS['turbo']} !important; color: {EY_COLORS['black']} !important; border: 1px solid {EY_COLORS['sonic_silver']} !important; }} .panel {{ padding: 16px; background: {EY_COLORS['white']}; border-radius: 10px; margin: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.12); color: {EY_COLORS['black']}; }} .sub-row {{ padding: 6px 0; display:flex; align-items:center; gap:12px; justify-content:space-between; }} .module-sub-label {{ font-weight:700; color:{EY_COLORS['black']}; }} .ey-title {{ font-size: 26px; font-weight: 700; color: {EY_COLORS['turbo']}; }} .ey-subtitle {{ color: {EY_COLORS['white']}; margin-top:3px; }} .login-error {{ padding: 8px; border-radius: 6px; background: rgba(255,0,0,0.06); border: 1px solid rgba(255,0,0,0.2); color: #FF3333; font-weight:700; }} h1,h2,h3 {{ color: {EY_COLORS['turbo']}; }} /* Make accordion content use full width */ .gradio-accordion .gradio-container > div {{ width: 100% !important; }} """ ) as demo: # LOGIN PANEL with gr.Column(visible=True) as login_panel: gr.Markdown("## 🔐 Login to Access Effort Estimation Tool", elem_id="login-title") username_input = gr.Textbox(label="Username") password_input = gr.Textbox(label="Password", type="password") login_btn = gr.Button("Login", elem_classes="ey-button") login_msg_html = gr.HTML("", visible=False) # MAIN APP PANEL (hidden initially) with gr.Column(visible=False) as main_app_panel: gr.HTML(f"""
Effort Estimation Tool
Enterprise Grade Estimation Platform - Digital Supply Chain
DSC Banner
""") # INPUT PAGE (single-column, full width) with gr.Column(visible=True) as page_inputs: with gr.Row(): competency = gr.Dropdown(choices=competencies, value=None, label="🏢 Select Competency", interactive=True) with gr.Row(): modules = gr.CheckboxGroup(choices=[], value=[], label="📦 Select Modules", interactive=True) # Collapsible sub-module configuration (Accordion) with gr.Accordion("🔧 Sub-module level configuration (click to expand/collapse)", open=True): # helper line in white as requested gr.HTML("🔔 Select only one Complexity level per sub-module. Phases can be multi-selected.") per_sm_row = [] per_sm_checkbox = [] per_sm_name = [] per_sm_count = [] per_sm_complexities = [] per_sm_phases = [] for i in range(MAX_SUBMODULES): with gr.Row(visible=False) as row: chk = gr.Checkbox(label="", value=False) name = gr.Textbox(value="", interactive=False, visible=False) cnt = gr.Number(value=1, minimum=1, label="Count", interactive=True) comps_radio = gr.Radio(choices=[], value=None, label="Complexity (L/M/H)", interactive=True) phases_cg = gr.CheckboxGroup(choices=[], value=[], label="Phases (select any)", interactive=True) per_sm_row.append(row) per_sm_checkbox.append(chk) per_sm_name.append(name) per_sm_count.append(cnt) per_sm_complexities.append(comps_radio) per_sm_phases.append(phases_cg) submodule_details = gr.HTML("", visible=True) # Buffer + calculate placed at the end (full width) with gr.Row(): with gr.Column(scale=3): buffer_percent = gr.Slider(0, 50, value=16, step=1, label="📊 Buffer Percentage (%)") with gr.Column(scale=1): calculate_btn = gr.Button("🚀 Calculate Total Effort", elem_classes="ey-button") gr.Markdown("
Results will open on a new page after calculation.
") # RESULTS PAGE (hidden initially) - no extra header with gr.Column(visible=False, elem_classes="panel") as page_results: results = gr.HTML("", label="Estimation Results") results_back = gr.Button("⬅️ Back to Inputs", elem_classes="ey-button") # EVENTS def on_competency_change(comp): return gr.update(choices=get_modules(comp), value=[]) competency.change(fn=on_competency_change, inputs=competency, outputs=modules) def on_modules_change(comp, mods): module_sm_pairs = get_submodules(comp, mods) if mods else [] updates = [] for i in range(MAX_SUBMODULES): if i < len(module_sm_pairs): module, sm_name = module_sm_pairs[i] identifier = f"{module}||{sm_name}" updates.append(gr.update(visible=True)) updates.append(gr.update(label=f"{module} \u2192 {sm_name}", value=False)) updates.append(gr.update(value=identifier, visible=False)) updates.append(gr.update(visible=True, value=1)) comp_choices = FIXED_COMPLEXITIES phase_choices = get_phases_for_submodule(comp, module, sm_name) updates.append(gr.update(choices=comp_choices, value=None)) updates.append(gr.update(choices=phase_choices, value=[])) else: updates.append(gr.update(visible=False)) updates.append(gr.update(label="", value=False)) updates.append(gr.update(value="", visible=False)) updates.append(gr.update(visible=False, value=1)) updates.append(gr.update(choices=[], value=None)) updates.append(gr.update(choices=[], value=[])) details_html = create_submodule_details_html(comp, mods, module_sm_pairs) if (comp and mods and module_sm_pairs) else "" return updates + [details_html] row_outputs = [] for i in range(MAX_SUBMODULES): row_outputs += [per_sm_row[i], per_sm_checkbox[i], per_sm_name[i], per_sm_count[i], per_sm_complexities[i], per_sm_phases[i]] modules.change( fn=on_modules_change, inputs=[competency, modules], outputs=row_outputs + [submodule_details] ) def collect_and_compute(comp, mods, *args_and_buffer): total_expected = MAX_SUBMODULES * 5 + 1 if len(args_and_buffer) != total_expected: return ("❌ Internal: unexpected inputs passed to calculator.", gr.update(visible=True), gr.update(visible=False)) row_args = args_and_buffer[:-1] buffer_val = args_and_buffer[-1] selected_sm_identifiers = [] submodule_counts = {} per_sub_comps = {} per_sub_phases = {} for i in range(MAX_SUBMODULES): base = i * 5 chk = row_args[base + 0] identifier = row_args[base + 1] cnt = row_args[base + 2] comps_sel_raw = row_args[base + 3] phases_sel = row_args[base + 4] or [] if comps_sel_raw is None or comps_sel_raw == "": comps_sel = [] else: comps_sel = [comps_sel_raw] if chk: if not identifier: continue selected_sm_identifiers.append(identifier) try: submodule_counts[identifier] = int(cnt) if (cnt is not None and str(cnt).strip() != "") else 1 except: submodule_counts[identifier] = 1 per_sub_comps[identifier] = comps_sel per_sub_phases[identifier] = phases_sel html = calculate_effort_per_submodule(comp, mods, selected_sm_identifiers, submodule_counts, per_sub_comps, per_sub_phases, buffer_val) # show results page (hide inputs) return (html, gr.update(visible=False), gr.update(visible=True)) calculation_inputs = [competency, modules] for i in range(MAX_SUBMODULES): calculation_inputs += [per_sm_checkbox[i], per_sm_name[i], per_sm_count[i], per_sm_complexities[i], per_sm_phases[i]] calculation_inputs += [buffer_percent] calculate_btn.click(fn=collect_and_compute, inputs=calculation_inputs, outputs=[results, page_inputs, page_results]) def back_to_inputs(): return (gr.update(visible=True), gr.update(visible=False), "") results_back.click(fn=back_to_inputs, inputs=[], outputs=[page_inputs, page_results, results]) # LOGIN BUTTON EVENT (popup) def on_login_click(username, password): success, _ = login(username, password) if success: return gr.update(visible=True), gr.update(visible=False, value=""), gr.update(visible=False) else: err_html = f"
❌ Invalid username or password
" return gr.update(visible=False), gr.update(visible=True, value=err_html), gr.update(visible=True) login_btn.click( fn=on_login_click, inputs=[username_input, password_input], outputs=[main_app_panel, login_msg_html, login_panel] ) demo.launch(share=True, debug=True)