import os import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go import streamlit as st from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split st.set_page_config( page_title="FreshWise Studio", page_icon="🥬", layout="wide", initial_sidebar_state="expanded", ) DATA_CANDIDATES = [ os.environ.get("DATA_PATH", ""), "perishable_goods_management.csv", "/app/perishable_goods_management.csv", "/data/perishable_goods_management.csv", "/mnt/data/perishable_goods_management.csv", ] BRAND = { "name": "FreshWise Studio", "tagline": "Turn perishables into profit, loyalty, and lower waste.", "manager_copy": "For operators who need sharper replenishment, clearer risk alerts, and smarter campaign design.", "consumer_copy": "For shoppers who want better deals, curated bundles, and easy discovery of time-sensitive offers.", } REGION_COORDS = { "North": (53.48, -2.24), "South": (51.50, -0.12), "East": (52.20, 0.12), "West": (51.48, -3.18), "Central": (52.48, -1.89), } def inject_css(): st.markdown( """ """, unsafe_allow_html=True, ) def find_data_path() -> str: for path in DATA_CANDIDATES: if path and os.path.exists(path): return path raise FileNotFoundError("perishable_goods_management.csv not found. Put it next to app.py or set DATA_PATH.") @st.cache_data(show_spinner=False) def load_data() -> pd.DataFrame: df = pd.read_csv(find_data_path()) df["transaction_date"] = pd.to_datetime(df["transaction_date"], errors="coerce") df["expiration_date"] = pd.to_datetime(df["expiration_date"], errors="coerce") df["sell_through_pct"] = np.where(df["initial_quantity"] > 0, df["units_sold"] / df["initial_quantity"], 0) df["stock_demand_ratio"] = np.where(df["daily_demand"] > 0, df["initial_quantity"] / df["daily_demand"], np.nan) df["gross_margin"] = df["selling_price"] - df["cost_price"] df["leftover_units"] = (df["initial_quantity"] - df["units_sold"]).clip(lower=0) df["savings"] = df["base_price"] - df["selling_price"] df["value_score"] = ( (1 - df["waste_pct"].clip(0, 1)) * 0.25 + df["discount_pct"].clip(0, 0.6) * 0.35 + df["sell_through_pct"].clip(0, 1) * 0.2 + (1 - np.minimum(df["days_until_expiry"], 14) / 14) * 0.2 ) df["expiry_bucket"] = pd.cut( df["days_until_expiry"], bins=[-1, 1, 3, 7, 30, 10000], labels=["<=1d", "2-3d", "4-7d", "8-30d", ">30d"], ) df["high_waste_flag"] = (df["waste_pct"] >= df["waste_pct"].quantile(0.75)).astype(int) df["promo_readiness"] = ( df["discount_pct"] * 0.3 + (1 - np.minimum(df["days_until_expiry"], 7) / 7) * 0.3 + df["spoilage_risk"] * 0.2 + (1 - df["sell_through_pct"].clip(0, 1)) * 0.2 ) return attach_store_locations(df) def _region_anchor(region: str): return REGION_COORDS.get(region, (52.0, 0.0)) def attach_store_locations(df: pd.DataFrame) -> pd.DataFrame: stores = sorted(df["store_id"].dropna().unique()) rows = [] for store in stores: sub = df[df["store_id"] == store] region = str(sub["region"].mode().iloc[0]) if not sub.empty else "Central" base_lat, base_lon = _region_anchor(region) seed = abs(hash(store)) % 10000 rng = np.random.default_rng(seed) rows.append( { "store_id": store, "store_lat": base_lat + rng.uniform(-0.35, 0.35), "store_lon": base_lon + rng.uniform(-0.45, 0.45), } ) loc = pd.DataFrame(rows) return df.merge(loc, on="store_id", how="left") @st.cache_resource(show_spinner=False) def risk_model(df: pd.DataFrame): features = [ "daily_demand", "initial_quantity", "shelf_life_days", "days_until_expiry", "temp_deviation", "temp_abuse_events", "handling_score", "packaging_score", "spoilage_risk", "discount_pct", "markdown_applied", "is_weekend", "supplier_score", ] X = df[features] y = df["high_waste_flag"] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) model = RandomForestClassifier(n_estimators=150, random_state=42, max_depth=10, n_jobs=-1) model.fit(X_train, y_train) importances = pd.Series(model.feature_importances_, index=features).sort_values(ascending=False) return model, importances def ensure_state(): if "auth_role" not in st.session_state: st.session_state.auth_role = None if "auth_name" not in st.session_state: st.session_state.auth_name = "" if "logged_in" not in st.session_state: st.session_state.logged_in = False def hero(title: str, subtitle: str): st.markdown( f"""
Fresh retail intelligence

{title}

{subtitle}

""", unsafe_allow_html=True, ) def landing_page(): hero(BRAND["name"], BRAND["tagline"]) c1, c2 = st.columns(2) with c1: st.markdown( f"""
Manager portal

Operate smarter stores

{BRAND['manager_copy']}

Waste alerts Store map Promotion simulation Replenishment
""", unsafe_allow_html=True, ) with st.form("manager_login"): name = st.text_input("Manager name", placeholder="Alex Chen") team = st.text_input("Team / region", placeholder="Operations - North") submitted = st.form_submit_button("Enter Manager Portal", use_container_width=True) if submitted: st.session_state.logged_in = True st.session_state.auth_role = "Manager" st.session_state.auth_name = name or "Manager" st.session_state.auth_team = team or "Operations" st.rerun() with c2: st.markdown( f"""
Consumer app

Find timely deals that fit life

{BRAND['consumer_copy']}

Deal finder Smart bundles Store map Personalized picks
""", unsafe_allow_html=True, ) with st.form("consumer_login"): name = st.text_input("Your name", placeholder="Jamie") home_store = st.text_input("Preferred store / area", placeholder="South") submitted = st.form_submit_button("Enter Consumer App", use_container_width=True) if submitted: st.session_state.logged_in = True st.session_state.auth_role = "Consumer" st.session_state.auth_name = name or "Guest" st.session_state.auth_team = home_store or "Nearby stores" st.rerun() a, b, c = st.columns(3) a.markdown("

Reduce waste

Turn near-expiry inventory into higher sell-through with timing-aware recommendations.

", unsafe_allow_html=True) b.markdown("

Improve margin

Simulate promotions before launch and balance discount depth, channel, and duration.

", unsafe_allow_html=True) c.markdown("

Boost shopper value

Guide customers toward bundles, bargains, and nearby offers that still feel fresh.

", unsafe_allow_html=True) def sidebar_filters(df: pd.DataFrame): st.sidebar.markdown(f"### Welcome, {st.session_state.auth_name}") st.sidebar.caption(f"{st.session_state.auth_role} · {st.session_state.auth_team}") if st.sidebar.button("Log out", use_container_width=True): st.session_state.logged_in = False st.session_state.auth_role = None st.rerun() st.sidebar.markdown("---") st.sidebar.subheader("Filters") regions = st.sidebar.multiselect("Region", sorted(df["region"].dropna().unique())) stores = st.sidebar.multiselect("Store", sorted(df["store_id"].dropna().unique())[:300]) categories = st.sidebar.multiselect("Category", sorted(df["category"].dropna().unique())) expiry_range = st.sidebar.slider("Days until expiry", 0, int(df["days_until_expiry"].max()), (0, 30)) weekend_choice = st.sidebar.selectbox("Day type", ["All", "Weekday", "Weekend"]) out = df.copy() if regions: out = out[out["region"].isin(regions)] if stores: out = out[out["store_id"].isin(stores)] if categories: out = out[out["category"].isin(categories)] out = out[(out["days_until_expiry"] >= expiry_range[0]) & (out["days_until_expiry"] <= expiry_range[1])] if weekend_choice == "Weekday": out = out[out["is_weekend"] == 0] elif weekend_choice == "Weekend": out = out[out["is_weekend"] == 1] return out def metric_row(df: pd.DataFrame): c1, c2, c3, c4, c5 = st.columns(5) c1.metric("Waste rate", f"{df['waste_pct'].mean():.1%}") c2.metric("Avg profit", f"€{df['profit'].mean():.2f}") c3.metric("Sell-through", f"{df['sell_through_pct'].mean():.1%}") c4.metric("Units wasted", f"{df['units_wasted'].mean():.1f}") c5.metric("Markdown rate", f"{df['markdown_applied'].mean():.1%}") def store_map(df: pd.DataFrame, color_col: str, size_col: str, title: str): m = ( df.groupby(["store_id", "region", "store_lat", "store_lon"])[[color_col, size_col, "profit", "units_sold"]] .mean() .reset_index() ) fig = px.scatter_mapbox( m, lat="store_lat", lon="store_lon", color=color_col, size=size_col, hover_name="store_id", hover_data={"region": True, "profit": ':.2f', "units_sold": ':.1f'}, zoom=4.3, height=520, color_continuous_scale="Viridis", size_max=22, title=title, ) fig.update_layout(mapbox_style="open-street-map", margin=dict(l=0, r=0, t=48, b=0)) return fig def manager_dashboard(df: pd.DataFrame): hero("Manager command center", "Monitor store health, waste exposure, profitability, and campaign readiness in one place.") metric_row(df) a, b = st.columns([1.25, 1]) with a: trend = df.groupby(df["transaction_date"].dt.to_period("M").astype(str))[["waste_pct", "profit"]].mean().reset_index() fig = go.Figure() fig.add_trace(go.Scatter(x=trend.iloc[:, 0], y=trend["waste_pct"], mode="lines+markers", name="Waste %")) fig.add_trace(go.Scatter(x=trend.iloc[:, 0], y=trend["profit"], mode="lines+markers", name="Profit", yaxis="y2")) fig.update_layout(title="Monthly performance curve", yaxis=dict(title="Waste %"), yaxis2=dict(title="Profit", overlaying="y", side="right"), legend=dict(orientation="h"), margin=dict(l=10, r=10, t=48, b=10)) st.plotly_chart(fig, use_container_width=True) with b: top = df.groupby("category")[["waste_pct", "profit", "stock_demand_ratio"]].mean().sort_values("waste_pct", ascending=False).head(8).reset_index() fig = px.bar(top, x="waste_pct", y="category", orientation="h", title="Highest-waste categories", color="profit", color_continuous_scale="RdYlGn") st.plotly_chart(fig, use_container_width=True) c1, c2 = st.columns(2) with c1: st.plotly_chart(store_map(df, "waste_pct", "units_sold", "Store map: waste hotspots and sales density"), use_container_width=True) with c2: expiry = df.groupby("expiry_bucket")[["waste_pct", "profit", "discount_pct"]].mean().reset_index() fig = px.line(expiry, x="expiry_bucket", y=["waste_pct", "profit", "discount_pct"], markers=True, title="Expiry-stage economics") st.plotly_chart(fig, use_container_width=True) def manager_inventory(df: pd.DataFrame): st.markdown("## Inventory & replenishment studio") rec = df.copy() rec["recommended_order_qty"] = 1.2 * rec["daily_demand"] * (1 + rec["demand_variability"]) - rec["leftover_units"] rec.loc[rec["shelf_life_days"] <= 7, "recommended_order_qty"] *= 0.7 rec.loc[rec["spoilage_risk"] >= rec["spoilage_risk"].quantile(0.75), "recommended_order_qty"] *= 0.8 rec["recommended_order_qty"] = rec["recommended_order_qty"].clip(lower=0).round() rec["recommended_action"] = np.select( [rec["recommended_order_qty"] < rec["daily_demand"] * 0.4, rec["recommended_order_qty"] > rec["daily_demand"] * 1.1], ["Cut order", "Increase order"], default="Keep steady", ) x1, x2 = st.columns([1.1, 1]) with x1: cat = rec.groupby("category")[["initial_quantity", "recommended_order_qty", "waste_pct", "profit"]].mean().reset_index() cat["order_reduction_pct"] = 1 - cat["recommended_order_qty"] / cat["initial_quantity"] fig = px.bar(cat.sort_values("order_reduction_pct", ascending=False), x="order_reduction_pct", y="category", orientation="h", color="waste_pct", title="Recommended order reduction by category") st.plotly_chart(fig, use_container_width=True) with x2: shortlist = rec.sort_values(["waste_pct", "stock_demand_ratio"], ascending=[False, False])[["store_id", "product_name", "category", "days_until_expiry", "initial_quantity", "daily_demand", "recommended_order_qty", "recommended_action"]].head(18) st.dataframe(shortlist, use_container_width=True, hide_index=True) st.markdown("### What-if simulator") c1, c2, c3, c4 = st.columns(4) selected_category = c1.selectbox("Category", sorted(df["category"].unique())) order_cut = c2.slider("Reduce order %", 0, 40, 12) markdown_shift = c3.slider("Advance markdown by days", 0, 6, 2) transfer_share = c4.slider("Inter-store transfer share %", 0, 30, 10) sim = df[df["category"] == selected_category].copy() current_waste = sim["waste_pct"].mean() current_profit = sim["profit"].mean() current_sell = sim["sell_through_pct"].mean() waste_reduction = 0.38 * (order_cut / 100) + 0.018 * markdown_shift + 0.12 * (transfer_share / 100) profit_uplift = 0.06 * (order_cut / 100) + 0.025 * markdown_shift + 0.08 * (transfer_share / 100) sell_uplift = 0.03 * markdown_shift + 0.05 * (transfer_share / 100) sim_waste = max(current_waste * (1 - waste_reduction), 0) sim_profit = current_profit * (1 + profit_uplift) sim_sell = min(current_sell * (1 + sell_uplift), 1.0) s1, s2, s3 = st.columns(3) s1.metric("Simulated waste", f"{sim_waste:.1%}", delta=f"-{(current_waste - sim_waste):.1%}") s2.metric("Simulated profit", f"€{sim_profit:.2f}", delta=f"€{(sim_profit - current_profit):.2f}") s3.metric("Simulated sell-through", f"{sim_sell:.1%}", delta=f"+{(sim_sell - current_sell):.1%}") def manager_promotions(df: pd.DataFrame): st.markdown("## Promotion simulation studio") left, right = st.columns([1, 1.2]) with left: promo_category = st.selectbox("Category", sorted(df["category"].unique())) expiry_target = st.selectbox("Expiry segment", ["<=1d", "2-3d", "4-7d", "8-30d", ">30d"]) channel = st.selectbox("Primary channel", ["In-store signage", "App push", "Email", "Social media", "Bundle endcap"]) objective = st.selectbox("Campaign objective", ["Reduce waste", "Grow traffic", "Lift margin", "Clear slow movers"]) discount = st.slider("Discount %", 0, 50, 18) duration = st.slider("Campaign duration (days)", 1, 14, 4) budget = st.slider("Media / display budget (€)", 0, 20000, 4000, step=500) bundle = st.checkbox("Bundle with complementary items", value=True) weekend_only = st.checkbox("Weekend only", value=False) geo_boost = st.checkbox("Geo-target high-risk stores", value=True) sub = df[(df["category"] == promo_category) & (df["expiry_bucket"].astype(str) == expiry_target)].copy() if weekend_only: sub = sub[sub["is_weekend"] == 1] base_units = sub["units_sold"].mean() if len(sub) else 0 base_waste = sub["waste_pct"].mean() if len(sub) else 0 base_profit = sub["profit"].mean() if len(sub) else 0 channel_factor = {"In-store signage": 0.09, "App push": 0.12, "Email": 0.08, "Social media": 0.10, "Bundle endcap": 0.14}[channel] objective_factor = {"Reduce waste": 0.14, "Grow traffic": 0.11, "Lift margin": 0.07, "Clear slow movers": 0.13}[objective] demand_lift = channel_factor + objective_factor + discount / 180 + min(duration / 50, 0.12) if bundle: demand_lift += 0.05 if geo_boost: demand_lift += 0.03 if weekend_only: demand_lift += 0.02 est_sales = base_units * (1 + demand_lift) est_waste = max(base_waste * (1 - min(0.48, demand_lift)), 0) est_profit = base_profit * (1 + demand_lift - discount / 140 - budget / 100000) roi = ((est_profit - base_profit) * max(len(sub), 1)) / max(budget, 1) m1, m2 = st.columns(2) m1.metric("Estimated avg units sold", f"{est_sales:.2f}", delta=f"+{(est_sales-base_units):.2f}") m2.metric("Estimated avg waste", f"{est_waste:.1%}", delta=f"-{(base_waste-est_waste):.1%}") m3, m4 = st.columns(2) m3.metric("Estimated avg profit", f"€{est_profit:.2f}", delta=f"€{(est_profit-base_profit):.2f}") m4.metric("Campaign ROI proxy", f"{roi:.2f}x") with right: promo_base = df.groupby("expiry_bucket")[["discount_pct", "waste_pct", "profit", "promo_readiness"]].mean().reset_index() fig = px.bar(promo_base, x="expiry_bucket", y=["discount_pct", "waste_pct"], barmode="group", title="Current discount and waste by expiry") st.plotly_chart(fig, use_container_width=True) scenario = pd.DataFrame({ "Metric": ["Sales lift", "Waste reduction", "Profit change", "Customer reach"], "Current": [1.0, 0.0, 0.0, 1.0], "Simulated": [1 + demand_lift, min(0.48, demand_lift), (est_profit - base_profit) / max(abs(base_profit), 1), 1 + (0.04 if geo_boost else 0) + (0.05 if channel in ["App push", "Social media"] else 0)], }) fig2 = px.bar(scenario, x="Metric", y=["Current", "Simulated"], barmode="group", title="Scenario comparison") st.plotly_chart(fig2, use_container_width=True) st.markdown("### Suggested campaign brief") st.markdown( f"""
Launch a {discount}% {promo_category} campaign for {expiry_target} inventory via {channel} for {duration} days. Prioritize the objective {objective}, {'bundle with complementary items' if bundle else 'keep as a single-item offer'}, and {'target high-risk stores first' if geo_boost else 'deploy evenly across stores'}. This scenario is expected to improve sell-through while lowering expiry-driven waste pressure.
""", unsafe_allow_html=True, ) def manager_risk(df: pd.DataFrame): st.markdown("## Risk monitor") _, importances = risk_model(df) c1, c2 = st.columns([1.05, 1]) with c1: fig = px.bar(importances.head(10).sort_values(), orientation="h", title="Top drivers of high waste risk") st.plotly_chart(fig, use_container_width=True) with c2: st.plotly_chart(store_map(df, "temp_deviation", "temp_abuse_events", "Store map: temperature risk and handling exposure"), use_container_width=True) alerts = ( df.groupby("store_id")[["temp_deviation", "temp_abuse_events", "waste_pct", "profit", "spoilage_risk"]] .mean() .assign(alert_score=lambda x: 0.28 * x["temp_deviation"] + 0.18 * x["temp_abuse_events"] + 0.34 * x["waste_pct"] * 10 + 0.2 * x["spoilage_risk"]) .sort_values("alert_score", ascending=False) .head(15) .reset_index() ) st.dataframe(alerts, use_container_width=True, hide_index=True) def consumer_deals(df: pd.DataFrame): hero("Consumer deal finder", "Discover nearby offers, value-packed products, and time-sensitive bargains.") c1, c2, c3 = st.columns(3) budget = c1.slider("Budget (€)", 5, 60, 20) preferred_category = c2.selectbox("Category", ["All"] + sorted(df["category"].unique())) max_expiry = c3.slider("Max days until expiry", 1, 14, 5) deals = df[df["days_until_expiry"] <= max_expiry].copy() if preferred_category != "All": deals = deals[deals["category"] == preferred_category] deals["deal_score"] = deals["discount_pct"] * 0.4 + deals["value_score"] * 0.4 + deals["savings"].clip(lower=0) / deals["base_price"].replace(0, np.nan).fillna(1) * 0.2 deals = deals.sort_values(["deal_score", "savings"], ascending=False) st.plotly_chart(store_map(deals, "discount_pct", "units_sold", "Nearby stores with strong deal intensity"), use_container_width=True) st.dataframe(deals[["product_name", "category", "store_id", "days_until_expiry", "base_price", "selling_price", "discount_pct", "savings"]].head(30), use_container_width=True, hide_index=True) best = deals[deals["selling_price"] <= budget].head(9) cols = st.columns(3) for i, (_, row) in enumerate(best.iterrows()): with cols[i % 3]: st.markdown( f"""
{row['category']}

{row['product_name']}

€{row['selling_price']:.2f} now · save €{row['savings']:.2f}

Store: {row['store_id']}

Expires in {int(row['days_until_expiry'])} day(s)

""", unsafe_allow_html=True, ) def build_bundle(df: pd.DataFrame, budget: float, people: int, theme: str): work = df[df["days_until_expiry"] <= 7].copy() work["score"] = work["value_score"] + work["discount_pct"] + np.where(work["selling_price"] <= budget / max(people, 1), 0.15, 0) theme_map = { "Quick dinner": ["Ready_to_Eat", "Produce", "Bakery", "Dairy"], "Healthy protein": ["Meat", "Seafood", "Dairy", "Produce"], "Family breakfast": ["Bakery", "Dairy", "Beverages", "Produce"], "Budget saver": list(work["category"].unique()), } work = work[work["category"].isin(theme_map.get(theme, list(work["category"].unique())))] work = work.sort_values(["score", "selling_price"], ascending=[False, True]) chosen, remaining, target_items, used = [], budget, min(max(people + 1, 3), 6), set() for _, row in work.iterrows(): if row["selling_price"] <= remaining: if theme != "Budget saver" and row["category"] in used: continue chosen.append(row) remaining -= row["selling_price"] used.add(row["category"]) if len(chosen) >= target_items: break if not chosen: return pd.DataFrame(), 0.0, 0.0 bundle = pd.DataFrame(chosen) return bundle, float(bundle["selling_price"].sum()), float(bundle["savings"].sum()) def consumer_bundles(df: pd.DataFrame): st.markdown("## Bundle builder") c1, c2, c3 = st.columns(3) budget = c1.slider("Bundle budget (€)", 8, 90, 28) people = c2.slider("People", 1, 6, 2) theme = c3.selectbox("Bundle theme", ["Quick dinner", "Healthy protein", "Family breakfast", "Budget saver"]) bundle, total, saved = build_bundle(df, budget, people, theme) if bundle.empty: st.warning("No bundle found for these settings.") return m1, m2, m3 = st.columns(3) m1.metric("Bundle total", f"€{total:.2f}") m2.metric("You save", f"€{saved:.2f}") m3.metric("Items", f"{len(bundle)}") st.dataframe(bundle[["product_name", "category", "store_id", "selling_price", "base_price", "discount_pct", "days_until_expiry"]], use_container_width=True, hide_index=True) st.info("Managers can reuse these bundles as prebuilt campaign templates for near-expiry conversion.") def consumer_personal(df: pd.DataFrame): st.markdown("## Personalized promotions") c1, c2, c3 = st.columns(3) favorite = c1.selectbox("Favorite category", sorted(df["category"].unique())) price_cap = c2.slider("Max item price (€)", 1, 30, 10) safe_window = c3.checkbox("Hide items expiring within 1 day", value=False) recs = df[df["category"] == favorite].copy() recs = recs[recs["selling_price"] <= price_cap] if safe_window: recs = recs[recs["days_until_expiry"] > 1] recs["score"] = recs["discount_pct"] * 0.5 + recs["value_score"] * 0.5 recs = recs.sort_values("score", ascending=False).head(12) cols = st.columns(3) for i, (_, row) in enumerate(recs.iterrows()): with cols[i % 3]: st.markdown( f"""
Recommended for you

{row['product_name']}

{row['category']} · {row['store_id']}

€{row['selling_price']:.2f} now · save €{row['savings']:.2f}

Expires in {int(row['days_until_expiry'])} day(s)

""", unsafe_allow_html=True, ) st.button("Save offer", key=f"save_{i}", use_container_width=True) def manager_shell(df: pd.DataFrame): tabs = st.tabs(["Overview", "Inventory", "Promotion Studio", "Risk", "Store Map"]) with tabs[0]: manager_dashboard(df) with tabs[1]: manager_inventory(df) with tabs[2]: manager_promotions(df) with tabs[3]: manager_risk(df) with tabs[4]: st.markdown("## Store network view") st.plotly_chart(store_map(df, "profit", "units_sold", "Store map: profitability and sales concentration"), use_container_width=True) by_store = df.groupby(["store_id", "region"])[["profit", "waste_pct", "units_sold", "temp_deviation"]].mean().reset_index() st.dataframe(by_store.sort_values("profit", ascending=False), use_container_width=True, hide_index=True) def consumer_shell(df: pd.DataFrame): tabs = st.tabs(["Deals", "Bundles", "Personalized", "Store Map"]) with tabs[0]: consumer_deals(df) with tabs[1]: consumer_bundles(df) with tabs[2]: consumer_personal(df) with tabs[3]: st.markdown("## Nearby store map") st.plotly_chart(store_map(df, "discount_pct", "units_sold", "Store map: where deals are strongest right now"), use_container_width=True) shortlist = df.sort_values(["discount_pct", "value_score"], ascending=False)[["store_id", "region", "product_name", "category", "selling_price", "discount_pct", "days_until_expiry"]].head(25) st.dataframe(shortlist, use_container_width=True, hide_index=True) def main(): inject_css() ensure_state() try: df = load_data() except Exception as e: st.error(str(e)) st.stop() if not st.session_state.logged_in: landing_page() st.stop() filtered = sidebar_filters(df) if filtered.empty: st.warning("No data left after filtering.") st.stop() st.caption(f"Logged in as {st.session_state.auth_name} · {st.session_state.auth_role}") if st.session_state.auth_role == "Manager": manager_shell(filtered) else: consumer_shell(filtered) st.markdown( """ """, unsafe_allow_html=True, ) if __name__ == "__main__": main()