Skip to content

Commit 40d72db

Browse files
committed
feat(groups): enhance expense splitting options with detailed methods and UI improvements
1 parent 0813758 commit 40d72db

File tree

1 file changed

+320
-18
lines changed

1 file changed

+320
-18
lines changed

ui-poc/pages/Groups.py

Lines changed: 320 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -305,34 +305,336 @@ def fetch_group_expenses(group_id):
305305
# Fetch members for payer selection
306306
members = fetch_group_members(group.get('_id'))
307307
member_options = {member.get('user', {}).get('name', f'User {i}'): member.get('userId') for i, member in enumerate(members, 1)}
308-
selected_payer = st.selectbox("Paid by", options=list(member_options.keys()), key=f"expense_payer_page_{group.get('_id')}")
308+
309+
col1, col2 = st.columns([3, 1])
310+
with col1:
311+
selected_payer = st.selectbox("Paid by", options=list(member_options.keys()), key=f"expense_payer_page_{group.get('_id')}")
312+
with col2:
313+
st.write("💰 **Total Amount**")
314+
st.write(f"**₹{expense_amount:.2f}**")
315+
316+
# Split options with tabs
317+
st.write("### Split Options")
318+
split_method_tooltip = """
319+
- Equally: Split the amount equally between selected members
320+
- By Percentages: Specify what percentage of the bill each person pays
321+
- By Shares: Assign shares to each person (e.g., 1 share = 1 portion)
322+
- By Exact Value: Enter the exact amount each person should pay
323+
"""
324+
st.info(split_method_tooltip)
325+
326+
# Set up tab tracking - make the radio button visually match the tabs
327+
tab_options = ["Equally", "By Percentages", "By Shares", "By Exact Value"]
328+
tab_key = f"active_tab_{group.get('_id')}"
329+
330+
if tab_key not in st.session_state:
331+
st.session_state[tab_key] = "Equally"
332+
333+
# Create a visible tab selector using radio buttons
334+
active_tab = st.radio(
335+
"Split Method",
336+
tab_options,
337+
horizontal=True,
338+
key=tab_key
339+
)
340+
341+
# Create the tabs that show content based on the active tab
342+
split_tabs = st.tabs(tab_options)
343+
344+
# Only show the content for the active tab
345+
active_tab_index = tab_options.index(active_tab)
346+
347+
# Initialize session state for member selection
348+
if f"selected_members_{group.get('_id')}" not in st.session_state:
349+
st.session_state[f"selected_members_{group.get('_id')}"] = [m.get("userId") for m in members]
350+
351+
# Tab 1: Split Equally
352+
with split_tabs[0]:
353+
st.write("Split equally between:")
354+
355+
# Initialize selected members dict if not exists
356+
tab_key = f"equal_members_{group.get('_id')}"
357+
if tab_key not in st.session_state:
358+
st.session_state[tab_key] = {m.get("userId"): True for m in members}
359+
360+
# Select/Deselect All checkbox
361+
all_selected_key = f"all_members_equal_{group.get('_id')}"
362+
363+
# Check if all are currently selected
364+
all_currently_selected = all(st.session_state[tab_key].values())
365+
366+
# The checkbox for Select All / Deselect All
367+
all_selected = st.checkbox(
368+
"Select/Deselect All",
369+
value=all_currently_selected,
370+
key=all_selected_key
371+
)
372+
373+
# If the checkbox state changes, update all members
374+
if all_selected != all_currently_selected:
375+
for member in members:
376+
st.session_state[tab_key][member.get('userId')] = all_selected
377+
378+
# Individual member checkboxes
379+
member_cols = st.columns(2) # Display in 2 columns for better space usage
380+
for i, member in enumerate(members):
381+
user_name = member.get('user', {}).get('name', 'Unknown User')
382+
with member_cols[i % 2]:
383+
is_selected = st.checkbox(
384+
user_name,
385+
value=st.session_state[tab_key].get(member.get('userId'), True),
386+
key=f"equal_member_{member.get('userId')}_{group.get('_id')}"
387+
)
388+
st.session_state[tab_key][member.get('userId')] = is_selected
389+
390+
# Get list of selected member IDs
391+
selected_member_ids = [
392+
member_id for member_id, is_selected in st.session_state[tab_key].items()
393+
if is_selected
394+
]
395+
396+
# Display warning if no members are selected
397+
if not selected_member_ids:
398+
st.warning("Please select at least one member to split the expense.")
399+
400+
# Tab 2: By Percentages
401+
with split_tabs[1]:
402+
st.write("Split by percentages")
403+
404+
# Initialize percentages
405+
percentage_inputs = {}
406+
total_percentage = 0
407+
408+
for member in members:
409+
user_name = member.get('user', {}).get('name', 'Unknown User')
410+
default_value = round(100 / len(members), 2) if len(members) > 0 else 0
411+
percentage = st.number_input(
412+
f"{user_name} (%)",
413+
min_value=0.0,
414+
max_value=100.0,
415+
value=default_value,
416+
step=0.1,
417+
format="%.2f",
418+
key=f"percent_{member.get('userId')}"
419+
)
420+
percentage_inputs[member.get('userId')] = percentage
421+
total_percentage += percentage
422+
423+
# Show total percentage
424+
if total_percentage != 100:
425+
st.warning(f"Total percentage: {total_percentage}% (should be 100%)")
426+
else:
427+
st.success(f"Total percentage: {total_percentage}%")
428+
429+
# Tab 3: By Shares
430+
with split_tabs[2]:
431+
st.write("Split by shares")
432+
433+
# Initialize shares
434+
share_inputs = {}
435+
total_shares = 0
436+
437+
for member in members:
438+
user_name = member.get('user', {}).get('name', 'Unknown User')
439+
shares = st.number_input(
440+
f"{user_name} (shares)",
441+
min_value=0,
442+
value=1,
443+
step=1,
444+
key=f"shares_{member.get('userId')}"
445+
)
446+
share_inputs[member.get('userId')] = shares
447+
total_shares += shares
448+
449+
# Show total shares
450+
if total_shares == 0:
451+
st.error("Total shares cannot be 0")
452+
else:
453+
st.info(f"Total shares: {total_shares}")
454+
455+
# Show preview of amount per person
456+
st.write("### Preview:")
457+
for member in members:
458+
user_name = member.get('user', {}).get('name', 'Unknown User')
459+
user_id = member.get('userId')
460+
if user_id in share_inputs and total_shares > 0:
461+
share_percentage = share_inputs[user_id] / total_shares
462+
amount = expense_amount * share_percentage
463+
st.write(f"{user_name}: ₹{amount:.2f} ({share_percentage*100:.2f}%)")
464+
465+
# Tab 4: By Exact Value
466+
with split_tabs[3]:
467+
st.write("Split by exact amounts")
468+
469+
# Initialize exact amounts
470+
exact_inputs = {}
471+
total_exact = 0
472+
473+
for member in members:
474+
user_name = member.get('user', {}).get('name', 'Unknown User')
475+
exact_amount = st.number_input(
476+
f"{user_name} (₹)",
477+
min_value=0.0,
478+
max_value=float(expense_amount),
479+
value=round(expense_amount / len(members), 2) if len(members) > 0 else 0,
480+
step=0.01,
481+
format="%.2f",
482+
key=f"exact_{member.get('userId')}"
483+
)
484+
exact_inputs[member.get('userId')] = exact_amount
485+
total_exact += exact_amount
486+
487+
# Show total amount
488+
if abs(total_exact - expense_amount) > 0.01:
489+
st.warning(f"Total: ₹{total_exact:.2f} (should be ₹{expense_amount:.2f})")
490+
else:
491+
st.success(f"Total: ₹{total_exact:.2f}")
492+
493+
# Active tab is already set by the set_active_tab function in each tab
494+
# This ensures we're using the correctly selected tab for calculations
495+
496+
# Show a summary of the split
497+
st.write("---")
498+
st.write("### Split Summary")
499+
500+
# Calculate and display split summary based on the active tab
501+
if active_tab == "Equally":
502+
# Get the list of selected members from the session state
503+
equal_tab_key = f"equal_members_{group.get('_id')}"
504+
selected_member_ids = [
505+
member_id for member_id, is_selected in st.session_state[equal_tab_key].items()
506+
if is_selected
507+
]
508+
509+
if selected_member_ids:
510+
equal_split_amount = round(expense_amount / len(selected_member_ids), 2)
511+
for member in members:
512+
user_name = member.get('user', {}).get('name', 'Unknown User')
513+
if member.get('userId') in selected_member_ids:
514+
st.write(f"• {user_name}: ₹{equal_split_amount:.2f}")
515+
else:
516+
st.write(f"• {user_name}: ₹0.00")
517+
else:
518+
st.warning("No members selected for splitting")
519+
520+
elif active_tab == "By Percentages":
521+
for member in members:
522+
user_name = member.get('user', {}).get('name', 'Unknown User')
523+
percentage = percentage_inputs.get(member.get('userId'), 0)
524+
amount = round(expense_amount * percentage / 100, 2)
525+
st.write(f"• {user_name}: ₹{amount:.2f} ({percentage}%)")
526+
527+
elif active_tab == "By Shares":
528+
if total_shares > 0:
529+
for member in members:
530+
user_name = member.get('user', {}).get('name', 'Unknown User')
531+
shares = share_inputs.get(member.get('userId'), 0)
532+
amount = round(expense_amount * shares / total_shares, 2) if shares > 0 else 0
533+
st.write(f"• {user_name}: ₹{amount:.2f} ({shares} shares)")
534+
else:
535+
st.warning("Total shares must be greater than 0")
536+
537+
elif active_tab == "By Exact Value":
538+
for member in members:
539+
user_name = member.get('user', {}).get('name', 'Unknown User')
540+
amount = exact_inputs.get(member.get('userId'), 0)
541+
st.write(f"• {user_name}: ₹{amount:.2f}")
542+
543+
if abs(total_exact - expense_amount) > 0.01:
544+
remaining = expense_amount - total_exact
545+
st.warning(f"Remaining amount to be allocated: ₹{remaining:.2f}")
309546

310547
submit_button = st.form_submit_button("Add Expense")
311548

312549
if submit_button and expense_title and expense_amount:
313550
try:
314551
headers = {"Authorization": f"Bearer {st.session_state.access_token}"}
315-
# Create equal splits for all members
552+
# Create splits based on selected tab
316553
try:
317554
if not members:
318555
st.error("No members found in group. Cannot create expense.")
319556
st.stop()
557+
558+
splits = []
559+
split_type = "equal"
560+
561+
if active_tab == "Equally":
562+
# Get the list of selected members from the session state
563+
equal_tab_key = f"equal_members_{group.get('_id')}"
564+
selected_member_ids = [
565+
member_id for member_id, is_selected in st.session_state[equal_tab_key].items()
566+
if is_selected
567+
]
568+
569+
if not selected_member_ids:
570+
st.error("No members selected for splitting the expense.")
571+
st.stop()
572+
573+
equal_split_amount = round(expense_amount / len(selected_member_ids), 2)
574+
splits = [
575+
{
576+
"userId": member_id,
577+
"amount": equal_split_amount,
578+
"type": "equal"
579+
}
580+
for member_id in selected_member_ids
581+
]
582+
split_type = "equal"
320583

321-
equal_split_amount = round(expense_amount / len(members), 2)
322-
splits = [
323-
{
324-
"userId": member.get("userId"),
325-
"amount": equal_split_amount,
326-
"type": "equal"
327-
}
328-
for member in members
329-
]
584+
elif active_tab == "By Percentages":
585+
if abs(total_percentage - 100) > 0.01:
586+
st.error("Total percentage must be 100%.")
587+
st.stop()
588+
589+
splits = [
590+
{
591+
"userId": member.get('userId'),
592+
"amount": round(expense_amount * percentage_inputs[member.get('userId')] / 100, 2),
593+
"type": "percentage"
594+
}
595+
for member in members if percentage_inputs.get(member.get('userId'), 0) > 0
596+
]
597+
split_type = "percentage"
598+
599+
elif active_tab == "By Shares":
600+
if total_shares == 0:
601+
st.error("Total shares cannot be 0.")
602+
st.stop()
603+
604+
splits = [
605+
{
606+
"userId": member.get('userId'),
607+
"amount": round(expense_amount * share_inputs[member.get('userId')] / total_shares, 2),
608+
"type": "unequal"
609+
}
610+
for member in members if share_inputs.get(member.get('userId'), 0) > 0
611+
]
612+
split_type = "unequal"
613+
614+
elif active_tab == "By Exact Value":
615+
if abs(total_exact - expense_amount) > 0.01:
616+
st.error(f"Total amount must be equal to ₹{expense_amount:.2f}.")
617+
st.stop()
618+
619+
splits = [
620+
{
621+
"userId": member.get('userId'),
622+
"amount": exact_inputs[member.get('userId')],
623+
"type": "exact"
624+
}
625+
for member in members if exact_inputs.get(member.get('userId'), 0) > 0
626+
]
627+
split_type = "exact"
628+
629+
# Get the payer's ID from the selected name
630+
payer_id = member_options.get(selected_payer)
330631

331632
expense_data = {
332633
"description": expense_title + (f" - {expense_description}" if expense_description else ""),
333634
"amount": expense_amount,
334635
"splits": splits,
335-
"splitType": "equal",
636+
"splitType": split_type,
637+
"paidBy": payer_id, # Add the payer ID
336638
"tags": []
337639
}
338640

@@ -344,12 +646,12 @@ def fetch_group_expenses(group_id):
344646
st.stop()
345647

346648
with st.spinner("Creating expense..."):
347-
response = make_api_request(
348-
'post',
349-
f"{API_URL}/groups/{group.get('_id')}/expenses",
350-
headers=headers,
351-
json_data=expense_data
352-
)
649+
response = make_api_request(
650+
'post',
651+
f"{API_URL}/groups/{group.get('_id')}/expenses",
652+
headers=headers,
653+
json_data=expense_data
654+
)
353655

354656
if response.status_code == 201:
355657
st.success("Expense added successfully!")

0 commit comments

Comments
 (0)