ExpenseManager / app.py
sulehree's picture
Update app.py
0ee76cf verified
Raw History Blame Contribute Delete
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}")