diff --git a/smart_pantry_manager/data/pantry_omnia.xlsx b/smart_pantry_manager/data/pantry_omnia.xlsx index c5b3b23..4b8d70b 100644 Binary files a/smart_pantry_manager/data/pantry_omnia.xlsx and b/smart_pantry_manager/data/pantry_omnia.xlsx differ diff --git a/smart_pantry_manager/pages/recommended_recipes.py b/smart_pantry_manager/pages/1- recommended_recipes.py similarity index 61% rename from smart_pantry_manager/pages/recommended_recipes.py rename to smart_pantry_manager/pages/1- recommended_recipes.py index cbbdae3..adb5658 100644 --- a/smart_pantry_manager/pages/recommended_recipes.py +++ b/smart_pantry_manager/pages/1- recommended_recipes.py @@ -1,6 +1,14 @@ -# recommended_recipes.py -# Optimized Recommended Recipes page for Smart Pantry Manager -# Date: 2025-11-20 +""" +recommended_recipes.py +Recommended Recipes Page for Smart Pantry Manager + +Features: +- Personalized recipe suggestions based on user's pantry +- Match % calculation and missing ingredient hints +- Streamlit UI with expandable recipe details + +Date: 2025-11-20 +""" import ast import os @@ -12,61 +20,61 @@ import pandas as pd import streamlit as st +# ---------- Page Setup ---------- st.set_page_config(page_title="Recommended Recipes", page_icon="๐Ÿณ", layout="wide") st.title("๐Ÿณ Recommended Recipes") st.caption("Discover recipes you can cook with what's already in your pantry!") -# ---------- Check username ---------- +# ---------- Check Username ---------- if "username" not in st.session_state or not st.session_state["username"]: st.warning("Please go to the Home page and enter your username first.") st.stop() username = st.session_state["username"] -USER_FILE = os.path.join( +user_file = os.path.join( "smart_pantry_manager", "data", f"pantry_{username.replace(' ', '_').lower()}.xlsx" ) -# ---------- Load pantry ---------- +# ---------- Load Pantry ---------- try: - pantry = pd.read_excel(USER_FILE) - # Normalize pantry product names - if "Product" in pantry.columns: - pantry["Product"] = pantry["Product"].astype(str).str.lower().str.strip() + pantry_df = pd.read_excel(user_file) + if "Product" in pantry_df.columns: + pantry_df["Product"] = pantry_df["Product"].astype(str).str.lower().str.strip() else: - pantry["Product"] = "" + pantry_df["Product"] = "" except FileNotFoundError: st.info("Your pantry is empty. Please add items on the Home page.") st.stop() -# remove duplicates and empty -pantry_products = sorted({p for p in pantry["Product"].tolist() if p and p.strip()}) - -# Build compiled regex patterns for strict whole-word matching -# e.g., pantry 'milk' -> regex '\bmilk\b' (case-insensitive) -pantry_regexes = [ - re.compile(rf"\b{re.escape(prod)}\b", flags=re.IGNORECASE) - for prod in pantry_products -] +# Remove duplicates and empty product names +pantry_products = sorted({p for p in pantry_df["Product"].tolist() if p and p.strip()}) -# ---------- Load recipes ---------- +# ---------- Load Recipes ---------- @st.cache_data def load_recipes() -> pd.DataFrame: + """ + Load recipes from SQLite and normalize columns. + Returns DataFrame with Recipe, Ingredients, Instructions + """ db_path = os.path.join("smart_pantry_manager", "data", "Recipe_Dataset.sqlite") if not os.path.exists(db_path): st.error( - "โš ๏ธ Recipes database not found. Please ensure Recipe_Dataset.sqlite is in smart_pantry_manager/data/." + "โš ๏ธ Recipes database not found. Ensure Recipe_Dataset.sqlite " + "is in smart_pantry_manager/data/." ) return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) + conn = sqlite3.connect(db_path) try: df = pd.read_sql_query("SELECT * FROM recipes", conn) - except Exception as e: - st.error(f"Error reading recipes: {e}") + except Exception as err: + st.error(f"Error reading recipes: {err}") conn.close() return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) conn.close() + # Normalize column names df.columns = [c.strip().lower() for c in df.columns] rename_map = { @@ -79,51 +87,44 @@ def load_recipes() -> pd.DataFrame: df.rename( columns={k: v for k, v in rename_map.items() if k in df.columns}, inplace=True ) - required_cols = ["Recipe", "Ingredients", "Instructions"] - for col in required_cols: + # Ensure required columns exist + for col in ["Recipe", "Ingredients", "Instructions"]: if col not in df.columns: df[col] = "" - # Keep required only - return df[required_cols] - + return df[["Recipe", "Ingredients", "Instructions"]] -recipes = load_recipes() -if recipes.empty: +recipes_df = load_recipes() +if recipes_df.empty: st.warning("No recipes available in the database.") st.stop() # ---------- Utilities ---------- def normalize_text(s: str) -> str: - """Normalize unicode artifacts and strip.""" + """Normalize unicode artifacts and strip spaces.""" if s is None: return "" - # fix weird combined characters from windows-1252/utf-8 issues s = str(s) s = unicodedata.normalize("NFKC", s) - # remove zero-width and weird control chars s = re.sub(r"[\u200b-\u200f\u2028\u2029]", "", s) return s.strip() def parse_ingredients(ingredients_str: str) -> List[str]: - """Parse ingredients stored as a Python list string into a list of cleaned ingredient strings.""" + """Parse ingredients string into a cleaned list of ingredients.""" if not ingredients_str: return [] s = normalize_text(ingredients_str) try: - # likely a list literal like "['1 cup milk', 'salt']" if s.startswith("[") and s.endswith("]"): parsed = ast.literal_eval(s) if isinstance(parsed, (list, tuple)): return [normalize_text(str(x)) for x in parsed if str(x).strip()] - # fallback: comma-separated if "," in s: - return [normalize_text(item) for item in s.split(",") if item.strip()] + return [normalize_text(x) for x in s.split(",") if x.strip()] return [s] except Exception: - # last-resort: try splitting by '|' or newline if "|" in s: return [normalize_text(x) for x in s.split("|") if x.strip()] if "\n" in s: @@ -133,38 +134,31 @@ def parse_ingredients(ingredients_str: str) -> List[str]: def strip_leading_qty(s: str) -> str: """ - Remove leading quantity & measurements to expose ingredient name for matching. - Example: '1 cup evaporated milk' -> 'evaporated milk' - This is intentionally conservative: we remove common leading patterns. + Remove leading quantity & units from ingredient to match pantry. + Example: '1 cup milk' -> 'milk' """ if not s: return "" s = s.lower() - # common patterns: numbers, fractions, parentheses, measurements at start - # remove leading parenthetical groups or leading numbers/fractions/measurements + s = re.sub(r"^\s*\(?\d+(?:[\/\u00BC-\u00BE\u2150-\u215E]?\d*)?\)?\s*", "", s) s = re.sub( - r"^\s*\(?\d+(?:[\/\u00BC-\u00BE\u2150-\u215E]?\d*)?\)?\s*", "", s - ) # leading numbers like "1", "1/2" - s = re.sub( - r"^\s*\d+(\.\d+)?\s*(cup|cups|tbsp|tbsp.|tbsps|tsp|tsp.|oz|lb|lbs|g|kg|ml|l)\b", + r"^\s*\d+(\.\d+)?\s*(cup|cups|tbsp|tbsp\.|tbsps|tsp|tsp\.|oz|lb|lbs|g|kg|ml|l)\b", "", s, ) - s = re.sub(r"^\s*(?:one|two|three|four|a|an)\s+", "", s) # words - # remove leftover leading measurement words + s = re.sub(r"^\s*(?:one|two|three|four|a|an)\s+", "", s) s = re.sub(r"^\s*\(?\d+[^a-zA-Z]*\)?\s*", "", s) - # strip extras s = re.sub(r"^[\-\โ€“\โ€”\s]+", "", s) return s.strip() -# Use st.cache_data for availability checks. -# Cache key will be the ingredients string and a tuple of pantry products (for hashing). @st.cache_data def cached_check_availability( recipe_ingredients: str, pantry_products_tuple: Tuple[str, ...] ) -> Tuple[float, List[str]]: - """Return (match_percent, missing_items_list) using strict whole-word matching.""" + """ + Return (match_percent, missing_items_list) using whole-word matching. + """ ingredients = parse_ingredients(recipe_ingredients) if not ingredients: return 0.0, [] @@ -173,7 +167,6 @@ def cached_check_availability( available_count = 0 missing_items = [] - # convert pantry regex list from pantry_products_tuple each call for safety regexes = [ re.compile(rf"\b{re.escape(p)}\b", flags=re.IGNORECASE) for p in pantry_products_tuple @@ -181,27 +174,17 @@ def cached_check_availability( for item in ingredients: item_norm = normalize_text(item).lower() - # strip leading qty to focus on name name_candidate = strip_leading_qty(item_norm) - # fallback to full item if strip produced emptiness text_to_search = name_candidate or item_norm - - matched = False - for rx in regexes: - if rx.search(text_to_search): - matched = True - break - + matched = any(rx.search(text_to_search) for rx in regexes) if matched: available_count += 1 else: - # record a cleaned short name as missing - # take last 3 words of the name candidate to make missing hint concise words = text_to_search.split() - short = " ".join(words[-3:]) if len(words) > 3 else " ".join(words) + short = " ".join(words[-3:] if len(words) > 3 else words) missing_items.append(short) - match_percentage = (available_count / total) * 100 if total > 0 else 0.0 + match_percentage = (available_count / total) * 100 if total else 0.0 return round(match_percentage, 1), missing_items @@ -219,26 +202,17 @@ def cached_check_availability( status_text = st.empty() results = [] -total_recipes = len(recipes) - -# Convert pantry_products to tuple for caching +total_recipes = len(recipes_df) pantry_key = tuple(pantry_products) -for idx, (_, row) in enumerate(recipes.iterrows()): - # Update progress (user requested not to throttle updates) - progress = (idx + 1) / total_recipes - progress_bar.progress(progress) +for idx, (_, row) in enumerate(recipes_df.iterrows()): + progress_bar.progress((idx + 1) / total_recipes) status_text.text(f"Processing recipe {idx + 1} of {total_recipes}...") - - # Safely get ingredients string ingredients_raw = normalize_text(row.get("Ingredients") or "") match_percent, missing = cached_check_availability(ingredients_raw, pantry_key) - if match_percent >= min_match: instr = normalize_text(row.get("Instructions") or "") - # Shorten instructions safely instr_preview = instr[:500] + "..." if len(instr) > 500 else instr - results.append( { "Recipe": row.get("Recipe") or "Unnamed Recipe", @@ -251,16 +225,14 @@ def cached_check_availability( } ) -# Clear progress indicators progress_bar.empty() status_text.empty() # Sort and limit results results_df = pd.DataFrame(results) if not results_df.empty: - results_df = results_df.sort_values(by="Match %", ascending=False).head( - int(max_recipes) - ) + results_df = results_df.sort_values(by="Match %", ascending=False) + results_df = results_df.head(int(max_recipes)) st.success(f"โœ… Found {len(results_df)} matching recipes!") st.write("### ๐Ÿ“‹ Recipe Match Overview") @@ -276,14 +248,12 @@ def cached_check_availability( "๐ŸŸข" if row["Match %"] >= 80 else "๐ŸŸก" if row["Match %"] >= 60 else "๐ŸŸ " ) with st.expander(f"{match_color} {row['Recipe']} โ€” {row['Match %']}% match"): - col1, col2 = st.columns([1, 2]) - with col1: + c1, c2 = st.columns([1, 2]) + with c1: st.markdown(f"**Match:** {row['Match %']}%") st.markdown(f"**Missing:** {row['Missing']}") - - # Ingredients: parse and display safely ing_list = parse_ingredients(row["Ingredients"] or "") - with col2: + with c2: st.markdown("**๐Ÿง‚ Ingredients:**") if ing_list: for ing in ing_list[:10]: @@ -292,10 +262,10 @@ def cached_check_availability( st.write(f"*...and {len(ing_list) - 10} more*") else: st.write("No ingredient data available.") - - st.markdown("**๐Ÿ‘ฉโ€๐Ÿณ Instructions:**") - st.write(row["Instructions"] or "No instructions available.") + st.markdown("**๐Ÿ‘ฉโ€๐Ÿณ Instructions:**") + st.write(row["Instructions"] or "No instructions available.") else: st.info( - f"No recipes found with at least {min_match}% match. Try lowering the minimum match percentage." + f"No recipes found with at least {min_match}% match. " + "Try lowering the minimum match percentage." ) diff --git a/smart_pantry_manager/pages/2- all_recipes.py b/smart_pantry_manager/pages/2- all_recipes.py new file mode 100644 index 0000000..dd17770 --- /dev/null +++ b/smart_pantry_manager/pages/2- all_recipes.py @@ -0,0 +1,153 @@ +""" +all_recipes.py +All Recipes Page for Smart Pantry Application (SQLite version) + +Features: +- Load recipes from SQLite database +- Display recipe list with ingredients and instructions +- Supports search/filtering + +Date: 2025-11-27 +""" + +import ast +import os +import sqlite3 + +import pandas as pd +import streamlit as st + +# ---------- Page Setup ---------- +st.set_page_config(page_title="All Recipes", page_icon="๐Ÿ“œ", layout="wide") + +st.title("๐Ÿ“œ All Recipes") +st.caption("Browse all available recipes in the Smart Pantry system.") + + +# ---------- Load Recipes ---------- +@st.cache_data +def load_recipes() -> pd.DataFrame: + """ + Load recipes from SQLite database and normalize column names. + + Returns: + pd.DataFrame: Columns = Recipe, Ingredients, Instructions + """ + db_path = "smart_pantry_manager/data/Recipe_Dataset.sqlite" + if not os.path.exists(db_path): + st.error(f"โŒ Database file not found at: {db_path}") + return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) + + conn = sqlite3.connect(db_path) + try: + df = pd.read_sql_query("SELECT * FROM recipes", conn) + except Exception as err: + st.error(f"Error reading database: {err}") + conn.close() + return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) + conn.close() + + # Normalize column names + df.columns = [c.strip().lower() for c in df.columns] + + rename_map = { + "title": "Recipe", + "cleaned_ingredients": "Ingredients", + "instruction": "Instructions", + "instructions": "Instructions", + } + df.rename( + columns={k: v for k, v in rename_map.items() if k in df.columns}, inplace=True + ) + + # Keep only required columns + required_cols = ["Recipe", "Ingredients", "Instructions"] + df = df[[col for col in required_cols if col in df.columns]] + return df + + +def format_ingredients(ingredients_str: str) -> list: + """ + Convert ingredients string into a list. + + Args: + ingredients_str (str): String representation of ingredients + + Returns: + list: List of ingredients as strings + """ + if not ingredients_str: + return [] + + s = ingredients_str.strip() + try: + if s.startswith("[") and s.endswith("]"): + parsed = ast.literal_eval(s) + if isinstance(parsed, (list, tuple)): + return [str(x).strip() for x in parsed if str(x).strip()] + if "," in s: + return [item.strip() for item in s.split(",") if item.strip()] + return [s] + except Exception: + # fallback for | or newline-separated strings + if "|" in s: + return [x.strip() for x in s.split("|") if x.strip()] + if "\n" in s: + return [x.strip() for x in s.split("\n") if x.strip()] + return [s] + + +recipes_df = load_recipes() + +# ---------- Search with suggestions ---------- +# text input for free keywords +search_term = st.text_input("๐Ÿ” Search for a recipe:") + +selected_recipe = None + +# build suggestion list when user types something +if search_term: + suggestion_list = ( + recipes_df[recipes_df["Recipe"].str.contains(search_term, case=False, na=False)] + .sort_values("Recipe")["Recipe"] + .tolist() + ) + # Show suggestions as clickable buttons instead of selectbox + for recipe in suggestion_list: + if st.button(f"๐Ÿ“„ {recipe}"): + selected_recipe = recipe + break # Stop after clicking one + +# Filter results +if selected_recipe: + filtered_df = recipes_df[recipes_df["Recipe"] == selected_recipe] +elif search_term: + filtered_df = recipes_df[ + recipes_df["Recipe"].str.contains(search_term, case=False, na=False) + ] +else: + filtered_df = recipes_df + +# show only the selected recipe OR search results +if selected_recipe and selected_recipe != "": + filtered_df = recipes_df[recipes_df["Recipe"] == selected_recipe] +else: + filtered_df = ( + recipes_df[recipes_df["Recipe"].str.contains(search_term, case=False, na=False)] + if search_term + else recipes_df + ) + +# ---------- Display Recipes ---------- +if filtered_df.empty: + st.info("No recipes found.") +else: + for _, row in filtered_df.iterrows(): + with st.expander(f"๐Ÿ“– {row['Recipe']}"): + st.markdown("**๐Ÿง‚ Ingredients:**") + ing_list = format_ingredients(row.get("Ingredients") or "") + for ing in ing_list: + st.write(f"โ€ข {ing}") + + st.markdown("**๐Ÿ‘ฉโ€๐Ÿณ Instructions:**") + st.write(row.get("Instructions") or "No instructions available.") diff --git a/smart_pantry_manager/pages/all_recipes.py b/smart_pantry_manager/pages/all_recipes.py deleted file mode 100644 index 85aa1e2..0000000 --- a/smart_pantry_manager/pages/all_recipes.py +++ /dev/null @@ -1,106 +0,0 @@ -# spell-checker: disable -""" -All Recipes Page for Smart Pantry Application (SQLite version) -""" - -import ast -import os -import sqlite3 - -import pandas as pd -import streamlit as st - -st.set_page_config(page_title="All Recipes", page_icon="๐Ÿ“œ", layout="wide") - -st.title("๐Ÿ“œ All Recipes") -st.caption("Browse all available recipes in the Smart Pantry system.") - - -# ---------- Load recipes from SQLite ---------- -@st.cache_data -def load_recipes(): - """ - Load recipes from the SQLite database and normalize column names. - Returns a DataFrame with columns: Recipe, Ingredients, Instructions - """ - db_path = "smart_pantry_manager/data/Recipe_Dataset.sqlite" - - # Check if database file exists - if not os.path.exists(db_path): - st.error(f"โŒ Database file not found at: {db_path}") - return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) - - # Connect to SQLite database - conn = sqlite3.connect(db_path) - - try: - # Load the table "recipes" - df = pd.read_sql_query("SELECT * FROM recipes", conn) - except Exception as e: - st.error(f"Error reading database: {e}") - conn.close() - return pd.DataFrame(columns=["Recipe", "Ingredients", "Instructions"]) - - conn.close() - - # Normalize column names - df.columns = [c.strip().lower() for c in df.columns] - - # Rename columns if they exist - rename_map = { - "title": "Recipe", - "cleaned_ingredients": "Ingredients", - "instruction": "Instructions", - "instructions": "Instructions", - } - df.rename( - columns={k: v for k, v in rename_map.items() if k in df.columns}, inplace=True - ) - - # Keep only required columns - required_cols = ["Recipe", "Ingredients", "Instructions"] - df = df[[col for col in required_cols if col in df.columns]] - - return df - - -def format_ingredients(ingredients_str): - """ - Convert ingredients from string representation of list to a Python list. - """ - try: - # Try to parse as a Python list - if ingredients_str.startswith("["): - return ast.literal_eval(ingredients_str) - # If it's comma-separated - elif "," in ingredients_str: - return [item.strip() for item in ingredients_str.split(",")] - else: - return [ingredients_str] - except: - # If parsing fails, return as single item list - return [ingredients_str] - - -recipes = load_recipes() - -# ---------- Display recipes ---------- -if recipes.empty: - st.info("No recipes found.") -else: - search = st.text_input("๐Ÿ” Search for a recipe:") - filtered = ( - recipes[recipes["Recipe"].str.contains(search, case=False, na=False)] - if search - else recipes - ) - - for _, row in filtered.iterrows(): - with st.expander(f"๐Ÿ“– {row['Recipe']}"): - st.markdown("**๐Ÿง‚ Ingredients:**") - ingredients_list = format_ingredients(row["Ingredients"]) - for ingredient in ingredients_list: - st.write(f"โ€ข {ingredient}") - - st.markdown("**๐Ÿ‘ฉโ€๐Ÿณ Instructions:**") - st.write(row["Instructions"]) diff --git a/smart_pantry_manager/smart_pantry.py b/smart_pantry_manager/smart_pantry.py index 58f35ac..b92729f 100644 --- a/smart_pantry_manager/smart_pantry.py +++ b/smart_pantry_manager/smart_pantry.py @@ -1,16 +1,16 @@ -# spell-checker: disable """ smart_pantry.py Smart Pantry Web App (Home Page Only) Features: -- Each user has a personal pantry (saved to Excel) -- Add, edit, and track products with expiry alerts -- Quantity + Unit input (supports numeric + count) -- Small loading animation when saving -- Optional intro demo video +- Personal pantry for each user, stored in Excel +- Add, edit, and track products with automatic expiry alerts +- Support for numeric quantities and units (e.g., g, kg, ml, count) +- Visual indicators for soon-to-expire and expired items +- Optional loading animation when saving +- Optional demo video to guide new users -Date: 29/10/2025 +Date: 27/11/2025 """ import os @@ -20,10 +20,29 @@ import pandas as pd import streamlit as st + +# ----- Styling Functions ----- +def style_expired(expired_products_df): + return expired_products_df.style.set_properties( + **{ + "background-color": "#EB4343", # Light Red + "color": "black", + } + ) + + +def style_expiring_soon(expiring_products_df): + return expiring_products_df.style.set_properties( + **{ + "background-color": "#F5DB76", # Light Yellow + "color": "black", + } + ) + + # ---------- Page Setup ---------- st.set_page_config(page_title="Smart Pantry Manager", page_icon="๐Ÿงบ", layout="centered") - st.title("๐Ÿ“ฑ Smart Pantry Manager ๐Ÿ“Š") st.subheader("Track your pantry items and discover what you can cook ๐Ÿ‘‡") @@ -35,7 +54,6 @@ st.warning("Please enter your name to start using the app.") st.stop() -# Store username globally for use in other pages st.session_state["username"] = username # Create user-specific file path @@ -48,15 +66,16 @@ # ---------- Load Pantry ---------- @st.cache_data def load_pantry(file_path): - """Load pantry data for a specific user or create an empty table.""" + """Load pantry data for a specific user or create empty table.""" try: df = pd.read_excel(file_path) df["Expiry Date"] = pd.to_datetime(df["Expiry Date"], errors="coerce") df["Days Left"] = (df["Expiry Date"] - datetime.now()).dt.days - # ๐Ÿงน Auto-remove expired items - df = df[df["Days Left"] >= 0].reset_index(drop=True) + # Remove expired items automatically + # df = df[df["Days Left"] >= 0].reset_index(drop=True) return df + except FileNotFoundError: return pd.DataFrame( columns=[ @@ -70,13 +89,17 @@ def load_pantry(file_path): ) -data = load_pantry(USER_FILE) +pantry_df = load_pantry(USER_FILE) + +# Store in session_state +if "pantry_data" not in st.session_state: + st.session_state["pantry_data"] = pantry_df # ---------- Add New Product ---------- st.header("โž• Add a New Product") -product = st.text_input("Product name:") -category = st.selectbox( +product_name = st.text_input("Product name:") +product_category = st.selectbox( "Category:", [ "Uncategorized", @@ -94,53 +117,128 @@ def load_pantry(file_path): "Frozen Foods", "Canned Goods", "Spices & Seasonings", + "Milks & Alternatives", "Drinks", ], ) -quantity = st.number_input("Quantity (numeric or count):", min_value=0.0, step=0.1) -unit = st.selectbox("Unit:", ["count", "g", "kg", "ml", "L", "cup", "tbsp", "tsp"]) -expiry = st.date_input("Expiry date:") +product_quantity = st.number_input( + "Quantity (numeric or count):", min_value=0.0, step=0.1 +) +product_unit = st.selectbox( + "Unit:", ["count", "g", "kg", "ml", "L", "cup", "tbsp", "tsp"] +) +expiry_date = st.date_input("Expiry date:") if st.button("๐Ÿ’พ Save product"): - if product: + if product_name: with st.spinner("๐Ÿ’พ Saving product... please wait..."): - time.sleep(2) # simulate loading + time.sleep(2) today = datetime.now().date() - days_left = (expiry - today).days - new_row = { - "Product": product, - "Category": category, - "Quantity": quantity, - "Unit": unit, - "Expiry Date": expiry, + days_left = (expiry_date - today).days + new_product = { + "Product": product_name, + "Category": product_category, + "Quantity": product_quantity, + "Unit": product_unit, + "Expiry Date": expiry_date, "Days Left": days_left, } - data = pd.concat([data, pd.DataFrame([new_row])], ignore_index=True) - data.to_excel(USER_FILE, index=False) + pantry_df = st.session_state["pantry_data"] + pantry_df = pd.concat( + [pantry_df, pd.DataFrame([new_product])], ignore_index=True + ) + + # Sort by Days Left + pantry_df = pantry_df.sort_values( + by="Days Left", ascending=True + ).reset_index(drop=True) + pantry_df.to_excel(USER_FILE, index=False) + st.session_state["pantry_data"] = pantry_df st.cache_data.clear() - st.success(f"โœ… {product} added successfully!") + st.success(f"โœ… {product_name} added successfully!") else: st.warning("Please enter a product name.") # ---------- Update Days Left ---------- -if not data.empty: - data["Expiry Date"] = pd.to_datetime(data["Expiry Date"], errors="coerce") +if not st.session_state["pantry_data"].empty: + pantry_df = st.session_state["pantry_data"] + pantry_df["Expiry Date"] = pd.to_datetime(pantry_df["Expiry Date"], errors="coerce") today = pd.Timestamp(datetime.now().date()) - data["Days Left"] = (data["Expiry Date"] - today).dt.days + pantry_df["Days Left"] = (pantry_df["Expiry Date"] - today).dt.days + st.session_state["pantry_data"] = pantry_df # ---------- Alerts ---------- -st.header("โš ๏ธ Expiry Alerts") -if not data.empty: - expired = data[data["Days Left"] < 0] - expiring_soon = data[(data["Days Left"] >= 0) & (data["Days Left"] <= 3)] +st.markdown("### โš ๏ธ Expiry Alerts") - if not expired.empty: - st.error("โŒ Some products have expired:") - st.table(expired[["Product", "Expiry Date", "Days Left"]]) +current_df = st.session_state["pantry_data"] # renamed - if not expiring_soon.empty: - st.warning("โฐ Some products are expiring soon:") - st.table(expiring_soon[["Product", "Expiry Date", "Days Left"]]) +if not current_df.empty: + expired_items = current_df[current_df["Days Left"] < 0].copy() # renamed + expired_items = expired_items.sort_values(by="Days Left") + expiring_items = current_df[ + (current_df["Days Left"] >= 0) & (current_df["Days Left"] <= 3) + ].copy() # renamed + + # ---------- Expired Products ---------- + if not expired_items.empty: + with st.expander("โŒ Expired Products (Click to view)", expanded=True): + st.dataframe( + style_expired(expired_items[["Product", "Expiry Date", "Days Left"]]), + use_container_width=True, + ) + col1, col2 = st.columns([2, 1]) + if col2.button("๐Ÿ—‘๏ธ Delete ALL expired"): + current_df = current_df[current_df["Days Left"] >= 0] + st.session_state["pantry_data"] = current_df + current_df.to_excel(USER_FILE, index=False) + + st.success("All expired products deleted!") + + # ---------- Expiring Soon ---------- + if not expiring_items.empty: + with st.expander("โฐ Expiring Soon (Within 3 days)", expanded=True): + st.dataframe( + style_expiring_soon( + expiring_items[["Product", "Expiry Date", "Days Left"]] + ), + use_container_width=True, + ) + + # ---------- Manage Section ---------- + st.markdown("---") + st.markdown("### ๐Ÿ›  Manage Products") + + colA, colB = st.columns([2, 3]) + selected_item = colA.selectbox("Choose a product:", current_df["Product"].unique()) + + if selected_item: + item_row = current_df[current_df["Product"] == selected_item].iloc[0] + colB.markdown(f"**Current:** {item_row['Quantity']} {item_row['Unit']}") + + new_quantity = colB.number_input( + "New quantity:", + min_value=0.0, + max_value=float(item_row["Quantity"]), + value=float(item_row["Quantity"]), + step=0.1, + ) + + col1, col2 = st.columns([1, 1]) + if col1.button("โœ”๏ธ Update Quantity"): + current_df.loc[current_df["Product"] == selected_item, "Quantity"] = ( + new_quantity + ) + st.session_state["pantry_data"] = current_df + current_df.to_excel(USER_FILE, index=False) + st.success("Quantity updated!") + + if col2.button("๐Ÿ—‘๏ธ Delete Product"): + current_df = current_df[current_df["Product"] != selected_item] + st.session_state["pantry_data"] = current_df + current_df.to_excel(USER_FILE, index=False) + st.success(f"{selected_item} deleted!") + +st.markdown("---") # ---------- Pantry Table ---------- st.header("๐Ÿ“ฆ Your Pantry Items") @@ -157,8 +255,9 @@ def color_days(val): return f"background-color: {color}; color: black;" -if not data.empty: - styled_data = data.reset_index(drop=True).style.applymap( +if not pantry_df.empty: + display_data = pantry_df.sort_values(by="Days Left", ascending=True) + styled_data = display_data.reset_index(drop=True).style.applymap( color_days, subset=["Days Left"] ) st.dataframe(styled_data, use_container_width=True) @@ -167,5 +266,10 @@ def color_days(val): # ---------- Manual Save ---------- if st.button("๐Ÿ”„ Save Changes"): - data.to_excel(USER_FILE, index=False) - st.success("Pantry data saved successfully!") + if not pantry_df.empty: + pantry_df_sorted = pantry_df.sort_values(by="Days Left", ascending=True) + pantry_df_sorted.to_excel(USER_FILE, index=False) + st.session_state["pantry_data"] = pantry_df_sorted + st.success("Pantry data saved successfully (sorted by expiry)!") + else: + st.info("No items to save.")