Spaces:
Runtime error
Runtime error
Download app.py from sulehree/ExpenseManager: direct link, hf CLI and curl.
- Browser
- Download file 41.9 kB
-
https://huggingface.co/spaces/sulehree/ExpenseManager/resolve/main/app.py
- Command line
-
hf download hf://spaces/sulehree/ExpenseManager/app.py
-
curl -L -o app.py https://huggingface.co/spaces/sulehree/ExpenseManager/resolve/main/app.py
41.9 kB
| import streamlit as st | |
| import pandas as pd | |
| from datetime import date | |
| st.set_page_config(page_title="Family Expense Manager", page_icon="💰", layout="wide") | |
| # ---------------------------- | |
| # UI styling | |
| # ---------------------------- | |
| st.markdown( | |
| ''' | |
| <style> | |
| div[data-testid="stDataEditor"] button { | |
| background: #1f6feb !important; | |
| color: white !important; | |
| border: 1px solid #1f6feb !important; | |
| opacity: 1 !important; | |
| } | |
| div[data-testid="stDataEditor"] button:hover { | |
| background: #1557b0 !important; | |
| color: white !important; | |
| } | |
| div[data-testid="stDataEditor"] [role="gridcell"]:focus, | |
| div[data-testid="stDataEditor"] [role="gridcell"]:focus-within { | |
| background-color: rgba(31, 111, 235, 0.08) !important; | |
| outline: 2px solid #1f6feb !important; | |
| } | |
| </style> | |
| ''', | |
| unsafe_allow_html=True | |
| ) | |
| # ---------------------------- | |
| # Session state initialization | |
| # ---------------------------- | |
| if "member_df" not in st.session_state: | |
| st.session_state.member_df = pd.DataFrame([ | |
| {"Name": "Abbas", "Family Position": "Father"}, | |
| {"Name": "Sara", "Family Position": "Mother"}, | |
| {"Name": "Bilal", "Family Position": "Son"}, | |
| {"Name": "Ayesha", "Family Position": "Daughter"}, | |
| ]) | |
| if "income_sources" not in st.session_state: | |
| st.session_state.income_sources = ["Salary", "Business", "Freelance", "Bonus", "Rent Income", "Other"] | |
| if "category_map" not in st.session_state: | |
| st.session_state.category_map = { | |
| "Groceries": ["Vegetables", "Meat", "General Store"], | |
| "Utilities": ["Electricity", "Gas", "Internet", "Water"], | |
| "Rent": ["House Rent"], | |
| "School Fees": ["Tuition Fee", "Books", "Transport Fee"], | |
| "Transport": ["Fuel", "Ride Fare", "Maintenance"], | |
| "Medical": ["Medicine", "Doctor", "Lab Tests"], | |
| "Entertainment": ["Family Dinner", "Movies", "Outing"], | |
| "Household": ["Cleaning", "Maintenance", "Furniture"], | |
| "Other": ["Miscellaneous"], | |
| } | |
| if "income_df" not in st.session_state: | |
| st.session_state.income_df = pd.DataFrame(columns=["ID", "Date", "Member", "Source", "Amount", "Notes"]) | |
| if "expense_df" not in st.session_state: | |
| st.session_state.expense_df = pd.DataFrame(columns=[ | |
| "ID", "Date", "Expense Type", "Paid By", "Participants", | |
| "Category", "Subcategory", "Amount", "Description", "Month" | |
| ]) | |
| if "budget_df" not in st.session_state: | |
| st.session_state.budget_df = pd.DataFrame([ | |
| {"Category": "Groceries", "Monthly Budget": 30000}, | |
| {"Category": "Utilities", "Monthly Budget": 15000}, | |
| {"Category": "Rent", "Monthly Budget": 50000}, | |
| {"Category": "School Fees", "Monthly Budget": 20000}, | |
| {"Category": "Transport", "Monthly Budget": 10000}, | |
| {"Category": "Medical", "Monthly Budget": 10000}, | |
| {"Category": "Entertainment", "Monthly Budget": 8000}, | |
| {"Category": "Household", "Monthly Budget": 12000}, | |
| {"Category": "Other", "Monthly Budget": 10000}, | |
| ]) | |
| if "dev_notes_df" not in st.session_state: | |
| st.session_state.dev_notes_df = pd.DataFrame(columns=["ID", "Date", "Type", "Title", "Details", "Status"]) | |
| if "next_income_id" not in st.session_state: | |
| st.session_state.next_income_id = 1 | |
| if "next_expense_id" not in st.session_state: | |
| st.session_state.next_expense_id = 1 | |
| if "next_note_id" not in st.session_state: | |
| st.session_state.next_note_id = 1 | |
| # ---------------------------- | |
| # Helpers | |
| # ---------------------------- | |
| def normalize_month(d): | |
| return pd.to_datetime(d).strftime("%Y-%m") | |
| def get_member_names(): | |
| if st.session_state.member_df.empty: | |
| return [] | |
| return st.session_state.member_df["Name"].tolist() | |
| def add_member(name, family_position): | |
| name = name.strip() | |
| family_position = family_position.strip() | |
| if not name or not family_position: | |
| return False, "Enter both name and family position." | |
| if name in get_member_names(): | |
| return False, "This member already exists." | |
| new_row = pd.DataFrame([{ | |
| "Name": name, | |
| "Family Position": family_position | |
| }]) | |
| st.session_state.member_df = pd.concat( | |
| [st.session_state.member_df, new_row], | |
| ignore_index=True | |
| ) | |
| return True, f"{name} added successfully." | |
| def dataframe_to_csv_download(df): | |
| return df.to_csv(index=False).encode("utf-8") | |
| def load_uploaded_csv(uploaded_file, expected_cols): | |
| df = pd.read_csv(uploaded_file) | |
| missing = [c for c in expected_cols if c not in df.columns] | |
| if missing: | |
| raise ValueError(f"Missing columns: {', '.join(missing)}") | |
| return df[expected_cols] | |
| def format_df(df): | |
| config = {} | |
| if "Amount" in df.columns: | |
| config["Amount"] = st.column_config.NumberColumn(format="%,d") | |
| if "Monthly Budget" in df.columns: | |
| config["Monthly Budget"] = st.column_config.NumberColumn(format="%,d") | |
| if "Spent" in df.columns: | |
| config["Spent"] = st.column_config.NumberColumn(format="%,d") | |
| if "Remaining" in df.columns: | |
| config["Remaining"] = st.column_config.NumberColumn(format="%,d") | |
| return config | |
| # ---------------------------- | |
| # Sidebar | |
| # ---------------------------- | |
| st.sidebar.title("💰 Family Expense Manager") | |
| page = st.sidebar.radio( | |
| "Go to", | |
| ["Dashboard", "Add Income", "Add Expense", "Monthly Budget", "Masters", "Reports", "Development Notes", "Data Backup"] | |
| ) | |
| st.sidebar.markdown("---") | |
| new_member_name = st.sidebar.text_input("Member Name") | |
| new_member_position = st.sidebar.selectbox( | |
| "Family Position", | |
| ["Father", "Mother", "Son", "Daughter", "Brother", "Sister", "Grandfather", "Grandmother", "Other"] | |
| ) | |
| if st.sidebar.button("Add Member"): | |
| success, message = add_member(new_member_name, new_member_position) | |
| if success: | |
| st.sidebar.success(message) | |
| else: | |
| st.sidebar.warning(message) | |
| st.sidebar.write("**Members:**") | |
| if not st.session_state.member_df.empty: | |
| for _, row in st.session_state.member_df.iterrows(): | |
| st.sidebar.write(f"• {row['Name']} ({row['Family Position']})") | |
| else: | |
| st.sidebar.write("No members added yet.") | |
| # ---------------------------- | |
| # Dashboard | |
| # ---------------------------- | |
| if page == "Dashboard": | |
| st.title("🏠 Family Expense Dashboard") | |
| income_df = st.session_state.income_df.copy() | |
| expense_df = st.session_state.expense_df.copy() | |
| budget_df = st.session_state.budget_df.copy() | |
| total_income = int(income_df["Amount"].sum()) if not income_df.empty else 0 | |
| total_expense = int(expense_df["Amount"].sum()) if not expense_df.empty else 0 | |
| balance = total_income - total_expense | |
| c1, c2, c3 = st.columns(3) | |
| c1.metric("Total Income", f"Rs. {total_income:,}") | |
| c2.metric("Total Expenses", f"Rs. {total_expense:,}") | |
| c3.metric("Net Balance", f"Rs. {balance:,}") | |
| st.markdown("### This Month Snapshot") | |
| current_month = date.today().strftime("%Y-%m") | |
| month_income = int(income_df[income_df["Date"].astype(str).str[:7] == current_month]["Amount"].sum()) if not income_df.empty else 0 | |
| month_expense = int(expense_df[expense_df["Month"] == current_month]["Amount"].sum()) if not expense_df.empty else 0 | |
| month_budget = int(budget_df["Monthly Budget"].sum()) if not budget_df.empty else 0 | |
| c4, c5, c6 = st.columns(3) | |
| c4.metric("This Month Income", f"Rs. {month_income:,}") | |
| c5.metric("This Month Expense", f"Rs. {month_expense:,}") | |
| c6.metric("Monthly Budget", f"Rs. {month_budget:,}") | |
| st.markdown("### Family Members") | |
| st.dataframe(st.session_state.member_df, use_container_width=True, hide_index=True) | |
| st.markdown("### Category Wise Expense") | |
| if not expense_df.empty: | |
| by_category = expense_df.groupby("Category", as_index=False)["Amount"].sum().sort_values("Amount", ascending=False) | |
| st.bar_chart(by_category.set_index("Category")) | |
| st.dataframe(by_category, use_container_width=True, hide_index=True, column_config=format_df(by_category)) | |
| else: | |
| st.info("No expenses added yet.") | |
| st.markdown("### Expense by Family Member") | |
| if not expense_df.empty: | |
| by_member = expense_df.groupby("Paid By", as_index=False)["Amount"].sum().sort_values("Amount", ascending=False) | |
| st.bar_chart(by_member.set_index("Paid By")) | |
| st.dataframe(by_member, use_container_width=True, hide_index=True, column_config=format_df(by_member)) | |
| else: | |
| st.info("No member expense data yet.") | |
| st.markdown("### Combined vs Individual Expense") | |
| if not expense_df.empty: | |
| by_type = expense_df.groupby("Expense Type", as_index=False)["Amount"].sum() | |
| st.bar_chart(by_type.set_index("Expense Type")) | |
| st.dataframe(by_type, use_container_width=True, hide_index=True, column_config=format_df(by_type)) | |
| # ---------------------------- | |
| # Add Income | |
| # ---------------------------- | |
| elif page == "Add Income": | |
| st.title("➕ Add Income") | |
| with st.form("income_form", clear_on_submit=True): | |
| income_date = st.date_input("Date", value=date.today()) | |
| member = st.selectbox("Member", get_member_names()) | |
| source = st.selectbox("Income Source", st.session_state.income_sources) | |
| amount = st.number_input("Amount", min_value=0, step=100, format="%d") | |
| notes = st.text_area("Notes") | |
| submit_income = st.form_submit_button("Save Income") | |
| if submit_income: | |
| if amount <= 0: | |
| st.error("Amount must be greater than zero.") | |
| else: | |
| new_row = pd.DataFrame([{ | |
| "ID": st.session_state.next_income_id, | |
| "Date": str(income_date), | |
| "Member": member, | |
| "Source": source, | |
| "Amount": int(amount), | |
| "Notes": notes | |
| }]) | |
| st.session_state.income_df = pd.concat([st.session_state.income_df, new_row], ignore_index=True) | |
| st.session_state.next_income_id += 1 | |
| st.success("Income added successfully.") | |
| st.markdown("### Income Records") | |
| st.dataframe(st.session_state.income_df, use_container_width=True, hide_index=True, column_config=format_df(st.session_state.income_df)) | |
| if not st.session_state.income_df.empty: | |
| st.markdown("### Edit or Delete Income") | |
| selected_income_id = st.selectbox("Select Income Record", st.session_state.income_df["ID"].tolist()) | |
| selected_income = st.session_state.income_df[st.session_state.income_df["ID"] == selected_income_id].iloc[0] | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| with st.form("edit_income_form"): | |
| edit_income_date = st.date_input("Edit Date", value=pd.to_datetime(selected_income["Date"]).date()) | |
| member_names = get_member_names() | |
| member_index = member_names.index(selected_income["Member"]) if selected_income["Member"] in member_names else 0 | |
| edit_member = st.selectbox("Edit Member", member_names, index=member_index) | |
| source_index = st.session_state.income_sources.index(selected_income["Source"]) if selected_income["Source"] in st.session_state.income_sources else 0 | |
| edit_source = st.selectbox("Edit Income Source", st.session_state.income_sources, index=source_index) | |
| edit_amount = st.number_input("Edit Amount", min_value=0, step=100, format="%d", value=int(selected_income["Amount"])) | |
| edit_notes = st.text_area("Edit Notes", value=selected_income["Notes"]) | |
| update_income = st.form_submit_button("Update Income") | |
| if update_income: | |
| idx = st.session_state.income_df.index[st.session_state.income_df["ID"] == selected_income_id][0] | |
| st.session_state.income_df.at[idx, "Date"] = str(edit_income_date) | |
| st.session_state.income_df.at[idx, "Member"] = edit_member | |
| st.session_state.income_df.at[idx, "Source"] = edit_source | |
| st.session_state.income_df.at[idx, "Amount"] = int(edit_amount) | |
| st.session_state.income_df.at[idx, "Notes"] = edit_notes | |
| st.success("Income updated successfully.") | |
| with col2: | |
| st.write("") | |
| st.write("") | |
| if st.button("Delete Income", type="primary"): | |
| st.session_state.income_df = st.session_state.income_df[st.session_state.income_df["ID"] != selected_income_id].reset_index(drop=True) | |
| st.success("Income deleted successfully.") | |
| st.rerun() | |
| # ---------------------------- | |
| # Add Expense | |
| # ---------------------------- | |
| elif page == "Add Expense": | |
| st.title("🧾 Add Expense") | |
| categories = list(st.session_state.category_map.keys()) | |
| member_names = get_member_names() | |
| with st.form("expense_form", clear_on_submit=True): | |
| exp_date = st.date_input("Date", value=date.today()) | |
| expense_type = st.radio("Expense Type", ["Individual", "Combined"], horizontal=True) | |
| paid_by = st.selectbox("Paid By", member_names) | |
| default_participants = [paid_by] if expense_type == "Individual" else member_names | |
| participants = st.multiselect("Participants", member_names, default=default_participants) | |
| category = st.selectbox("Category", categories) | |
| subcategory = st.selectbox("Subcategory", st.session_state.category_map.get(category, ["Miscellaneous"])) | |
| amount = st.number_input("Amount", min_value=0, step=100, format="%d") | |
| description = st.text_area("Description") | |
| submit_expense = st.form_submit_button("Save Expense") | |
| if submit_expense: | |
| if amount <= 0: | |
| st.error("Amount must be greater than zero.") | |
| elif expense_type == "Combined" and not participants: | |
| st.error("Please select at least one participant for combined expense.") | |
| else: | |
| if expense_type == "Individual": | |
| participants = [paid_by] | |
| new_row = pd.DataFrame([{ | |
| "ID": st.session_state.next_expense_id, | |
| "Date": str(exp_date), | |
| "Expense Type": expense_type, | |
| "Paid By": paid_by, | |
| "Participants": ", ".join(participants), | |
| "Category": category, | |
| "Subcategory": subcategory, | |
| "Amount": int(amount), | |
| "Description": description, | |
| "Month": normalize_month(exp_date) | |
| }]) | |
| st.session_state.expense_df = pd.concat([st.session_state.expense_df, new_row], ignore_index=True) | |
| st.session_state.next_expense_id += 1 | |
| st.success("Expense added successfully.") | |
| st.markdown("### Expense Records") | |
| st.dataframe(st.session_state.expense_df, use_container_width=True, hide_index=True, column_config=format_df(st.session_state.expense_df)) | |
| if not st.session_state.expense_df.empty: | |
| st.markdown("### Edit or Delete Expense") | |
| selected_expense_id = st.selectbox("Select Expense Record", st.session_state.expense_df["ID"].tolist()) | |
| selected_expense = st.session_state.expense_df[st.session_state.expense_df["ID"] == selected_expense_id].iloc[0] | |
| existing_participants = [p.strip() for p in str(selected_expense["Participants"]).split(",") if p.strip()] | |
| category_index = categories.index(selected_expense["Category"]) if selected_expense["Category"] in categories else 0 | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| with st.form("edit_expense_form"): | |
| edit_exp_date = st.date_input("Edit Date", value=pd.to_datetime(selected_expense["Date"]).date()) | |
| edit_expense_type = st.radio( | |
| "Edit Expense Type", | |
| ["Individual", "Combined"], | |
| index=0 if selected_expense["Expense Type"] == "Individual" else 1, | |
| horizontal=True | |
| ) | |
| paid_by_index = member_names.index(selected_expense["Paid By"]) if selected_expense["Paid By"] in member_names else 0 | |
| edit_paid_by = st.selectbox("Edit Paid By", member_names, index=paid_by_index) | |
| edit_participants = st.multiselect("Edit Participants", member_names, default=existing_participants) | |
| edit_category = st.selectbox("Edit Category", categories, index=category_index) | |
| subcategory_options = st.session_state.category_map.get(edit_category, ["Miscellaneous"]) | |
| sub_index = subcategory_options.index(selected_expense["Subcategory"]) if selected_expense["Subcategory"] in subcategory_options else 0 | |
| edit_subcategory = st.selectbox("Edit Subcategory", subcategory_options, index=sub_index) | |
| edit_amount = st.number_input("Edit Amount", min_value=0, step=100, format="%d", value=int(selected_expense["Amount"])) | |
| edit_description = st.text_area("Edit Description", value=selected_expense["Description"]) | |
| update_expense = st.form_submit_button("Update Expense") | |
| if update_expense: | |
| if edit_expense_type == "Combined" and not edit_participants: | |
| st.error("Please select at least one participant for combined expense.") | |
| else: | |
| if edit_expense_type == "Individual": | |
| edit_participants = [edit_paid_by] | |
| idx = st.session_state.expense_df.index[st.session_state.expense_df["ID"] == selected_expense_id][0] | |
| st.session_state.expense_df.at[idx, "Date"] = str(edit_exp_date) | |
| st.session_state.expense_df.at[idx, "Expense Type"] = edit_expense_type | |
| st.session_state.expense_df.at[idx, "Paid By"] = edit_paid_by | |
| st.session_state.expense_df.at[idx, "Participants"] = ", ".join(edit_participants) | |
| st.session_state.expense_df.at[idx, "Category"] = edit_category | |
| st.session_state.expense_df.at[idx, "Subcategory"] = edit_subcategory | |
| st.session_state.expense_df.at[idx, "Amount"] = int(edit_amount) | |
| st.session_state.expense_df.at[idx, "Description"] = edit_description | |
| st.session_state.expense_df.at[idx, "Month"] = normalize_month(edit_exp_date) | |
| st.success("Expense updated successfully.") | |
| with col2: | |
| st.write("") | |
| st.write("") | |
| if st.button("Delete Expense", type="primary"): | |
| st.session_state.expense_df = st.session_state.expense_df[st.session_state.expense_df["ID"] != selected_expense_id].reset_index(drop=True) | |
| st.success("Expense deleted successfully.") | |
| st.rerun() | |
| # ---------------------------- | |
| # Monthly Budget | |
| # ---------------------------- | |
| elif page == "Monthly Budget": | |
| st.title("📅 Monthly Budget") | |
| budget_df = st.session_state.budget_df.copy() | |
| edited_budget = st.data_editor( | |
| budget_df, | |
| num_rows="dynamic", | |
| use_container_width=True, | |
| key="budget_editor", | |
| hide_index=True, | |
| column_config={ | |
| "Monthly Budget": st.column_config.NumberColumn(format="%,d") | |
| } | |
| ) | |
| col1, col2 = st.columns([1, 1]) | |
| with col1: | |
| if st.button("Save Budget"): | |
| edited_budget["Monthly Budget"] = pd.to_numeric(edited_budget["Monthly Budget"], errors="coerce").fillna(0).astype(int) | |
| st.session_state.budget_df = edited_budget | |
| st.success("Budget updated successfully.") | |
| with col2: | |
| if not st.session_state.budget_df.empty: | |
| delete_budget_category = st.selectbox("Delete Budget Row", st.session_state.budget_df["Category"].tolist()) | |
| if st.button("Delete Budget Category"): | |
| st.session_state.budget_df = st.session_state.budget_df[ | |
| st.session_state.budget_df["Category"] != delete_budget_category | |
| ].reset_index(drop=True) | |
| st.success("Budget row deleted successfully.") | |
| st.rerun() | |
| st.markdown("### Budget vs Expense") | |
| expense_df = st.session_state.expense_df.copy() | |
| current_month = date.today().strftime("%Y-%m") | |
| if not expense_df.empty: | |
| this_month_expense = expense_df[expense_df["Month"] == current_month].groupby("Category", as_index=False)["Amount"].sum() | |
| else: | |
| this_month_expense = pd.DataFrame(columns=["Category", "Amount"]) | |
| merged = st.session_state.budget_df.merge(this_month_expense, on="Category", how="left") | |
| merged["Amount"] = merged["Amount"].fillna(0).astype(int) | |
| merged["Remaining"] = (merged["Monthly Budget"] - merged["Amount"]).astype(int) | |
| merged = merged.rename(columns={"Amount": "Spent"}) | |
| st.dataframe(merged, use_container_width=True, hide_index=True, column_config=format_df(merged)) | |
| # ---------------------------- | |
| # Masters | |
| # ---------------------------- | |
| elif page == "Masters": | |
| st.title("🛠️ Masters") | |
| tab1, tab2, tab3 = st.tabs(["Family Members", "Income Sources", "Categories & Subcategories"]) | |
| with tab1: | |
| st.subheader("Family Members") | |
| st.caption("At least one family member is compulsory.") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| new_name = st.text_input("New Family Member Name") | |
| new_position = st.selectbox( | |
| "New Family Position", | |
| ["Father", "Mother", "Son", "Daughter", "Brother", "Sister", "Grandfather", "Grandmother", "Other"], | |
| key="masters_new_position" | |
| ) | |
| if st.button("Add Family Member"): | |
| success, message = add_member(new_name, new_position) | |
| if success: | |
| st.success(message) | |
| st.rerun() | |
| else: | |
| st.warning(message) | |
| with col2: | |
| member_names = get_member_names() | |
| selected_member_name = st.selectbox("Delete Family Member", member_names) | |
| if st.button("Delete Family Member"): | |
| if len(st.session_state.member_df) <= 1: | |
| st.error("At least one family member must remain.") | |
| else: | |
| st.session_state.member_df = st.session_state.member_df[ | |
| st.session_state.member_df["Name"] != selected_member_name | |
| ].reset_index(drop=True) | |
| st.success("Family member deleted successfully.") | |
| st.rerun() | |
| st.markdown("### Rename / Update Family Position") | |
| member_names = get_member_names() | |
| selected_edit_member = st.selectbox("Select Family Member", member_names, key="edit_family_member") | |
| selected_row = st.session_state.member_df[ | |
| st.session_state.member_df["Name"] == selected_edit_member | |
| ].iloc[0] | |
| edit_name = st.text_input("Edit Name", value=selected_row["Name"]) | |
| family_positions = ["Father", "Mother", "Son", "Daughter", "Brother", "Sister", "Grandfather", "Grandmother", "Other"] | |
| edit_position = st.selectbox( | |
| "Edit Family Position", | |
| family_positions, | |
| index=family_positions.index(selected_row["Family Position"]) if selected_row["Family Position"] in family_positions else 0, | |
| key="edit_family_position" | |
| ) | |
| if st.button("Update Family Member"): | |
| edit_name = edit_name.strip() | |
| if not edit_name: | |
| st.warning("Name cannot be empty.") | |
| else: | |
| old_name = selected_row["Name"] | |
| duplicate_name = st.session_state.member_df[ | |
| (st.session_state.member_df["Name"] == edit_name) & | |
| (st.session_state.member_df["Name"] != old_name) | |
| ] | |
| if not duplicate_name.empty: | |
| st.warning("Another family member already has this name.") | |
| else: | |
| idx = st.session_state.member_df.index[ | |
| st.session_state.member_df["Name"] == old_name | |
| ][0] | |
| st.session_state.member_df.at[idx, "Name"] = edit_name | |
| st.session_state.member_df.at[idx, "Family Position"] = edit_position | |
| if not st.session_state.income_df.empty: | |
| st.session_state.income_df.loc[ | |
| st.session_state.income_df["Member"] == old_name, "Member" | |
| ] = edit_name | |
| if not st.session_state.expense_df.empty: | |
| st.session_state.expense_df.loc[ | |
| st.session_state.expense_df["Paid By"] == old_name, "Paid By" | |
| ] = edit_name | |
| st.session_state.expense_df["Participants"] = st.session_state.expense_df["Participants"].astype(str).str.replace( | |
| old_name, edit_name, regex=False | |
| ) | |
| st.success("Family member updated successfully.") | |
| st.rerun() | |
| st.dataframe(st.session_state.member_df, use_container_width=True, hide_index=True) | |
| with tab2: | |
| st.subheader("Income Sources") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| new_income_source = st.text_input("New Income Source") | |
| if st.button("Add Income Source"): | |
| if not new_income_source.strip(): | |
| st.warning("Enter a valid income source.") | |
| elif new_income_source.strip() in st.session_state.income_sources: | |
| st.warning("This income source already exists.") | |
| else: | |
| st.session_state.income_sources.append(new_income_source.strip()) | |
| st.success("Income source added successfully.") | |
| st.rerun() | |
| with col2: | |
| selected_source = st.selectbox("Delete Income Source", st.session_state.income_sources) | |
| if st.button("Delete Income Source"): | |
| if len(st.session_state.income_sources) <= 1: | |
| st.error("At least one income source must remain.") | |
| else: | |
| st.session_state.income_sources.remove(selected_source) | |
| st.success("Income source deleted successfully.") | |
| st.rerun() | |
| rename_source = st.selectbox("Rename Income Source", st.session_state.income_sources) | |
| renamed_value = st.text_input("New Name for Selected Source") | |
| if st.button("Update Income Source Name"): | |
| if not renamed_value.strip(): | |
| st.warning("Enter a valid new name.") | |
| elif renamed_value.strip() in st.session_state.income_sources: | |
| st.warning("This income source already exists.") | |
| else: | |
| idx = st.session_state.income_sources.index(rename_source) | |
| st.session_state.income_sources[idx] = renamed_value.strip() | |
| st.success("Income source renamed successfully.") | |
| st.rerun() | |
| st.dataframe(pd.DataFrame({"Income Source": st.session_state.income_sources}), use_container_width=True, hide_index=True) | |
| with tab3: | |
| st.subheader("Categories & Subcategories") | |
| categories = list(st.session_state.category_map.keys()) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| new_category = st.text_input("New Category") | |
| if st.button("Add Category"): | |
| if not new_category.strip(): | |
| st.warning("Enter a valid category.") | |
| elif new_category.strip() in st.session_state.category_map: | |
| st.warning("This category already exists.") | |
| else: | |
| st.session_state.category_map[new_category.strip()] = ["Miscellaneous"] | |
| st.success("Category added successfully.") | |
| st.rerun() | |
| with col2: | |
| delete_category = st.selectbox("Delete Category", categories) | |
| if st.button("Delete Category"): | |
| if len(st.session_state.category_map) <= 1: | |
| st.error("At least one category must remain.") | |
| else: | |
| del st.session_state.category_map[delete_category] | |
| st.success("Category deleted successfully.") | |
| st.rerun() | |
| rename_category = st.selectbox("Rename Category", categories) | |
| renamed_category_value = st.text_input("New Category Name") | |
| if st.button("Update Category Name"): | |
| if not renamed_category_value.strip(): | |
| st.warning("Enter a valid category name.") | |
| elif renamed_category_value.strip() in st.session_state.category_map: | |
| st.warning("This category already exists.") | |
| else: | |
| st.session_state.category_map[renamed_category_value.strip()] = st.session_state.category_map.pop(rename_category) | |
| st.success("Category renamed successfully.") | |
| st.rerun() | |
| st.markdown("---") | |
| current_category = st.selectbox("Manage Subcategories For", list(st.session_state.category_map.keys())) | |
| current_subcategories = st.session_state.category_map[current_category] | |
| sub1, sub2 = st.columns(2) | |
| with sub1: | |
| new_subcategory = st.text_input("New Subcategory") | |
| if st.button("Add Subcategory"): | |
| if not new_subcategory.strip(): | |
| st.warning("Enter a valid subcategory.") | |
| elif new_subcategory.strip() in st.session_state.category_map[current_category]: | |
| st.warning("This subcategory already exists.") | |
| else: | |
| st.session_state.category_map[current_category].append(new_subcategory.strip()) | |
| st.success("Subcategory added successfully.") | |
| st.rerun() | |
| with sub2: | |
| delete_subcategory = st.selectbox("Delete Subcategory", current_subcategories) | |
| if st.button("Delete Subcategory"): | |
| if len(st.session_state.category_map[current_category]) <= 1: | |
| st.error("At least one subcategory must remain in a category.") | |
| else: | |
| st.session_state.category_map[current_category].remove(delete_subcategory) | |
| st.success("Subcategory deleted successfully.") | |
| st.rerun() | |
| rename_subcategory = st.selectbox("Rename Subcategory", current_subcategories) | |
| renamed_subcategory_value = st.text_input("New Subcategory Name") | |
| if st.button("Update Subcategory Name"): | |
| if not renamed_subcategory_value.strip(): | |
| st.warning("Enter a valid subcategory name.") | |
| elif renamed_subcategory_value.strip() in st.session_state.category_map[current_category]: | |
| st.warning("This subcategory already exists.") | |
| else: | |
| idx = st.session_state.category_map[current_category].index(rename_subcategory) | |
| st.session_state.category_map[current_category][idx] = renamed_subcategory_value.strip() | |
| st.success("Subcategory renamed successfully.") | |
| st.rerun() | |
| flat_rows = [] | |
| for cat, subs in st.session_state.category_map.items(): | |
| for sub in subs: | |
| flat_rows.append({"Category": cat, "Subcategory": sub}) | |
| st.dataframe(pd.DataFrame(flat_rows), use_container_width=True, hide_index=True) | |
| # ---------------------------- | |
| # Reports | |
| # ---------------------------- | |
| elif page == "Reports": | |
| st.title("📊 Reports") | |
| expense_df = st.session_state.expense_df.copy() | |
| income_df = st.session_state.income_df.copy() | |
| if expense_df.empty and income_df.empty: | |
| st.info("No data available yet.") | |
| else: | |
| st.markdown("### Filter Reports") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| report_month = st.text_input("Month (YYYY-MM)", value=date.today().strftime("%Y-%m")) | |
| with col2: | |
| member_filter = st.selectbox("Member", ["All"] + get_member_names()) | |
| with col3: | |
| category_filter = st.selectbox("Category", ["All"] + list(st.session_state.category_map.keys())) | |
| filtered_expense = expense_df.copy() | |
| filtered_income = income_df.copy() | |
| if not filtered_expense.empty: | |
| filtered_expense = filtered_expense[filtered_expense["Month"] == report_month] | |
| if not filtered_income.empty: | |
| filtered_income = filtered_income[filtered_income["Date"].astype(str).str[:7] == report_month] | |
| if member_filter != "All": | |
| if not filtered_expense.empty: | |
| filtered_expense = filtered_expense[filtered_expense["Paid By"] == member_filter] | |
| if not filtered_income.empty: | |
| filtered_income = filtered_income[filtered_income["Member"] == member_filter] | |
| if category_filter != "All" and not filtered_expense.empty: | |
| filtered_expense = filtered_expense[filtered_expense["Category"] == category_filter] | |
| total_income = int(filtered_income["Amount"].sum()) if not filtered_income.empty else 0 | |
| total_expense = int(filtered_expense["Amount"].sum()) if not filtered_expense.empty else 0 | |
| savings = total_income - total_expense | |
| r1, r2, r3 = st.columns(3) | |
| r1.metric("Report Income", f"Rs. {total_income:,}") | |
| r2.metric("Report Expense", f"Rs. {total_expense:,}") | |
| r3.metric("Savings", f"Rs. {savings:,}") | |
| st.markdown("### Expense Table") | |
| st.dataframe(filtered_expense, use_container_width=True, hide_index=True, column_config=format_df(filtered_expense)) | |
| st.markdown("### Income Table") | |
| st.dataframe(filtered_income, use_container_width=True, hide_index=True, column_config=format_df(filtered_income)) | |
| if not filtered_expense.empty: | |
| st.markdown("### Category Summary") | |
| summary = filtered_expense.groupby("Category", as_index=False)["Amount"].sum().sort_values("Amount", ascending=False) | |
| st.dataframe(summary, use_container_width=True, hide_index=True, column_config=format_df(summary)) | |
| st.bar_chart(summary.set_index("Category")) | |
| st.markdown("### Expense Type Summary") | |
| type_summary = filtered_expense.groupby("Expense Type", as_index=False)["Amount"].sum() | |
| st.dataframe(type_summary, use_container_width=True, hide_index=True, column_config=format_df(type_summary)) | |
| st.bar_chart(type_summary.set_index("Expense Type")) | |
| # ---------------------------- | |
| # Development Notes | |
| # ---------------------------- | |
| elif page == "Development Notes": | |
| st.title("📝 Development Notes") | |
| with st.form("dev_note_form", clear_on_submit=True): | |
| note_date = st.date_input("Date", value=date.today()) | |
| note_type = st.selectbox("Type", ["Requirement", "Bug", "Improvement", "Idea", "Task"]) | |
| title = st.text_input("Title") | |
| details = st.text_area("Details") | |
| status = st.selectbox("Status", ["Open", "In Progress", "Completed", "Hold"]) | |
| submit_note = st.form_submit_button("Add Note") | |
| if submit_note: | |
| if not title.strip(): | |
| st.error("Title is required.") | |
| else: | |
| new_row = pd.DataFrame([{ | |
| "ID": st.session_state.next_note_id, | |
| "Date": str(note_date), | |
| "Type": note_type, | |
| "Title": title.strip(), | |
| "Details": details.strip(), | |
| "Status": status | |
| }]) | |
| st.session_state.dev_notes_df = pd.concat([st.session_state.dev_notes_df, new_row], ignore_index=True) | |
| st.session_state.next_note_id += 1 | |
| st.success("Development note added successfully.") | |
| st.markdown("### Notes List") | |
| st.dataframe(st.session_state.dev_notes_df, use_container_width=True, hide_index=True) | |
| if not st.session_state.dev_notes_df.empty: | |
| st.markdown("### Edit or Delete Note") | |
| selected_note_id = st.selectbox("Select Note", st.session_state.dev_notes_df["ID"].tolist()) | |
| selected_note = st.session_state.dev_notes_df[st.session_state.dev_notes_df["ID"] == selected_note_id].iloc[0] | |
| type_options = ["Requirement", "Bug", "Improvement", "Idea", "Task"] | |
| status_options = ["Open", "In Progress", "Completed", "Hold"] | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| with st.form("edit_note_form"): | |
| edit_note_date = st.date_input("Edit Date", value=pd.to_datetime(selected_note["Date"]).date()) | |
| edit_note_type = st.selectbox("Edit Type", type_options, index=type_options.index(selected_note["Type"])) | |
| edit_title = st.text_input("Edit Title", value=selected_note["Title"]) | |
| edit_details = st.text_area("Edit Details", value=selected_note["Details"]) | |
| edit_status = st.selectbox("Edit Status", status_options, index=status_options.index(selected_note["Status"])) | |
| update_note = st.form_submit_button("Update Note") | |
| if update_note: | |
| if not edit_title.strip(): | |
| st.error("Title is required.") | |
| else: | |
| idx = st.session_state.dev_notes_df.index[st.session_state.dev_notes_df["ID"] == selected_note_id][0] | |
| st.session_state.dev_notes_df.at[idx, "Date"] = str(edit_note_date) | |
| st.session_state.dev_notes_df.at[idx, "Type"] = edit_note_type | |
| st.session_state.dev_notes_df.at[idx, "Title"] = edit_title.strip() | |
| st.session_state.dev_notes_df.at[idx, "Details"] = edit_details.strip() | |
| st.session_state.dev_notes_df.at[idx, "Status"] = edit_status | |
| st.success("Development note updated successfully.") | |
| with col2: | |
| st.write("") | |
| st.write("") | |
| if st.button("Delete Note", type="primary"): | |
| st.session_state.dev_notes_df = st.session_state.dev_notes_df[st.session_state.dev_notes_df["ID"] != selected_note_id].reset_index(drop=True) | |
| st.success("Development note deleted successfully.") | |
| st.rerun() | |
| # ---------------------------- | |
| # Data Backup | |
| # ---------------------------- | |
| elif page == "Data Backup": | |
| st.title("💾 Data Backup") | |
| st.markdown("### Download Current Data") | |
| st.download_button( | |
| "Download Family Members CSV", | |
| data=dataframe_to_csv_download(st.session_state.member_df), | |
| file_name="family_members.csv", | |
| mime="text/csv" | |
| ) | |
| st.download_button( | |
| "Download Income CSV", | |
| data=dataframe_to_csv_download(st.session_state.income_df), | |
| file_name="income_data.csv", | |
| mime="text/csv" | |
| ) | |
| st.download_button( | |
| "Download Expense CSV", | |
| data=dataframe_to_csv_download(st.session_state.expense_df), | |
| file_name="expense_data.csv", | |
| mime="text/csv" | |
| ) | |
| st.download_button( | |
| "Download Budget CSV", | |
| data=dataframe_to_csv_download(st.session_state.budget_df), | |
| file_name="budget_data.csv", | |
| mime="text/csv" | |
| ) | |
| st.download_button( | |
| "Download Development Notes CSV", | |
| data=dataframe_to_csv_download(st.session_state.dev_notes_df), | |
| file_name="development_notes.csv", | |
| mime="text/csv" | |
| ) | |
| st.markdown("### Restore from CSV") | |
| uploaded_members = st.file_uploader("Upload Family Members CSV", type=["csv"], key="members_upload") | |
| uploaded_income = st.file_uploader("Upload Income CSV", type=["csv"], key="income_upload") | |
| uploaded_expense = st.file_uploader("Upload Expense CSV", type=["csv"], key="expense_upload") | |
| uploaded_budget = st.file_uploader("Upload Budget CSV", type=["csv"], key="budget_upload") | |
| uploaded_notes = st.file_uploader("Upload Development Notes CSV", type=["csv"], key="notes_upload") | |
| if st.button("Restore Uploaded Data"): | |
| try: | |
| if uploaded_members is not None: | |
| st.session_state.member_df = load_uploaded_csv(uploaded_members, ["Name", "Family Position"]) | |
| if uploaded_income is not None: | |
| st.session_state.income_df = load_uploaded_csv(uploaded_income, ["ID", "Date", "Member", "Source", "Amount", "Notes"]) | |
| if uploaded_expense is not None: | |
| st.session_state.expense_df = load_uploaded_csv( | |
| uploaded_expense, | |
| ["ID", "Date", "Expense Type", "Paid By", "Participants", "Category", "Subcategory", "Amount", "Description", "Month"] | |
| ) | |
| if uploaded_budget is not None: | |
| st.session_state.budget_df = load_uploaded_csv(uploaded_budget, ["Category", "Monthly Budget"]) | |
| if uploaded_notes is not None: | |
| st.session_state.dev_notes_df = load_uploaded_csv(uploaded_notes, ["ID", "Date", "Type", "Title", "Details", "Status"]) | |
| if not st.session_state.income_df.empty: | |
| st.session_state.next_income_id = int(pd.to_numeric(st.session_state.income_df["ID"], errors="coerce").fillna(0).max()) + 1 | |
| if not st.session_state.expense_df.empty: | |
| st.session_state.next_expense_id = int(pd.to_numeric(st.session_state.expense_df["ID"], errors="coerce").fillna(0).max()) + 1 | |
| if not st.session_state.dev_notes_df.empty: | |
| st.session_state.next_note_id = int(pd.to_numeric(st.session_state.dev_notes_df["ID"], errors="coerce").fillna(0).max()) + 1 | |
| st.success("Data restored successfully.") | |
| except Exception as e: | |
| st.error(f"Restore failed: {e}") |