Spaces:
Sleeping
Sleeping
| # 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""" | |
| <div style='background: {EY_COLORS["white"]}; padding: 12px; border-radius: 8px; margin: 6px 0; border: 1px solid {EY_COLORS["sonic_silver"]}; color:{EY_COLORS['black']}'> | |
| <h4 style='margin-top: 0;'>π Sub-Module Details (Available)</h4> | |
| """ | |
| grouped = {} | |
| for module, sm in module_submodule_pairs: | |
| grouped.setdefault(module, []).append(sm) | |
| for module, sms in grouped.items(): | |
| html += f"<div style='margin-bottom:8px;'><strong style='color:{EY_COLORS['black']}'>Module: {module}</strong><br>" | |
| for sm in sms: | |
| complexities = get_complexities_for_submodule(competency, module, sm) | |
| phases = get_phases_for_submodule(competency, module, sm) | |
| html += f"<div style='padding:8px;margin:6px 0;border-radius:6px;border-left:4px solid {EY_COLORS['turbo']};background:{EY_COLORS['white']};'>" | |
| html += f"<strong>{sm}</strong><br><small style='color:{EY_COLORS['sonic_silver']};'>Complexities: {', '.join(complexities)} | Phases: {', '.join(phases) if phases else 'None'}</small>" | |
| html += "</div>" | |
| html += "</div>" | |
| html += "</div>" | |
| 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""" | |
| <div style='background: rgba(11,11,11,0.95); padding: 12px; border-radius: 6px; color: {EY_COLORS['white']};'> | |
| <span style='font-weight:bold; color:{EY_COLORS['white']};'>Competency:</span> {competency}<br> | |
| <span style='font-weight:bold; color:{EY_COLORS['white']};'>Selected Modules/Sub-Modules:</span> {', '.join(selected_sm_identifiers)}<br> | |
| <span style='font-weight:bold; color:{EY_COLORS['white']};'>Buffer Percentage:</span> {buffer_percent}% | |
| </div> | |
| """ | |
| table_html = f""" | |
| <h2 style='color: {EY_COLORS['turbo']};'>Effort Breakdown</h2> | |
| <table style='width:100%; border-collapse:collapse; border:2px solid {EY_COLORS['sonic_silver']}'> | |
| <tr style='background:{EY_COLORS['sizzling_sunrise']}'> | |
| <th style='padding:12px; border:1px solid {EY_COLORS['sonic_silver']}; text-align:left;'>Module</th> | |
| <th style='padding:12px; border:1px solid {EY_COLORS['sonic_silver']}; text-align:left;'>Sub Module</th> | |
| <th style='padding:12px; border:1px solid {EY_COLORS['sonic_silver']}; text-align:left;'>Complexity</th> | |
| <th style='padding:12px; border:1px solid {EY_COLORS['sonic_silver']}; text-align:left;'>Count</th> | |
| <th style='padding:12px; border:1px solid {EY_COLORS['sonic_silver']}; text-align:left;'>Phases</th> | |
| <th style='padding:12px; border:1px solid {EY_COLORS['sonic_silver']}; text-align:right;'>Effort (days)</th> | |
| </tr> | |
| """ | |
| # all rows except header: white background for clarity | |
| for row in breakdown_rows: | |
| table_html += f""" | |
| <tr style='background: {EY_COLORS['white']}; color: {EY_COLORS['black']}'> | |
| <td style='padding:10px; border:1px solid {EY_COLORS['sonic_silver']};'>{row['module']}</td> | |
| <td style='padding:10px; border:1px solid {EY_COLORS['sonic_silver']};'>{row['submodule']}</td> | |
| <td style='padding:10px; border:1px solid {EY_COLORS['sonic_silver']};'>{row['complexity']}</td> | |
| <td style='padding:10px; border:1px solid {EY_COLORS['sonic_silver']};'>{row['count']}</td> | |
| <td style='padding:10px; border:1px solid {EY_COLORS['sonic_silver']};'>{row['phases']}</td> | |
| <td style='padding:10px; border:1px solid {EY_COLORS['sonic_silver']}; text-align:right;'>{row['effort']}</td> | |
| </tr> | |
| """ | |
| table_html += "</table>" | |
| totals_html = f""" | |
| <div style='margin-top:20px; padding:12px; background:{EY_COLORS['white']}; border:1px solid {EY_COLORS['sonic_silver']}; border-radius:6px; color:{EY_COLORS['black']}'> | |
| <table style='width:100%'> | |
| <tr><td><strong>Total Efforts (before buffer):</strong></td><td style='text-align:right'>{total_effort:.2f} days</td></tr> | |
| <tr><td><strong>Buffer Applied ({buffer_percent}%):</strong></td><td style='text-align:right'>+ {total_effort * buffer_percent/100:.2f} days</td></tr> | |
| <tr style='background:{EY_COLORS["sizzling_sunrise"]}'><td style='padding:8px;'><strong>Total Efforts (with buffer):</strong></td><td style='text-align:right; font-size:1.1em; padding:8px;'><strong>{final_effort:.2f} days</strong></td></tr> | |
| </table> | |
| </div> | |
| """ | |
| # detailed phase breakdown area - white text on dark background | |
| phase_html = "<div style='margin-top:16px; padding:12px; background: rgba(11,11,11,0.95); color: white; border-radius:6px;'>" | |
| phase_html += "<h3 style='color:" + EY_COLORS['turbo'] + ";'>Detailed Phase Breakdown:</h3>" | |
| for row in breakdown_rows: | |
| prefix = f"{row['module']} - {row['submodule']} ({row['complexity']}):" | |
| phase_html += ( | |
| f"<p style='margin:6px 0; color:{EY_COLORS['white']};'>" | |
| f"<strong style='color:{EY_COLORS['white']};'>{prefix}</strong> " | |
| f"{row['phase_details']}</p>" | |
| ) | |
| phase_html += "</div>" | |
| full_html = f""" | |
| <div style='font-family: Arial, sans-serif;'> | |
| {summary_html} | |
| <div style='margin-top:12px;'>{table_html}</div> | |
| <div>{totals_html}</div> | |
| <div>{phase_html}</div> | |
| </div> | |
| """ | |
| 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""" | |
| <div class="ey-header" style="display:flex; justify-content:space-between; align-items:center;"> | |
| <!-- Left block: EY logo + title --> | |
| <div style="display:flex; align-items:center;"> | |
| <img class="logo" src="Picture1.png" alt="EY Logo" | |
| style="height:60px; margin-right:8px;" | |
| onerror="this.src='https://www.ey.com/content/dam/ey-unified-site/ey-com/en-in/generic/images/ey-logo-black.png'"> | |
| <div class="title-block"> | |
| <div class="ey-title">Effort Estimation Tool</div> | |
| <div class="ey-subtitle">Enterprise Grade Estimation Platform - Digital Supply Chain</div> | |
| </div> | |
| </div> | |
| <!-- Right block: DSC image --> | |
| <div> | |
| <img src="Digital Supply Chain.png" alt="DSC Banner" style="height:60px;" | |
| onerror="this.style.display='none'"> | |
| </div> | |
| </div> | |
| """) | |
| # 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("<small style='color: black;'>π Select only one Complexity level per sub-module. Phases can be multi-selected.</small>") | |
| 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("<div style='text-align:right; color:#cccccc; margin-top:8px;'>Results will open on a new page after calculation.</div>") | |
| # 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"<div class='login-error'>β Invalid username or password</div>" | |
| 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) | |