diff --git "a/vendors.py" "b/vendors.py"
new file mode 100644--- /dev/null
+++ "b/vendors.py"
@@ -0,0 +1,3216 @@
+import streamlit as st
+import json
+from datetime import datetime
+from config_manager import ConfigManager
+
+class VendorManager:
+ def __init__(self):
+ self.config_manager = ConfigManager()
+
+ def create_initial_payment_history(self, deposit_paid, deposit_paid_date, payment_method, payment_notes, is_credit=False):
+ """Create initial payment history with deposit if any"""
+ payment_history = []
+ if deposit_paid > 0:
+ payment_type = 'credit' if is_credit else 'deposit'
+ notes_prefix = "Initial credit/reimbursement" if is_credit else "Initial payment"
+ payment_record = {
+ 'id': datetime.now().strftime("%Y%m%d_%H%M%S_%f"),
+ 'amount': deposit_paid,
+ 'date': deposit_paid_date.isoformat() if deposit_paid_date else datetime.now().isoformat(),
+ 'method': payment_method,
+ 'notes': f"{notes_prefix}. {payment_notes}" if payment_notes else notes_prefix,
+ 'type': payment_type
+ }
+ payment_history.append(payment_record)
+ return payment_history
+
+ def update_deposit_in_payment_history(self, vendor, new_deposit_paid, new_deposit_paid_date, payment_method, payment_notes):
+ """Update deposit in payment history when deposit amount or date changes"""
+ payment_history = vendor.get('payment_history', [])
+
+ # Find existing deposit record
+ deposit_record = None
+ for record in payment_history:
+ if record.get('type') == 'deposit':
+ deposit_record = record
+ break
+
+ # Remove old deposit record if it exists
+ if deposit_record:
+ payment_history.remove(deposit_record)
+
+ # Add new deposit record if deposit amount > 0
+ if new_deposit_paid > 0:
+ new_deposit_record = {
+ 'id': datetime.now().strftime("%Y%m%d_%H%M%S_%f"),
+ 'amount': new_deposit_paid,
+ 'date': new_deposit_paid_date.isoformat() if new_deposit_paid_date else datetime.now().isoformat(),
+ 'method': payment_method,
+ 'notes': f"Payment. {payment_notes}" if payment_notes else "Payment",
+ 'type': 'deposit'
+ }
+ payment_history.append(new_deposit_record)
+
+ return payment_history
+
+ def migrate_deposits_to_payment_history(self, vendors):
+ """Migrate existing deposits to payment history for vendors that don't have them"""
+ vendors_updated = False
+ for vendor in vendors:
+ if 'payment_history' not in vendor:
+ vendor['payment_history'] = []
+
+ # Check if deposit exists but not in payment history
+ deposit_paid = vendor.get('deposit_paid', 0)
+ if deposit_paid > 0:
+ # Check if deposit is already in payment history
+ has_deposit_in_history = any(record.get('type') == 'deposit' for record in vendor.get('payment_history', []))
+
+ if not has_deposit_in_history:
+ # Add deposit to payment history
+ deposit_record = {
+ 'id': datetime.now().strftime("%Y%m%d_%H%M%S_%f"),
+ 'amount': deposit_paid,
+ 'date': vendor.get('deposit_paid_date', datetime.now().isoformat()),
+ 'method': vendor.get('payment_method', 'Not Specified'),
+ 'notes': f"Payment. {vendor.get('payment_notes', '')}" if vendor.get('payment_notes') else "Payment",
+ 'type': 'deposit'
+ }
+ vendor['payment_history'].append(deposit_record)
+ vendors_updated = True
+
+ return vendors_updated
+
+ def sync_paid_installments_to_payment_history(self, vendors):
+ """Sync paid installments to payment history for accurate payment tracking"""
+ vendors_updated = False
+ for vendor in vendors:
+ payment_installments = vendor.get('payment_installments', [])
+ payment_history = vendor.get('payment_history', [])
+
+ if not payment_installments:
+ continue
+
+ # Get existing installment payment IDs and their records
+ existing_installment_records = {}
+ for record in payment_history:
+ if record.get('type') == 'installment' and record.get('installment_id'):
+ existing_installment_records[record.get('installment_id')] = record
+
+ # Check each installment
+ for i, installment in enumerate(payment_installments):
+ installment_id = f"installment_{i+1}_{vendor.get('id', 'unknown')}"
+ is_paid = installment.get('paid', False)
+ installment_amount = installment.get('amount', 0)
+
+ if is_paid:
+ # Installment is marked as paid - add to payment history if not already there
+ if installment_id not in existing_installment_records:
+ # Check if this payment amount already exists in payment history
+ # to avoid creating duplicate payments
+ amount_already_recorded = False
+ for record in payment_history:
+ if (record.get('amount') == installment_amount and
+ record.get('type') in ['payment', 'deposit', 'installment'] and
+ not record.get('installment_id')):
+ amount_already_recorded = True
+ break
+
+ # Only create installment record if the amount hasn't been recorded yet
+ # AND if there's a paid_date (indicating the payment was actually made)
+ if not amount_already_recorded and installment.get('paid_date'):
+ installment_record = {
+ 'id': datetime.now().strftime("%Y%m%d_%H%M%S_%f"),
+ 'installment_id': installment_id,
+ 'amount': installment_amount,
+ 'date': installment.get('paid_date') or installment.get('due_date') or datetime.now().isoformat(),
+ 'method': vendor.get('payment_method', 'Not Specified'),
+ 'notes': f"Installment {i+1}",
+ 'type': 'installment'
+ }
+ payment_history.append(installment_record)
+ vendors_updated = True
+ else:
+ # Installment is marked as unpaid - remove from payment history if it exists
+ if installment_id in existing_installment_records:
+ payment_history = [record for record in payment_history if record.get('installment_id') != installment_id]
+ vendors_updated = True
+
+ # Update the vendor's payment history
+ vendor['payment_history'] = payment_history
+
+ return vendors_updated
+
+ def cleanup_duplicate_payments(self, vendors):
+ """Remove duplicate payment records that may have been created"""
+ vendors_updated = False
+ for vendor in vendors:
+ payment_history = vendor.get('payment_history', [])
+ if not payment_history:
+ continue
+
+ # Group payments by amount, date, and method to find true duplicates
+ # Only consider payments with the same amount, date, method, and type as potential duplicates
+ payment_groups = {}
+ for record in payment_history:
+ amount = record.get('amount', 0)
+ date = record.get('date', '')
+ method = record.get('method', '')
+ payment_type = record.get('type', 'payment')
+ # Use a more specific key that includes method and type to avoid false positives
+ key = f"{amount}_{date}_{method}_{payment_type}"
+
+ if key not in payment_groups:
+ payment_groups[key] = []
+ payment_groups[key].append(record)
+
+ # Remove only true duplicates - payments with identical amount, date, method, and type
+ cleaned_history = []
+ for key, records in payment_groups.items():
+ if len(records) > 1:
+ # Keep the first record, remove true duplicates
+ # Only mark as updated if we actually found duplicates
+ cleaned_history.append(records[0])
+ vendors_updated = True
+ else:
+ cleaned_history.append(records[0])
+
+ vendor['payment_history'] = cleaned_history
+
+ return vendors_updated
+
+ def check_payment_due_dates(self, vendors):
+ """Check for payment due dates and show notifications"""
+ from datetime import datetime, date, timedelta
+
+ today = date.today()
+ past_due_vendors = []
+ due_today_vendors = []
+ due_soon_vendors = []
+
+ for vendor in vendors:
+ payment_installments = vendor.get('payment_installments', [])
+
+ if payment_installments:
+ # Check each installment
+ for i, installment in enumerate(payment_installments):
+ if not installment.get('paid', False): # Only check unpaid installments
+ due_date_str = installment.get('due_date')
+ if due_date_str:
+ try:
+ due_date = datetime.fromisoformat(due_date_str).date()
+ days_until_due = (due_date - today).days
+
+ vendor_info = {
+ 'name': vendor.get('name', ''),
+ 'installment_num': i + 1,
+ 'amount': installment.get('amount', 0),
+ 'due_date': due_date_str
+ }
+
+ if days_until_due < 0:
+ past_due_vendors.append(vendor_info)
+ elif days_until_due == 0:
+ due_today_vendors.append(vendor_info)
+ elif days_until_due <= 30: # Due within a month
+ due_soon_vendors.append(vendor_info)
+ except:
+ continue
+ else:
+ # Check single payment due date
+ payment_due_date_str = vendor.get('payment_due_date')
+ if payment_due_date_str:
+ try:
+ due_date = datetime.fromisoformat(payment_due_date_str).date()
+ days_until_due = (due_date - today).days
+
+ # Only show if not fully paid
+ total_cost = vendor.get('total_cost', vendor.get('cost', 0))
+ deposit_paid = vendor.get('deposit_paid', 0)
+
+ if deposit_paid < total_cost: # Not fully paid
+ vendor_info = {
+ 'name': vendor.get('name', ''),
+ 'installment_num': None,
+ 'amount': total_cost - deposit_paid,
+ 'due_date': payment_due_date_str
+ }
+
+ if days_until_due < 0:
+ past_due_vendors.append(vendor_info)
+ elif days_until_due == 0:
+ due_today_vendors.append(vendor_info)
+ elif days_until_due <= 30:
+ due_soon_vendors.append(vendor_info)
+ except:
+ continue
+
+ # Show notifications
+ if past_due_vendors:
+ st.error("🚨 **PAST DUE PAYMENTS**")
+ for vendor_info in past_due_vendors:
+ if vendor_info['installment_num']:
+ st.error(f"**{vendor_info['name']}** - Installment {vendor_info['installment_num']}: ${vendor_info['amount']:,.0f} (was due {vendor_info['due_date']})")
+ else:
+ st.error(f"**{vendor_info['name']}** - ${vendor_info['amount']:,.0f} (was due {vendor_info['due_date']})")
+
+ if due_today_vendors:
+ st.warning("⚠️ **PAYMENTS DUE TODAY**")
+ for vendor_info in due_today_vendors:
+ if vendor_info['installment_num']:
+ st.warning(f"**{vendor_info['name']}** - Installment {vendor_info['installment_num']}: ${vendor_info['amount']:,.0f}")
+ else:
+ st.warning(f"**{vendor_info['name']}** - ${vendor_info['amount']:,.0f}")
+
+ if due_soon_vendors:
+ st.info("📅 **PAYMENTS DUE SOON** (within 30 days)")
+ for vendor_info in due_soon_vendors:
+ # Calculate days until due for better formatting
+ try:
+ due_date = datetime.fromisoformat(vendor_info['due_date']).date()
+ days_until = (due_date - today).days
+ if days_until == 1:
+ due_text = "tomorrow"
+ elif days_until <= 30:
+ due_text = f"in {days_until} days"
+ else:
+ due_text = f"on {vendor_info['due_date']}"
+ except:
+ due_text = f"on {vendor_info['due_date']}"
+
+ if vendor_info['installment_num']:
+ st.info(f"**{vendor_info['name']}** - Installment {vendor_info['installment_num']}: ${vendor_info['amount']:,.0f} (due {due_text})")
+ else:
+ st.info(f"**{vendor_info['name']}** - ${vendor_info['amount']:,.0f} (due {due_text})")
+
+ # Add spacing if any notifications were shown
+ if past_due_vendors or due_today_vendors or due_soon_vendors:
+ st.markdown("---")
+
+ def show_upcoming_payments_summary(self, vendors):
+ """Show a summary of upcoming payments due within a month"""
+ from datetime import datetime, date, timedelta
+
+ today = date.today()
+ upcoming_payments = []
+
+ for vendor in vendors:
+ payment_installments = vendor.get('payment_installments', [])
+
+ if payment_installments:
+ # Check each installment
+ for i, installment in enumerate(payment_installments):
+ if not installment.get('paid', False): # Only check unpaid installments
+ due_date_str = installment.get('due_date')
+ if due_date_str:
+ try:
+ due_date = datetime.fromisoformat(due_date_str).date()
+ days_until_due = (due_date - today).days
+
+ if 0 <= days_until_due <= 30: # Due within a month (including today)
+ upcoming_payments.append({
+ 'vendor_name': vendor.get('name', ''),
+ 'installment_num': i + 1,
+ 'amount': installment.get('amount', 0),
+ 'due_date': due_date,
+ 'days_until': days_until_due,
+ 'is_installment': True
+ })
+ except:
+ continue
+ else:
+ # Check single payment due date
+ payment_due_date_str = vendor.get('payment_due_date')
+ if payment_due_date_str:
+ try:
+ due_date = datetime.fromisoformat(payment_due_date_str).date()
+ days_until_due = (due_date - today).days
+
+ # Only show if not fully paid
+ total_cost = vendor.get('total_cost', vendor.get('cost', 0))
+ deposit_paid = vendor.get('deposit_paid', 0)
+
+ if deposit_paid < total_cost and 0 <= days_until_due <= 30:
+ upcoming_payments.append({
+ 'vendor_name': vendor.get('name', ''),
+ 'installment_num': None,
+ 'amount': total_cost - deposit_paid,
+ 'due_date': due_date,
+ 'days_until': days_until_due,
+ 'is_installment': False
+ })
+ except:
+ continue
+
+ # Sort by days until due (most urgent first)
+ upcoming_payments.sort(key=lambda x: x['days_until'])
+
+ # Display summary if there are upcoming payments
+ if upcoming_payments:
+ st.markdown("### 🔔 Upcoming Payments (Next 30 Days)")
+
+ # Create a simple table
+ import pandas as pd
+
+ # Prepare data for the table
+ table_data = []
+ for payment in upcoming_payments:
+ # Format payment description
+ if payment['is_installment']:
+ payment_desc = f"{payment['vendor_name']} - Installment {payment['installment_num']}"
+ else:
+ payment_desc = f"{payment['vendor_name']} - Final Payment"
+
+ # Format due date
+ if payment['days_until'] == 0:
+ due_text = "🟠 Today"
+ elif payment['days_until'] == 1:
+ due_text = "🟡 Tomorrow"
+ elif payment['days_until'] <= 3:
+ due_text = f"🟡 {payment['days_until']} days"
+ else:
+ due_text = f"🟢 {payment['days_until']} days"
+
+ table_data.append({
+ 'Vendor & Payment': payment_desc,
+ 'Amount': f"${payment['amount']:,.0f}",
+ 'Due': due_text
+ })
+
+ # Create and display the table
+ df = pd.DataFrame(table_data)
+ st.dataframe(df, use_container_width=True, hide_index=True)
+
+ def render(self, config):
+ st.markdown("## Vendor & Item Management")
+
+ # Load vendors
+ vendors = self.config_manager.load_json_data('vendors.json')
+
+ # Show upcoming payments summary at the top
+ self.show_upcoming_payments_summary(vendors)
+
+ # Ensure all vendors have payment_history field (backward compatibility)
+ vendors_updated = False
+ for vendor in vendors:
+ if 'payment_history' not in vendor:
+ vendor['payment_history'] = []
+ vendors_updated = True
+
+ # Migrate existing deposits to payment history
+ if self.migrate_deposits_to_payment_history(vendors):
+ vendors_updated = True
+
+ # Sync paid installments to payment history
+ # DISABLED: This function was causing deleted payments to be recreated
+ # if self.sync_paid_installments_to_payment_history(vendors):
+ # vendors_updated = True
+
+ # Clean up any duplicate payment records
+ # DISABLED: This function was causing deleted payments to be recreated
+ # if self.cleanup_duplicate_payments(vendors):
+ # vendors_updated = True
+
+ # Save updated vendors if any were modified
+ if vendors_updated:
+ self.config_manager.save_json_data('vendors.json', vendors)
+
+ # Payment notifications are now handled in show_upcoming_payments_summary
+
+ # Add new vendor/item section
+ with st.expander("Add New Vendor or Item", expanded=False):
+ self.render_vendor_form()
+
+ # Vendor/Item summary
+ if vendors:
+ # Separate vendors and items
+ vendor_items = [v for v in vendors if v.get('type', 'Vendor/Service') == 'Vendor/Service']
+ purchased_items = [v for v in vendors if v.get('type') == 'Item/Purchase']
+
+ col1, col2, col3, col4, col5 = st.columns(5)
+ with col1:
+ st.metric("Total Vendors", len(vendor_items))
+ with col2:
+ st.metric("Total Items", len(purchased_items))
+ with col3:
+ booked_count = len([v for v in vendor_items if v.get('status') == 'Booked'])
+ delivered_count = len([v for v in purchased_items if v.get('status') == 'Delivered'])
+ st.metric("Booked/Delivered", booked_count + delivered_count)
+ with col4:
+ pending_count = len([v for v in vendor_items if v.get('status') == 'Pending'])
+ ordered_count = len([v for v in purchased_items if v.get('status') == 'Ordered'])
+ st.metric("Pending/Ordered", pending_count + ordered_count)
+ with col5:
+ # Calculate both cost metrics
+ total_estimated_cost = sum([v.get('total_cost', v.get('cost', 0)) for v in vendors])
+ total_actualized_cost = self.calculate_actualized_cost(vendors)
+ st.metric("Total Estimated", f"${total_estimated_cost:,.0f}")
+
+ # Add prominent cost breakdown section
+ st.markdown("---")
+ col1, col2, col3 = st.columns(3)
+
+ with col1:
+ st.markdown("### 💰 Total Estimated Cost")
+ st.markdown(f"**${total_estimated_cost:,.0f}**")
+ st.caption("All vendors & items regardless of status")
+
+ with col2:
+ st.markdown("### ✅ Actualized Cost")
+ st.markdown(f"**${total_actualized_cost:,.0f}**")
+ st.caption("Only booked/ordered/delivered items")
+
+ with col3:
+ pending_cost = total_estimated_cost - total_actualized_cost
+ st.markdown("### ⏳ Pending Cost")
+ st.markdown(f"**${pending_cost:,.0f}**")
+ st.caption("Estimated costs not yet confirmed")
+
+ # Event-based breakdown
+ self.render_event_breakdown(vendors)
+
+ # Payer breakdown
+ self.render_payer_breakdown(vendors)
+
+ # Filters
+ col1, col2, col3, col4, col5, col6 = st.columns(6)
+
+ with col1:
+ type_filter = st.selectbox("Filter by Type", ["All", "Vendor/Service", "Item/Purchase"])
+
+ with col2:
+ category_filter = st.selectbox("Filter by Category", ["All"] + self.get_vendor_categories(vendors))
+
+ with col3:
+ status_filter = st.selectbox("Filter by Status", ["All", "Booked", "Pending", "Researching", "Ordered", "Shipped", "Delivered", "Not Needed"])
+
+ with col4:
+ # Event filter
+ config = self.config_manager.load_config()
+ events = config.get('wedding_events', [])
+ event_names = [event['name'] for event in events]
+ event_filter = st.selectbox("Filter by Event", ["All"] + event_names)
+
+ with col5:
+ payer_filter = st.selectbox("Filter by Payer", ["All"] + self.get_unique_payers(vendors))
+
+ with col6:
+ search_term = st.text_input("Search", placeholder="Search by name or service")
+
+ # Filter vendors
+ filtered_vendors = self.filter_vendors(vendors, type_filter, category_filter, status_filter, event_filter, payer_filter, search_term)
+
+ # Display vendors/items
+ if filtered_vendors:
+ st.markdown(f"### Vendors & Items ({len(filtered_vendors)} of {len(vendors)} total)")
+
+ # View toggle
+ view_mode = st.radio("View Mode", ["Card View", "Table View"], horizontal=True)
+
+ if view_mode == "Table View":
+ self.render_vendor_table(filtered_vendors)
+ else:
+ for vendor in filtered_vendors:
+ self.render_vendor_card(vendor)
+ else:
+ st.info("No vendors or items found. Add your first vendor or item above!")
+
+ def render_vendor_form(self):
+ # Type selection outside of form
+ item_type = st.radio("Type", ["Vendor/Service", "Item/Purchase"], horizontal=True)
+
+ if item_type == "Vendor/Service":
+ # Event selection outside of form for vendors
+ config = self.config_manager.load_config()
+ events = config.get('wedding_events', [])
+ event_names = [event['name'] for event in events]
+
+ if event_names:
+ selected_events = st.multiselect(
+ "Events *",
+ event_names,
+ help="Select which events this vendor will be providing services for. Vendors can be associated with multiple events."
+ )
+ else:
+ selected_events = []
+ st.info("No events configured yet. Please add events in the wedding configuration first.")
+
+ # Single cost structure for all vendors - cost is split equally across events
+ self.render_vendor_service_form(selected_events)
+ else:
+ # Event selection outside of form for items
+ config = self.config_manager.load_config()
+ events = config.get('wedding_events', [])
+ event_names = [event['name'] for event in events]
+
+ if event_names:
+ selected_events = st.multiselect(
+ "Events *",
+ event_names,
+ help="Select which events this item will be used for. Items can be associated with multiple events."
+ )
+ else:
+ selected_events = []
+ st.info("No events configured yet. Please add events in the wedding configuration first.")
+
+ self.render_item_purchase_form(selected_events)
+
+ def render_vendor_service_form(self, selected_events):
+ # Payment installments selection outside of form
+ st.markdown("#### Payment Structure")
+ use_installments = st.checkbox("Use payment installments", help="Check this if payments are made in multiple installments", key="flat_use_installments")
+
+ # Number of installments outside of form for dynamic updates
+ num_installments = 2
+ if use_installments:
+ num_installments = st.number_input("Number of Installments", min_value=2, max_value=10, value=2, step=1, key="num_installments_flat")
+
+ with st.form("vendor_service_form"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ name = st.text_input("Vendor Name *", placeholder="Enter vendor name")
+ category = st.multiselect("Categories", [
+ "Venue", "Catering", "Photography", "Videography", "Music/DJ", "Entertainment",
+ "Flowers", "Decor", "Attire", "Hair & Makeup", "Transportation",
+ "Invitations", "Cake", "Officiant", "Lodging", "Other"
+ ], help="Select one or more categories that apply to this vendor")
+ contact_person = st.text_input("Contact Person", placeholder="Enter contact person name")
+ phone = st.text_input("Phone", placeholder="Enter phone number")
+
+ with col2:
+ email = st.text_input("Email", placeholder="Enter email address")
+ website = st.text_input("Website", placeholder="Enter website URL")
+ address = st.text_area("Address", placeholder="Enter full address")
+ status = st.selectbox("Status", ["Researching", "Contacted", "Pending", "Booked", "Not Needed"])
+
+ # Cost and payment information
+ st.markdown("#### Cost & Payment Information")
+ st.info("💡 **Cost Allocation:** The total cost will be split equally across all selected events.")
+
+ col3, col4 = st.columns(2)
+ with col3:
+ total_cost = st.number_input("Total Cost", min_value=0.0, value=0.0, step=100.0)
+ deposit_paid = st.number_input("Amount Paid", min_value=0.0, value=0.0, step=100.0)
+ deposit_paid_date = st.date_input("Payment Date", value=None, help="When was this payment made?")
+ payment_method = st.selectbox("Payment Method", ["Not Specified", "Check", "Credit Card", "Bank Transfer", "Cash", "PayPal", "Venmo", "Zelle", "Reimbursement", "Credit", "Other"])
+ with col4:
+ # Only show payment due date if not using installments
+ if not use_installments:
+ payment_due_date = st.date_input("Payment Due Date", value=None)
+ else:
+ payment_due_date = None
+ payer = st.text_input("Payer", placeholder="Who is paying for this vendor?")
+ payment_notes = st.text_area("Payment Notes", placeholder="Additional notes about payment method or process", height=100)
+
+ # Initialize payment_installments list
+ payment_installments = []
+
+ # Payment installments section (inside form but controlled by outside checkbox)
+ if use_installments:
+ st.markdown("#### Payment Installments")
+ st.info("💡 **Payment Installments:** Set up multiple payment dates and amounts. The system will track each installment and remind you when payments are due.")
+
+ # Create a fixed number of installment inputs (up to 10)
+ for i in range(10):
+ if i < num_installments:
+ st.markdown(f"**Installment {i+1}:**")
+ col_inst1, col_inst2, col_inst3 = st.columns(3)
+
+ with col_inst1:
+ installment_amount = st.number_input(
+ f"Amount {i+1}",
+ min_value=0.0,
+ value=(total_cost - deposit_paid)/num_installments if total_cost > 0 else 0.0,
+ step=100.0,
+ key=f"flat_installment_amount_{i}"
+ )
+
+ with col_inst2:
+ installment_date = st.date_input(
+ f"Due Date {i+1}",
+ value=None,
+ key=f"flat_installment_date_{i}"
+ )
+
+ with col_inst3:
+ installment_paid = st.checkbox(
+ f"Paid {i+1}",
+ value=False,
+ key=f"flat_installment_paid_{i}"
+ )
+
+ payment_installments.append({
+ 'amount': installment_amount,
+ 'due_date': installment_date.isoformat() if installment_date else None,
+ 'paid': installment_paid,
+ 'paid_date': None
+ })
+ else:
+ # Single payment structure
+ payment_installments = [{
+ 'amount': total_cost,
+ 'due_date': payment_due_date.isoformat() if payment_due_date else None,
+ 'paid': deposit_paid >= total_cost if total_cost > 0 else False,
+ 'paid_date': None
+ }]
+
+ # Set the same payer for all events (cost is split equally across events)
+ event_costs = {}
+ event_payers = {}
+ for event in selected_events:
+ event_costs[event] = 0 # Cost is stored in total_cost, not per-event
+ event_payers[event] = payer
+
+ notes = st.text_area("Notes", placeholder="Additional notes about this vendor")
+ submitted = st.form_submit_button("Add Vendor", type="primary")
+
+ if submitted:
+ if name and selected_events:
+ new_vendor = {
+ 'id': datetime.now().strftime("%Y%m%d_%H%M%S"),
+ 'name': name,
+ 'type': 'Vendor/Service',
+ 'categories': category, # Store as list of categories
+ 'category': category[0] if category else '', # Keep backward compatibility
+ 'contact_person': contact_person,
+ 'phone': phone,
+ 'email': email,
+ 'website': website,
+ 'address': address,
+ 'status': status,
+ 'events': selected_events,
+ 'event_costs': event_costs,
+ 'event_payers': event_payers,
+ 'total_cost': total_cost,
+ 'deposit_paid': deposit_paid,
+ 'deposit_paid_date': deposit_paid_date.isoformat() if deposit_paid_date else None,
+ 'payment_due_date': payment_due_date.isoformat() if payment_due_date else None,
+ 'payment_method': payment_method,
+ 'payment_notes': payment_notes,
+ 'payment_installments': payment_installments,
+ 'payment_history': self.create_initial_payment_history(deposit_paid, deposit_paid_date, payment_method, payment_notes),
+ 'notes': notes,
+ 'created_date': datetime.now().isoformat()
+ }
+
+ # Load existing vendors and add new one
+ vendors = self.config_manager.load_json_data('vendors.json')
+ vendors.append(new_vendor)
+
+ if self.config_manager.save_json_data('vendors.json', vendors):
+ st.success("Vendor added successfully!")
+ st.rerun()
+ else:
+ st.error("Error saving vendor")
+ elif not name:
+ st.error("Please enter a vendor name")
+ elif not selected_events:
+ st.error("Please select at least one event for this vendor")
+
+
+ def render_item_purchase_form(self, selected_events):
+ # Payment installments selection outside of form
+ st.markdown("#### Payment Structure")
+ use_installments = st.checkbox("Use payment installments", help="Check this if payments are made in multiple installments", key="item_use_installments")
+
+ # Number of installments outside of form for dynamic updates
+ num_installments = 2
+ if use_installments:
+ num_installments = st.number_input("Number of Installments", min_value=2, max_value=10, value=2, step=1, key="num_installments_item")
+
+ with st.form("item_purchase_form"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ name = st.text_input("Item Name *", placeholder="Enter item name")
+ category = st.multiselect("Categories", [
+ "Decorations", "Centerpieces", "Favors", "Signage", "Linens",
+ "Tableware", "Lighting", "Flowers", "Attire", "Accessories",
+ "Invitations", "Stationery", "Gifts", "Other"
+ ], help="Select one or more categories that apply to this item")
+ quantity = st.number_input("Quantity", min_value=1, value=1, step=1)
+ unit_cost = st.number_input("Unit Cost", min_value=0.0, value=0.0, step=100.0, format="%.2f")
+
+ with col2:
+ status = st.selectbox("Status", ["Researching", "Ordered", "Shipped", "Delivered", "Not Needed"])
+ # Calculate total cost from quantity and unit cost
+ total_item_cost = quantity * unit_cost
+ st.info(f"**Total Item Cost: ${total_item_cost:,.2f}**")
+
+ # Seller contact information section
+ st.markdown("#### Seller Contact Information")
+ col3, col4 = st.columns(2)
+
+ with col3:
+ seller_phone = st.text_input("Seller Phone", placeholder="Enter seller phone number")
+ seller_email = st.text_input("Seller Email", placeholder="Enter seller email address")
+
+ with col4:
+ seller_website = st.text_input("Seller Website", placeholder="Enter seller website URL")
+
+ # Cost and payment information
+ st.markdown("#### Cost & Payment Information")
+ st.info("💡 **Cost Allocation:** The total cost will be split equally across all selected events.")
+
+ col3, col4 = st.columns(2)
+ with col3:
+ total_cost = total_item_cost
+ st.number_input("Total Cost", value=total_cost, disabled=True, help="Calculated from quantity × unit cost")
+ deposit_paid = st.number_input("Amount Paid", min_value=0.0, value=0.0, step=100.0)
+ deposit_paid_date = st.date_input("Payment Date", value=None, help="When was this payment made?")
+ payment_method = st.selectbox("Payment Method", ["Not Specified", "Check", "Credit Card", "Bank Transfer", "Cash", "PayPal", "Venmo", "Zelle", "Reimbursement", "Credit", "Other"])
+ with col4:
+ # Only show payment due date if not using installments
+ if not use_installments:
+ payment_due_date = st.date_input("Payment Due Date", value=None)
+ else:
+ payment_due_date = None
+ payer = st.text_input("Who paid for this item?", placeholder="Who is paying for this item?")
+ payment_notes = st.text_area("Payment Notes", placeholder="Additional notes about payment method or process", height=100)
+
+ # Initialize payment_installments list
+ payment_installments = []
+
+ # Payment installments section (inside form but controlled by outside checkbox)
+ if use_installments:
+ st.markdown("#### Payment Installments")
+ st.info("💡 **Payment Installments:** Set up multiple payment dates and amounts. The system will track each installment and remind you when payments are due.")
+
+ # Create a fixed number of installment inputs (up to 10)
+ for i in range(10):
+ if i < num_installments:
+ st.markdown(f"**Installment {i+1}:**")
+ col_inst1, col_inst2, col_inst3 = st.columns(3)
+
+ with col_inst1:
+ installment_amount = st.number_input(
+ f"Amount {i+1}",
+ min_value=0.0,
+ value=(total_cost - deposit_paid)/num_installments if total_cost > 0 else 0.0,
+ step=100.0,
+ key=f"item_installment_amount_{i}"
+ )
+
+ with col_inst2:
+ installment_date = st.date_input(
+ f"Due Date {i+1}",
+ value=None,
+ key=f"item_installment_date_{i}"
+ )
+
+ with col_inst3:
+ installment_paid = st.checkbox(
+ f"Paid {i+1}",
+ value=False,
+ key=f"item_installment_paid_{i}"
+ )
+
+ payment_installments.append({
+ 'amount': installment_amount,
+ 'due_date': installment_date.isoformat() if installment_date else None,
+ 'paid': installment_paid,
+ 'paid_date': None
+ })
+ else:
+ # Single payment structure
+ payment_installments = [{
+ 'amount': total_cost,
+ 'due_date': payment_due_date.isoformat() if payment_due_date else None,
+ 'paid': deposit_paid >= total_cost if total_cost > 0 else False,
+ 'paid_date': None
+ }]
+
+ # Set the same payer for all events (cost is split equally across events)
+ event_costs = {}
+ event_payers = {}
+ for event in selected_events:
+ event_costs[event] = 0 # Cost is stored in total_cost, not per-event
+ event_payers[event] = payer
+
+ notes = st.text_area("Notes", placeholder="Additional notes about this item")
+ submitted = st.form_submit_button("Add Item", type="primary")
+
+ if submitted:
+ if name and selected_events:
+ new_vendor = {
+ 'id': datetime.now().strftime("%Y%m%d_%H%M%S"),
+ 'name': name,
+ 'type': 'Item/Purchase',
+ 'categories': category, # Store as list of categories
+ 'category': category[0] if category else '', # Keep backward compatibility
+ 'contact_person': "", # Items don't have contact person
+ 'phone': "", # Items don't have phone
+ 'email': "", # Items don't have email
+ 'website': "", # Items don't have website
+ 'address': "", # Items don't have address
+ 'seller_phone': seller_phone, # Seller contact info
+ 'seller_email': seller_email, # Seller contact info
+ 'seller_website': seller_website, # Seller contact info
+ 'status': status,
+ 'events': selected_events,
+ 'event_costs': event_costs,
+ 'event_payers': event_payers,
+ 'total_cost': total_cost,
+ 'deposit_paid': deposit_paid,
+ 'deposit_paid_date': deposit_paid_date.isoformat() if deposit_paid_date else None,
+ 'payment_due_date': payment_due_date.isoformat() if payment_due_date else None,
+ 'payment_method': payment_method,
+ 'payment_notes': payment_notes,
+ 'payment_installments': payment_installments,
+ 'payment_history': self.create_initial_payment_history(deposit_paid, deposit_paid_date, payment_method, payment_notes),
+ 'notes': notes,
+ 'quantity': quantity,
+ 'unit_cost': unit_cost,
+ 'created_date': datetime.now().isoformat()
+ }
+
+ # Load existing vendors and add new one
+ vendors = self.config_manager.load_json_data('vendors.json')
+ vendors.append(new_vendor)
+
+ if self.config_manager.save_json_data('vendors.json', vendors):
+ st.success("Item added successfully!")
+ st.rerun()
+ else:
+ st.error("Error saving item")
+ elif not name:
+ st.error("Please enter an item name")
+ elif not selected_events:
+ st.error("Please select at least one event for this item")
+
+ def filter_vendors(self, vendors, type_filter, category_filter, status_filter, event_filter, payer_filter, search_term):
+ filtered = vendors.copy()
+
+ # Type filter
+ if type_filter != "All":
+ filtered = [vendor for vendor in filtered if vendor.get('type', 'Vendor/Service') == type_filter]
+
+ # Category filter - check both single category and multiple categories
+ if category_filter != "All":
+ filtered = [vendor for vendor in filtered if
+ vendor.get('category') == category_filter or
+ category_filter in vendor.get('categories', [])]
+
+ # Status filter
+ if status_filter != "All":
+ filtered = [vendor for vendor in filtered if vendor.get('status') == status_filter]
+
+ # Event filter
+ if event_filter != "All":
+ filtered = [vendor for vendor in filtered if event_filter in vendor.get('events', [])]
+
+ # Payer filter
+ if payer_filter != "All":
+ filtered = [vendor for vendor in filtered if self.get_payer(vendor) == payer_filter]
+
+ # Search filter
+ if search_term:
+ search_lower = search_term.lower()
+ filtered = [vendor for vendor in filtered if
+ search_lower in vendor.get('name', '').lower() or
+ search_lower in vendor.get('category', '').lower() or
+ search_lower in vendor.get('contact_person', '').lower() or
+ any(search_lower in cat.lower() for cat in vendor.get('categories', []) if cat)]
+
+ return filtered
+
+ def get_vendor_categories(self, vendors):
+ categories = set()
+ for vendor in vendors:
+ # Check single category field (backward compatibility)
+ category = vendor.get('category', '')
+ if category:
+ categories.add(category)
+
+ # Check multiple categories field
+ categories_list = vendor.get('categories', [])
+ if categories_list and isinstance(categories_list, list):
+ for cat in categories_list:
+ if cat:
+ categories.add(cat)
+ return sorted(list(categories))
+
+ def get_unique_payers(self, vendors):
+ """Get unique list of payers from vendor data"""
+ payers = set()
+ for vendor in vendors:
+ event_payers = vendor.get('event_payers', {})
+ if event_payers:
+ # Get the payer from any event (all events should have same payer)
+ payer = list(event_payers.values())[0]
+ if payer and payer.strip():
+ payers.add(payer)
+
+ return sorted(list(payers))
+
+ def render_event_breakdown(self, vendors):
+ """Render event-based vendor breakdown"""
+ config = self.config_manager.load_config()
+ events = config.get('wedding_events', [])
+
+ if not events:
+ return
+
+ st.markdown("### Vendors & Items by Event")
+ st.info("💡 **Cost Allocation:** All vendors and items have their costs split equally across all events they serve. This provides a more accurate per-event cost breakdown.")
+
+ # Create event breakdown data
+ event_data = {}
+ for event in events:
+ event_name = event['name']
+ event_vendors = [v for v in vendors if event_name in v.get('events', [])]
+
+ # Calculate cost for this specific event
+ event_cost = 0
+ for vendor in event_vendors:
+ vendor_type = vendor.get('type', 'Vendor/Service')
+ vendor_events = vendor.get('events', [])
+ num_events = len(vendor_events) if vendor_events else 1
+
+ # All vendors and items now have their cost split equally across events
+ total_cost = vendor.get('total_cost', vendor.get('cost', 0))
+
+ # Only include cost if vendor is booked or item is ordered/delivered
+ if vendor_type == 'Vendor/Service':
+ if vendor.get('status') == 'Booked':
+ event_cost += total_cost / num_events
+ elif vendor_type == 'Item/Purchase':
+ status = vendor.get('status', 'Researching')
+ if status in ['Ordered', 'Shipped', 'Delivered']:
+ event_cost += total_cost / num_events
+
+ # Count booked vendors and delivered items
+ booked_delivered_count = 0
+ for v in event_vendors:
+ vendor_type = v.get('type', 'Vendor/Service')
+ if vendor_type == 'Vendor/Service' and v.get('status') == 'Booked':
+ booked_delivered_count += 1
+ elif vendor_type == 'Item/Purchase' and v.get('status') == 'Delivered':
+ booked_delivered_count += 1
+
+ event_data[event_name] = {
+ 'total_vendors': len(event_vendors),
+ 'booked_vendors': booked_delivered_count,
+ 'total_cost': event_cost
+ }
+
+ # Display event breakdown in a table
+ import pandas as pd
+
+ # Create table data
+ table_data = []
+ for event_name, data in event_data.items():
+ table_data.append({
+ 'Event': event_name,
+ 'Total Vendors/Items': data['total_vendors'],
+ 'Booked/Delivered': data['booked_vendors'],
+ 'Total Cost': f"${data['total_cost']:,.0f}"
+ })
+
+ if table_data:
+ df = pd.DataFrame(table_data)
+ st.dataframe(df, use_container_width=True, hide_index=True)
+
+ def render_event_cost_details(self, events, event_costs, event_payers):
+ """Render detailed cost and payer information for each event"""
+ if not events:
+ return
+
+ # Show the payer if available (all events have same payer)
+ if event_payers:
+ payer = list(event_payers.values())[0] # All events have same payer
+ if payer:
+ st.markdown(f"**Payer:** {payer}")
+
+ def calculate_actualized_cost(self, vendors):
+ """Calculate total cost for only booked/ordered/delivered items"""
+ total_cost = 0
+
+ for vendor in vendors:
+ vendor_type = vendor.get('type', 'Vendor/Service')
+ vendor_total_cost = vendor.get('total_cost', vendor.get('cost', 0))
+
+ # Only include cost if vendor is booked or item is ordered/delivered
+ should_include = False
+ if vendor_type == 'Vendor/Service':
+ if vendor.get('status') == 'Booked':
+ should_include = True
+ elif vendor_type == 'Item/Purchase':
+ status = vendor.get('status', 'Researching')
+ if status in ['Ordered', 'Shipped', 'Delivered']:
+ should_include = True
+
+ if should_include:
+ total_cost += vendor_total_cost
+
+ return total_cost
+
+ def render_payer_breakdown(self, vendors):
+ """Render payer-based cost breakdown showing both actualized and estimated costs"""
+ payer_data = {}
+
+ for vendor in vendors:
+ event_costs = vendor.get('event_costs', {})
+ event_payers = vendor.get('event_payers', {})
+ events = vendor.get('events', [])
+
+ vendor_type = vendor.get('type', 'Vendor/Service')
+ total_cost = vendor.get('total_cost', vendor.get('cost', 0))
+ deposit_paid = vendor.get('deposit_paid', 0)
+
+ # Get payer (all events have same payer)
+ if event_payers:
+ payer = list(event_payers.values())[0]
+ else:
+ payer = 'Not specified'
+
+ # Initialize payer data if not exists
+ if payer not in payer_data:
+ payer_data[payer] = {
+ 'actualized_cost': 0,
+ 'estimated_cost': 0,
+ 'total_paid': 0,
+ 'vendor_count': 0,
+ 'events': set()
+ }
+
+ # Always add to estimated cost (all vendors/items regardless of status)
+ payer_data[payer]['estimated_cost'] += total_cost
+
+ # Calculate total paid from payment history instead of just deposit_paid
+ payment_history = vendor.get('payment_history', [])
+ total_paid_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') != 'credit')
+ total_credits_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') == 'credit')
+ net_paid = total_paid_from_history - total_credits_from_history
+
+ payer_data[payer]['total_paid'] += net_paid
+ payer_data[payer]['vendor_count'] += 1
+ payer_data[payer]['events'].update(events)
+
+ # Only add to actualized cost if vendor is booked or item is ordered/delivered
+ should_include_actualized = False
+ if vendor_type == 'Vendor/Service':
+ if vendor.get('status') == 'Booked':
+ should_include_actualized = True
+ elif vendor_type == 'Item/Purchase':
+ status = vendor.get('status', 'Researching')
+ if status in ['Ordered', 'Shipped', 'Delivered']:
+ should_include_actualized = True
+
+ if should_include_actualized:
+ payer_data[payer]['actualized_cost'] += total_cost
+
+ if payer_data:
+ st.markdown("### Costs by Payer")
+ st.info("💡 **Note:** Shows both actualized costs (confirmed) and estimated costs (all items) for each payer. ")
+
+ # Display payer breakdown in a table
+ import pandas as pd
+
+ # Create table data
+ table_data = []
+ for payer, data in payer_data.items():
+ # Calculate balances for both actualized and estimated
+ actualized_balance = data['actualized_cost'] - data['total_paid']
+ estimated_balance = data['estimated_cost'] - data['total_paid']
+ pending_cost = data['estimated_cost'] - data['actualized_cost']
+
+ table_data.append({
+ 'Payer': payer,
+ 'Actualized Cost': f"${data['actualized_cost']:,.0f}",
+ 'Estimated Cost': f"${data['estimated_cost']:,.0f}",
+ 'Pending Cost': f"${pending_cost:,.0f}",
+ 'Net Amount Paid': f"${data['total_paid']:,.0f}",
+ 'Actualized Balance': f"${actualized_balance:,.0f}",
+ 'Estimated Balance': f"${estimated_balance:,.0f}",
+ 'Vendors/Items': data['vendor_count']
+ })
+
+ df = pd.DataFrame(table_data)
+ st.dataframe(df, use_container_width=True, hide_index=True)
+
+ def render_vendor_table(self, vendors):
+ """Render vendors in a comprehensive table format"""
+ import pandas as pd
+
+ # Prepare table data
+ table_data = []
+ for vendor in vendors:
+ # Basic info
+ name = vendor.get('name', '')
+ item_type = vendor.get('type', 'Vendor/Service')
+ category = self.get_category_display_text(vendor)
+ status = vendor.get('status', 'Researching')
+
+ # Cost info
+ total_cost = vendor.get('total_cost', vendor.get('cost', 0))
+ deposit_paid = vendor.get('deposit_paid', 0)
+ # Calculate remaining balance using payment history for accuracy
+ payment_history = vendor.get('payment_history', [])
+ total_paid_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') != 'credit')
+ total_credits_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') == 'credit')
+ remaining_balance = total_cost - total_paid_from_history + total_credits_from_history
+
+ # Payment info
+ payment_due_date = vendor.get('payment_due_date', '')
+ payment_method = vendor.get('payment_method', 'Not Specified')
+ payment_notes = vendor.get('payment_notes', '')
+
+ # Determine payment due display - check for installments first, then single payment
+ payment_installments = vendor.get('payment_installments', [])
+ next_installment_due = None
+
+ # Check for upcoming unpaid installments
+ if payment_installments and len(payment_installments) > 1:
+ for installment in payment_installments:
+ if not installment.get('paid', False):
+ next_installment_due = installment.get('due_date', '')
+ break
+
+ # Determine what to display
+ if not payment_due_date and remaining_balance <= 0:
+ payment_due_display = 'Fully Paid'
+ elif next_installment_due:
+ payment_due_display = next_installment_due
+ elif payment_due_date:
+ payment_due_display = payment_due_date
+ else:
+ payment_due_display = 'Not Set'
+
+ # Payment history
+ payment_history = vendor.get('payment_history', [])
+ payment_count = len(payment_history)
+
+ # Event payers
+ event_payers = vendor.get('event_payers', {})
+ payers_str = ', '.join(set(event_payers.values())) if event_payers else 'Not Specified'
+
+ # Contact info
+ contact_person = vendor.get('contact_person', '')
+ phone = vendor.get('phone', '')
+ email = vendor.get('email', '')
+ website = vendor.get('website', '')
+
+ # Seller contact info for items
+ seller_phone = vendor.get('seller_phone', '')
+ seller_email = vendor.get('seller_email', '')
+ seller_website = vendor.get('seller_website', '')
+
+ # Events
+ events = vendor.get('events', [])
+ events_str = ', '.join(events) if events else 'None'
+
+ # Additional fields for items
+ quantity = vendor.get('quantity', '')
+ unit_cost = vendor.get('unit_cost', '')
+
+ # Create row data
+ row_data = {
+ 'Name': name,
+ 'Type': item_type,
+ 'Category': category,
+ 'Status': status,
+ 'Events': events_str,
+ 'Total Cost': f"${total_cost:,.0f}" if total_cost > 0 else 'Not Set',
+ 'Net Amount Paid': f"${total_paid_from_history:,.0f}" if total_paid_from_history > 0 else '$0',
+ 'Remaining': f"${remaining_balance:,.0f}" if remaining_balance > 0 else '$0',
+ 'Payment Due': payment_due_display,
+ 'Payment Method': payment_method,
+ 'Payments': f"{payment_count} payment(s)" if payment_count > 0 else 'No payments',
+ 'Payer(s)': payers_str,
+ 'Payment Notes': payment_notes,
+ 'Contact Person': contact_person,
+ 'Phone': phone,
+ 'Email': email,
+ 'Website': website,
+ 'Seller Phone': seller_phone,
+ 'Seller Email': seller_email,
+ 'Seller Website': seller_website
+ }
+
+ # Add item-specific fields if it's an item
+ if item_type == 'Item/Purchase':
+ row_data['Quantity'] = quantity if quantity else '1'
+ row_data['Unit Cost'] = f"${unit_cost:,.0f}" if unit_cost else 'Not Set'
+
+ table_data.append(row_data)
+
+ if table_data:
+ df = pd.DataFrame(table_data)
+
+ # Configure column display with proper text wrapping and standard widths
+ column_config = {
+ 'Name': st.column_config.TextColumn('Name', width=200, help="Vendor or item name"),
+ 'Type': st.column_config.TextColumn('Type', width=120, help="Vendor/Service or Item/Purchase"),
+ 'Category': st.column_config.TextColumn('Category', width=150, help="Category of vendor or item"),
+ 'Status': st.column_config.TextColumn('Status', width=120, help="Current status"),
+ 'Events': st.column_config.TextColumn('Events', width=200, help="Associated events"),
+ 'Total Cost': st.column_config.TextColumn('Total Cost', width=120, help="Total cost amount"),
+ 'Amount Paid': st.column_config.TextColumn('Amount Paid', width=120, help="Total amount paid"),
+ 'Remaining': st.column_config.TextColumn('Remaining', width=120, help="Remaining balance"),
+ 'Payment Due': st.column_config.TextColumn('Due Date', width=120, help="Payment due date"),
+ 'Payment Method': st.column_config.TextColumn('Payment Method', width=150, help="Payment method used"),
+ 'Payments': st.column_config.TextColumn('Payments', width=120, help="Number of payments made"),
+ 'Payment Notes': st.column_config.TextColumn('Payment Notes', width=250, help="Additional payment notes"),
+ 'Contact Person': st.column_config.TextColumn('Contact', width=150, help="Primary contact person"),
+ 'Phone': st.column_config.TextColumn('Phone', width=150, help="Phone number"),
+ 'Email': st.column_config.TextColumn('Email', width=200, help="Email address"),
+ 'Website': st.column_config.TextColumn('Website', width=150, help="Website URL"),
+ 'Seller Phone': st.column_config.TextColumn('Seller Phone', width=150, help="Seller phone number"),
+ 'Seller Email': st.column_config.TextColumn('Seller Email', width=200, help="Seller email address"),
+ 'Seller Website': st.column_config.TextColumn('Seller Website', width=150, help="Seller website URL"),
+ 'Payer(s)': st.column_config.TextColumn('Payer(s)', width=150, help="Who is paying for this"),
+ 'Quantity': st.column_config.TextColumn('Qty', width=80, help="Quantity of items"),
+ 'Unit Cost': st.column_config.TextColumn('Unit Cost', width=120, help="Cost per unit")
+ }
+
+ # Use custom HTML table for proper text wrapping
+ self.render_custom_table(df)
+ else:
+ st.info("No vendor data to display")
+
+ def render_custom_table(self, df):
+ """Render a custom HTML table with proper text wrapping"""
+ import pandas as pd
+
+ # Create HTML table with proper text wrapping
+ html = """
+
+
+
+
+
+
+ """
+
+ # Add column headers
+ for col in df.columns:
+ html += f"| {col} | "
+ html += "
"
+
+ # Add data rows
+ for _, row in df.iterrows():
+ html += ""
+ for col in df.columns:
+ value = str(row[col]) if pd.notna(row[col]) else ""
+ html += f"| {value} | "
+ html += "
"
+
+ html += "
"
+
+ st.markdown(html, unsafe_allow_html=True)
+
+ def render_vendor_card(self, vendor):
+ vendor_id = vendor.get('id', '')
+ name = vendor.get('name', '')
+ item_type = vendor.get('type', 'Vendor/Service')
+ category = self.get_category_display_text(vendor)
+ contact_person = vendor.get('contact_person', '')
+ phone = vendor.get('phone', '')
+ email = vendor.get('email', '')
+ website = vendor.get('website', '')
+ address = vendor.get('address', '')
+ status = vendor.get('status', 'Researching')
+ events = vendor.get('events', [])
+ event_costs = vendor.get('event_costs', {})
+ event_payers = vendor.get('event_payers', {})
+ total_cost = vendor.get('total_cost', vendor.get('cost', 0))
+ deposit_paid = vendor.get('deposit_paid', 0)
+ payment_due_date = vendor.get('payment_due_date', '')
+ payment_method = vendor.get('payment_method', 'Not Specified')
+ payment_notes = vendor.get('payment_notes', '')
+ notes = vendor.get('notes', '')
+
+ # Item-specific fields
+ quantity = vendor.get('quantity', 1)
+ unit_cost = vendor.get('unit_cost', 0)
+
+ # Calculate remaining balance - use payment history for accurate total
+ payment_history = vendor.get('payment_history', [])
+ total_paid_amount = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') != 'credit')
+ total_credits = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') == 'credit')
+ remaining_balance = total_cost - total_paid_amount + total_credits
+
+ # Create a visually distinct card with better separation
+ with st.container():
+ # Card header with background styling
+ st.markdown(f"""
+
+
{name} - ${total_cost:,.2f}
+
+ Category: {category} |
+ Type: {item_type} |
+ Status: {status}
+
+
+ """, unsafe_allow_html=True)
+
+ # Basic Information Section
+ with st.expander("📋 Basic Information", expanded=False):
+ # Events
+ if events:
+ st.markdown(f"**Events:** {', '.join(events)}")
+
+ # Contact Information (for vendors only)
+ if item_type == 'Vendor/Service':
+ contact_info = []
+ if contact_person:
+ contact_info.append(f"**Contact Person:** {contact_person}")
+ if phone:
+ contact_info.append(f"**Phone:** {phone}")
+ if email:
+ contact_info.append(f"**Email:** {email}")
+ if website:
+ contact_info.append(f"**Website:** [{website}]({website})")
+ if address:
+ contact_info.append(f"**Address:** {address}")
+
+ if contact_info:
+ for info in contact_info:
+ st.markdown(info)
+
+ # Item-specific information
+ if item_type == 'Item/Purchase':
+ st.markdown(f"**Quantity:** {quantity}")
+ st.markdown(f"**Unit Cost:** ${unit_cost:.2f}")
+ st.markdown(f"**Total Cost:** ${total_cost:.2f}")
+
+ # Seller contact information for items
+ seller_phone = vendor.get('seller_phone', '')
+ seller_email = vendor.get('seller_email', '')
+ seller_website = vendor.get('seller_website', '')
+
+ seller_contact_info = []
+ if seller_phone:
+ seller_contact_info.append(f"**Seller Phone:** {seller_phone}")
+ if seller_email:
+ seller_contact_info.append(f"**Seller Email:** {seller_email}")
+ if seller_website:
+ seller_contact_info.append(f"**Seller Website:** [{seller_website}]({seller_website})")
+
+ if seller_contact_info:
+ st.markdown("##### Seller Contact Information")
+ for info in seller_contact_info:
+ st.markdown(f"{info}", unsafe_allow_html=True)
+
+ # Cost and Payment Information
+ with st.expander("💰 Cost & Payment Information", expanded=False):
+ # Cost metrics in columns
+ cost_col1, cost_col2, cost_col3, cost_col4 = st.columns(4)
+ with cost_col1:
+ st.metric("Total Cost", f"${total_cost:,.0f}")
+ with cost_col2:
+ # Show total paid amount from payment history
+ st.metric("Net Amount Paid", f"${total_paid_amount:,.0f}")
+ with cost_col3:
+ st.metric("Remaining Balance", f"${remaining_balance:,.0f}")
+
+ # Show credits if any, otherwise show payment due date
+ if total_credits > 0:
+ with cost_col4:
+ st.metric("Credits Received", f"${total_credits:,.0f}")
+ else:
+ with cost_col4:
+ # Check for upcoming unpaid installments
+ payment_installments = vendor.get('payment_installments', [])
+ next_installment_due = None
+
+ if payment_installments and len(payment_installments) > 1:
+ for installment in payment_installments:
+ if not installment.get('paid', False):
+ next_installment_due = installment.get('due_date', '')
+ break
+
+ # Determine what to display
+ if not payment_due_date and remaining_balance <= 0:
+ st.metric("Payment Due", "Fully Paid")
+ elif next_installment_due:
+ st.metric("Payment Due", next_installment_due)
+ elif payment_due_date:
+ st.metric("Payment Due", payment_due_date)
+ else:
+ st.metric("Payment Due", "Not Set")
+
+ # Payment details
+ if payment_method and payment_method != 'Not Specified':
+ st.markdown(f"**Payment Method:** {payment_method}")
+
+ # Payer information
+ payer = self.get_payer(vendor)
+ if payer:
+ st.markdown(f"**Payer:** {payer}")
+
+ # Payment notes
+ if payment_notes:
+ st.markdown(f"**Payment Notes:** {payment_notes}")
+
+ # Payment installments display
+ payment_installments = vendor.get('payment_installments', [])
+ if payment_installments and len(payment_installments) > 1:
+ st.markdown("#### Payment Installments")
+
+ # Calculate installment summary
+ total_installment_amount = sum(inst.get('amount', 0) for inst in payment_installments)
+ paid_installments = sum(1 for inst in payment_installments if inst.get('paid', False))
+ # Use payment history for accurate total paid amount
+ total_paid_amount = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') != 'credit')
+ total_credits = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') == 'credit')
+
+ # Show installment summary
+ inst_col1, inst_col2, inst_col3 = st.columns(3)
+ with inst_col1:
+ st.metric("Total Installments", len(payment_installments))
+ with inst_col2:
+ st.metric("Paid Installments", f"{paid_installments}/{len(payment_installments)}")
+ with inst_col3:
+ st.metric("Amount Paid", f"${total_paid_amount:,.0f}")
+
+ # Show individual installments
+ for i, installment in enumerate(payment_installments):
+ amount = installment.get('amount', 0)
+ due_date = installment.get('due_date', '')
+ paid = installment.get('paid', False)
+ paid_date = installment.get('paid_date', '')
+
+ # Determine status and color
+ if paid:
+ status_icon = "✅"
+ status_text = "Paid"
+ if paid_date:
+ status_text += f" ({paid_date})"
+ else:
+ if due_date:
+ try:
+ from datetime import datetime, date
+ due_date_obj = datetime.fromisoformat(due_date).date()
+ today = date.today()
+ if due_date_obj < today:
+ status_icon = "🔴"
+ status_text = "Past Due"
+ elif due_date_obj == today:
+ status_icon = "🟡"
+ status_text = "Due Today"
+ else:
+ status_icon = "⏳"
+ status_text = "Pending"
+ except:
+ status_icon = "⏳"
+ status_text = "Pending"
+ else:
+ status_icon = "⏳"
+ status_text = "Pending"
+
+ # Display installment
+ col_inst1, col_inst2, col_inst3 = st.columns([2, 2, 1])
+ with col_inst1:
+ st.markdown(f"**Installment {i+1}:** ${amount:,.0f}")
+ with col_inst2:
+ st.markdown(f"Due: {due_date if due_date else 'Not Set'}")
+ with col_inst3:
+ st.markdown(f"{status_icon} {status_text}")
+
+ # Payment History
+ payment_history = vendor.get('payment_history', [])
+ if payment_history:
+ with st.expander("💳 Payment History", expanded=False):
+ self.render_payment_history(vendor, context="card")
+
+ # Additional Notes
+ if notes:
+ with st.expander("📝 Notes", expanded=False):
+ st.markdown(notes)
+
+ # Action Buttons Section with better styling
+ st.markdown("""
+
+ """, unsafe_allow_html=True)
+
+ st.markdown("#### ⚙️ Actions")
+
+ # Create action buttons in a clean layout
+ action_col1, action_col2, action_col3, action_col4, action_col5 = st.columns(5)
+
+ with action_col1:
+ edit_clicked = st.button("✏️ Edit", key=f"edit_vendor_{vendor_id}", help=f"Edit {'vendor' if item_type == 'Vendor/Service' else 'item'}", use_container_width=True)
+
+ with action_col2:
+ tasks_clicked = st.button("📋 Tasks", key=f"tasks_vendor_{vendor_id}", help="View/Add Tasks", use_container_width=True)
+
+ with action_col3:
+ status_clicked = st.button("📊 Status", key=f"status_vendor_{vendor_id}", help="Update Status", use_container_width=True)
+
+ with action_col4:
+ if item_type == "Vendor/Service":
+ payment_clicked = st.button("💳 Payment", key=f"payment_vendor_{vendor_id}", help="Record Payment", use_container_width=True)
+ track_clicked = False
+ else:
+ # For items, show both Track and Payment buttons
+ col4a, col4b = st.columns(2)
+ with col4a:
+ track_clicked = st.button("📦 Track", key=f"track_item_{vendor_id}", help="Track Item Status", use_container_width=True)
+ with col4b:
+ payment_clicked = st.button("💳 Payment", key=f"payment_item_{vendor_id}", help="Record Payment", use_container_width=True)
+
+ with action_col5:
+ delete_clicked = st.button("🗑️ Delete", key=f"delete_vendor_{vendor_id}", help=f"Delete {'vendor' if item_type == 'Vendor/Service' else 'item'}", use_container_width=True)
+
+ st.markdown("
", unsafe_allow_html=True)
+
+ # Add spacing between cards
+ st.markdown("
", unsafe_allow_html=True)
+
+ # Check if any modal should be shown
+ if st.session_state.get(f"show_delete_{vendor_id}", False):
+ self.show_delete_confirmation(vendor_id)
+ elif st.session_state.get(f"show_edit_{vendor_id}", False):
+ self.edit_vendor(vendor)
+ elif st.session_state.get(f"show_track_{vendor_id}", False):
+ self.track_item_status(vendor)
+ elif st.session_state.get(f"show_payment_{vendor_id}", False):
+ self.record_payment(vendor)
+ elif st.session_state.get(f"show_tasks_{vendor_id}", False):
+ self.manage_vendor_tasks(vendor)
+ elif st.session_state.get(f"show_status_{vendor_id}", False):
+ self.update_vendor_status(vendor)
+ else:
+ # Handle button clicks
+ if edit_clicked:
+ # Set session state to show edit form
+ st.session_state[f"show_edit_{vendor_id}"] = True
+ st.rerun()
+ elif tasks_clicked:
+ # Set session state to show tasks management
+ st.session_state[f"show_tasks_{vendor_id}"] = True
+ st.rerun()
+ elif status_clicked:
+ # Set session state to show status update form
+ st.session_state[f"show_status_{vendor_id}"] = True
+ st.rerun()
+ elif payment_clicked:
+ # Set session state to show payment form
+ st.session_state[f"show_payment_{vendor_id}"] = True
+ st.rerun()
+ elif track_clicked:
+ # Set session state to show track status form
+ st.session_state[f"show_track_{vendor_id}"] = True
+ st.rerun()
+ elif delete_clicked:
+ # Set session state to show delete confirmation
+ st.session_state[f"show_delete_{vendor_id}"] = True
+ st.rerun()
+
+ def edit_vendor(self, vendor):
+ st.markdown(f"### Edit {vendor.get('name', '')}")
+
+ item_type = vendor.get('type', 'Vendor/Service')
+
+ # Event selection outside of form
+ config = self.config_manager.load_config()
+ events = config.get('wedding_events', [])
+ event_names = [event['name'] for event in events]
+ current_events = vendor.get('events', [])
+
+ if event_names:
+ selected_events = st.multiselect(
+ "Events *",
+ event_names,
+ default=current_events,
+ key=f"edit_events_{vendor.get('id', '')}"
+ )
+ else:
+ selected_events = []
+ st.info("No events configured yet.")
+
+ # Single cost structure for all vendors and items - cost is split equally across events
+ st.info("💡 **Cost Allocation:** The total cost will be split equally across all selected events.")
+
+ # Get existing payment installments
+ existing_installments = vendor.get('payment_installments', [])
+
+ # Payment installments selection outside of form
+ st.markdown("#### Payment Structure")
+ use_installments = st.checkbox(
+ "Use payment installments",
+ value=len(existing_installments) > 1,
+ help="Check this if payments are made in multiple installments. You can switch between single payment and installments.",
+ key=f"edit_use_installments_{vendor.get('id', '')}"
+ )
+
+ # Number of installments outside of form for dynamic updates
+ num_installments = len(existing_installments) if existing_installments else 2
+ if use_installments:
+ num_installments = st.number_input(
+ "Number of Installments",
+ min_value=2,
+ max_value=10,
+ value=len(existing_installments) if existing_installments else 2,
+ step=1,
+ key=f"edit_num_installments_{vendor.get('id', '')}"
+ )
+
+ with st.form(f"edit_form_{vendor.get('id', '')}"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ name = st.text_input("Name *", value=vendor.get('name', ''), key=f"edit_name_{vendor.get('id', '')}")
+
+ # Combined category list for both vendor and item types
+ all_categories = [
+ "Venue", "Catering", "Photography", "Videography", "Music/DJ", "Entertainment",
+ "Flowers", "Decor", "Attire", "Hair & Makeup", "Transportation",
+ "Invitations", "Cake", "Officiant", "Lodging", "Decorations", "Centerpieces",
+ "Favors", "Signage", "Linens", "Tableware", "Lighting", "Accessories",
+ "Stationery", "Gifts", "Other"
+ ]
+
+ default_categories = self.get_default_categories(vendor, item_type)
+ category = st.multiselect("Categories", all_categories,
+ default=default_categories,
+ help="Select one or more categories that apply to this vendor/item",
+ key=f"edit_category_{vendor.get('id', '')}")
+
+ if item_type == 'Vendor/Service':
+ contact_person = st.text_input("Contact Person", value=vendor.get('contact_person', ''), key=f"edit_contact_{vendor.get('id', '')}")
+ phone = st.text_input("Phone", value=vendor.get('phone', ''), key=f"edit_phone_{vendor.get('id', '')}")
+ else:
+ quantity = st.number_input("Quantity", min_value=1, value=vendor.get('quantity', 1), key=f"edit_quantity_{vendor.get('id', '')}")
+ unit_cost = st.number_input("Unit Cost", min_value=0.0, value=float(vendor.get('unit_cost', 0)), step=0.01, format="%.2f", key=f"edit_unit_cost_{vendor.get('id', '')}")
+
+ # Seller contact information for items
+ st.markdown("#### Seller Contact Information")
+ col_seller1, col_seller2 = st.columns(2)
+ with col_seller1:
+ seller_phone = st.text_input("Seller Phone", value=vendor.get('seller_phone', ''), key=f"edit_seller_phone_{vendor.get('id', '')}")
+ seller_email = st.text_input("Seller Email", value=vendor.get('seller_email', ''), key=f"edit_seller_email_{vendor.get('id', '')}")
+ with col_seller2:
+ seller_website = st.text_input("Seller Website", value=vendor.get('seller_website', ''), key=f"edit_seller_website_{vendor.get('id', '')}")
+
+ with col2:
+ if item_type == 'Vendor/Service':
+ email = st.text_input("Email", value=vendor.get('email', ''), key=f"edit_email_{vendor.get('id', '')}")
+ website = st.text_input("Website", value=vendor.get('website', ''), key=f"edit_website_{vendor.get('id', '')}")
+ address = st.text_area("Address", value=vendor.get('address', ''), key=f"edit_address_{vendor.get('id', '')}")
+
+ status_options = ["Researching", "Contacted", "Pending", "Booked", "Not Needed"] if item_type == 'Vendor/Service' else ["Researching", "Ordered", "Shipped", "Delivered", "Not Needed"]
+ current_status = vendor.get('status', 'Researching')
+ status_index = status_options.index(current_status) if current_status in status_options else 0
+ status = st.selectbox("Status", status_options, index=status_index, key=f"edit_status_{vendor.get('id', '')}")
+
+
+ # Cost and payment information
+ st.markdown("#### Cost & Payment Information")
+
+ # Initialize event costs and payers
+ event_costs = {}
+ event_payers = {}
+
+ if item_type == 'Item/Purchase':
+ # Items - calculate total cost from quantity and unit cost
+ total_cost = quantity * unit_cost
+ st.number_input("Total Cost", value=total_cost, disabled=True, help="Calculated from quantity × unit cost")
+ else:
+ # Vendors - use total cost field
+ total_cost = st.number_input("Total Cost", min_value=0.0, value=float(vendor.get('total_cost', 0)), step=100.0, key=f"edit_total_cost_{vendor.get('id', '')}")
+
+ col3, col4 = st.columns(2)
+ with col3:
+ deposit_paid = st.number_input("Deposit Paid", min_value=0.0, value=float(vendor.get('deposit_paid', 0)), step=100.0, key=f"edit_deposit_{vendor.get('id', '')}")
+ deposit_paid_date = st.date_input("Deposit Paid Date", value=self.parse_date(vendor.get('deposit_paid_date', '')), key=f"edit_deposit_date_{vendor.get('id', '')}")
+ payment_method = st.selectbox("Payment Method", ["Not Specified", "Check", "Credit Card", "Bank Transfer", "Cash", "PayPal", "Venmo", "Zelle", "Reimbursement", "Credit", "Other"],
+ index=self.get_payment_method_index(vendor.get('payment_method', 'Not Specified')), key=f"edit_payment_method_{vendor.get('id', '')}")
+
+ with col4:
+ # Only show payment due date if not using installments
+ if not st.session_state.get(f"edit_use_installments_{vendor.get('id', '')}", len(existing_installments) > 1):
+ payment_due_date = st.date_input("Payment Due Date", value=self.parse_date(vendor.get('payment_due_date', '')), key=f"edit_due_date_{vendor.get('id', '')}")
+ else:
+ payment_due_date = None
+ payer = st.text_input("Payer", value=self.get_payer(vendor), key=f"edit_payer_{vendor.get('id', '')}")
+ payment_notes = st.text_area("Payment Notes", value=vendor.get('payment_notes', ''), height=100, key=f"edit_payment_notes_{vendor.get('id', '')}")
+
+ # Set the same payer for all events (cost is split equally across events)
+ for event in selected_events:
+ event_costs[event] = 0 # Cost is stored in total_cost, not per-event
+ event_payers[event] = payer
+
+ # Payment installments section (now handled outside the form)
+ payment_installments = []
+ if use_installments:
+ st.info("💡 **Payment Installments:** Set up multiple payment dates and amounts. The system will track each installment and remind you when payments are due.")
+
+ # Show helpful message if switching from single payment
+ if len(existing_installments) == 1:
+ st.warning("⚠️ **Switching to Installments:** You're changing from a single payment to installments. The existing payment information will be used as a starting point.")
+
+ for i in range(num_installments):
+ st.markdown(f"**Installment {i+1}:**")
+ col_inst1, col_inst2, col_inst3 = st.columns(3)
+
+ # Get existing installment data if available
+ existing_installment = existing_installments[i] if i < len(existing_installments) else {}
+
+ with col_inst1:
+ # Calculate default amount - use existing if available, otherwise split total cost
+ default_amount = existing_installment.get('amount', 0.0)
+ if default_amount == 0.0 and total_cost > 0:
+ default_amount = total_cost / num_installments
+
+ installment_amount = st.number_input(
+ f"Amount {i+1}",
+ min_value=0.0,
+ value=default_amount,
+ step=100.0,
+ key=f"edit_installment_amount_{i}_{vendor.get('id', '')}"
+ )
+
+ with col_inst2:
+ existing_date = None
+ if existing_installment.get('due_date'):
+ try:
+ from datetime import datetime
+ existing_date = datetime.fromisoformat(existing_installment['due_date']).date()
+ except:
+ existing_date = None
+ elif len(existing_installments) == 1 and i == 0:
+ # If switching from single payment, use the existing payment due date for first installment
+ try:
+ from datetime import datetime
+ existing_date = datetime.fromisoformat(existing_installments[0].get('due_date', '')).date() if existing_installments[0].get('due_date') else None
+ except:
+ existing_date = None
+
+ installment_date = st.date_input(
+ f"Due Date {i+1}",
+ value=existing_date,
+ key=f"edit_installment_date_{i}_{vendor.get('id', '')}"
+ )
+
+ with col_inst3:
+ # Determine if this installment should be marked as paid
+ paid_value = existing_installment.get('paid', False)
+ if not paid_value and len(existing_installments) == 1 and i == 0:
+ # If switching from single payment and it was paid, mark first installment as paid
+ paid_value = existing_installments[0].get('paid', False)
+
+ installment_paid = st.checkbox(
+ f"Paid {i+1}",
+ value=paid_value,
+ key=f"edit_installment_paid_{i}_{vendor.get('id', '')}"
+ )
+
+ # Preserve paid_date if installment is paid and has existing paid_date
+ paid_date = existing_installment.get('paid_date', None)
+ if not paid_date and len(existing_installments) == 1 and i == 0 and installment_paid:
+ # If switching from single payment and it was paid, preserve the paid_date
+ paid_date = existing_installments[0].get('paid_date', None)
+
+ payment_installments.append({
+ 'amount': installment_amount,
+ 'due_date': installment_date.isoformat() if installment_date else None,
+ 'paid': installment_paid,
+ 'paid_date': paid_date
+ })
+ else:
+ # Single payment structure
+ if len(existing_installments) > 1:
+ st.warning("⚠️ **Switching to Single Payment:** You're changing from installments to a single payment. The total cost and deposit information will be used.")
+
+ payment_installments = [{
+ 'amount': total_cost,
+ 'due_date': payment_due_date.isoformat() if payment_due_date else None,
+ 'paid': deposit_paid >= total_cost if total_cost > 0 else False,
+ 'paid_date': deposit_paid_date.isoformat() if deposit_paid_date and deposit_paid >= total_cost else None
+ }]
+
+ notes = st.text_area("Notes", value=vendor.get('notes', ''), key=f"edit_notes_{vendor.get('id', '')}")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ submitted = st.form_submit_button("Update", type="primary")
+ with col2:
+ cancel_clicked = st.form_submit_button("Cancel")
+
+ if submitted:
+ if name and selected_events:
+ # Update vendor data
+ vendors = self.config_manager.load_json_data('vendors.json')
+ for v in vendors:
+ if v.get('id') == vendor.get('id'):
+ v['name'] = name
+ v['categories'] = category # Store as list of categories
+ # Keep backward compatibility with single category field
+ v['category'] = category[0] if category else ''
+ v['status'] = status
+ v['events'] = selected_events
+ v['total_cost'] = total_cost
+ v['deposit_paid'] = deposit_paid
+ v['deposit_paid_date'] = deposit_paid_date.isoformat() if deposit_paid_date else None
+ v['payment_due_date'] = payment_due_date.isoformat() if payment_due_date else None
+ v['payment_method'] = payment_method
+ v['payment_notes'] = payment_notes
+ v['notes'] = notes
+
+ # Update cost structure and event costs/payers for all types
+ v['event_costs'] = event_costs
+ v['event_payers'] = event_payers
+ v['payment_installments'] = payment_installments
+ # Update payment history with deposit changes
+ v['payment_history'] = self.update_deposit_in_payment_history(v, deposit_paid, deposit_paid_date, payment_method, payment_notes)
+
+ # Sync paid installments to payment history
+ # DISABLED: This function was causing deleted payments to be recreated
+ # self.sync_paid_installments_to_payment_history([v])
+
+ if item_type == 'Vendor/Service':
+ v['contact_person'] = contact_person
+ v['phone'] = phone
+ v['email'] = email
+ v['website'] = website
+ v['address'] = address
+ else:
+ v['quantity'] = quantity
+ v['unit_cost'] = unit_cost
+ v['seller_phone'] = seller_phone
+ v['seller_email'] = seller_email
+ v['seller_website'] = seller_website
+
+ break
+
+ if self.config_manager.save_json_data('vendors.json', vendors):
+ st.success("Vendor/Item updated successfully!")
+ # Clear the edit session state
+ vendor_id = vendor.get('id')
+ if f"show_edit_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_edit_{vendor_id}"]
+ st.rerun()
+ else:
+ st.error("Error saving changes")
+ elif not name:
+ st.error("Please enter a name")
+ elif not selected_events:
+ st.error("Please select at least one event")
+ elif cancel_clicked:
+ # Clear the edit session state
+ vendor_id = vendor.get('id')
+ if f"show_edit_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_edit_{vendor_id}"]
+ st.rerun()
+
+ def get_category_index(self, category, item_type):
+ """Get the index of a category in the appropriate list"""
+ if item_type == 'Vendor/Service':
+ categories = ["Venue", "Catering", "Photography", "Videography", "Music/DJ", "Entertainment",
+ "Flowers", "Decor", "Attire", "Hair & Makeup", "Transportation",
+ "Invitations", "Cake", "Officiant", "Lodging", "Other"]
+ else:
+ categories = ["Decorations", "Centerpieces", "Favors", "Signage", "Linens",
+ "Tableware", "Lighting", "Flowers", "Attire", "Accessories",
+ "Invitations", "Stationery", "Gifts", "Other"]
+
+ try:
+ return categories.index(category)
+ except ValueError:
+ return 0
+
+ def get_default_categories(self, vendor, item_type):
+ """Get default categories for multiselect, handling both single and multiple categories"""
+ existing_category = vendor.get('category', '')
+ existing_categories = vendor.get('categories', [])
+
+ # If categories field exists and is a list, use it
+ if existing_categories and isinstance(existing_categories, list):
+ return existing_categories
+
+ # If category field exists and is a string, convert to list
+ if existing_category and isinstance(existing_category, str):
+ return [existing_category]
+
+ # If category field exists and is already a list, use it
+ if existing_category and isinstance(existing_category, list):
+ return existing_category
+
+ return []
+
+ def get_category_display_text(self, vendor):
+ """Get formatted category display text for a vendor"""
+ categories_list = vendor.get('categories', [])
+ single_category = vendor.get('category', '')
+
+ # If categories field exists and is a list, use it
+ if categories_list and isinstance(categories_list, list):
+ return ', '.join(categories_list)
+
+ # Fall back to single category field
+ return single_category if single_category else 'No Category'
+
+ def get_payment_method_index(self, payment_method):
+ """Get the index of a payment method"""
+ methods = ["Not Specified", "Check", "Credit Card", "Bank Transfer", "Cash", "PayPal", "Venmo", "Zelle", "Reimbursement", "Credit", "Other"]
+ try:
+ return methods.index(payment_method)
+ except ValueError:
+ return 0
+
+ def parse_date(self, date_string):
+ """Parse date string to date object"""
+ if not date_string:
+ return None
+ try:
+ from datetime import datetime
+ return datetime.fromisoformat(date_string).date()
+ except:
+ return None
+
+ def get_payer(self, vendor):
+ """Get the payer from vendor data"""
+ event_payers = vendor.get('event_payers', {})
+ if event_payers:
+ return list(event_payers.values())[0]
+ return ""
+
+ def render_payment_history(self, vendor, context="main"):
+ """Render payment history for a vendor"""
+ payment_history = vendor.get('payment_history', [])
+
+ if not payment_history:
+ st.info("No payment history available.")
+ return
+
+ st.markdown("#### 💳 Payment History")
+
+ # Calculate summary statistics
+ total_payments = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') != 'credit')
+ total_credits = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') == 'credit')
+
+ # Show summary if there are credits
+ if total_credits > 0:
+ col1, col2 = st.columns(2)
+ with col1:
+ st.metric("Total Payments", f"${total_payments:,.0f}")
+ with col2:
+ st.metric("Total Credits", f"${total_credits:,.0f}")
+ st.markdown("---")
+
+ # Sort payment history by date (newest first)
+ sorted_history = sorted(payment_history, key=lambda x: x.get('date', ''), reverse=True)
+
+ # Display payment history in a table format
+ import pandas as pd
+
+ table_data = []
+ for payment in sorted_history:
+ amount = payment.get('amount', 0)
+ date = payment.get('date', '')
+ method = payment.get('method', 'Not Specified')
+ notes = payment.get('notes', '')
+ installment_info = payment.get('installment_info', '')
+ payment_type = payment.get('type', 'payment')
+
+ # Format date
+ try:
+ from datetime import datetime
+ date_obj = datetime.fromisoformat(date)
+ formatted_date = date_obj.strftime('%Y-%m-%d')
+ except:
+ formatted_date = date
+
+ # Create description with payment type
+ description_parts = []
+ if payment_type == 'deposit':
+ description_parts.append('💰 Deposit')
+ elif payment_type == 'credit':
+ description_parts.append('Credit/Reimbursement')
+ elif installment_info:
+ description_parts.append(installment_info)
+ else:
+ description_parts.append('💳 Payment')
+
+ if notes:
+ description_parts.append(notes)
+ description = ' - '.join(description_parts) if description_parts else 'Payment'
+
+ # Format amount based on payment type
+ if payment_type == 'credit':
+ formatted_amount = f"-${amount:,.0f}"
+ else:
+ formatted_amount = f"${amount:,.0f}"
+
+ table_data.append({
+ 'Date': formatted_date,
+ 'Amount': formatted_amount,
+ 'Method': method,
+ 'Description': description
+ })
+
+ if table_data:
+ df = pd.DataFrame(table_data)
+ st.dataframe(
+ df,
+ use_container_width=True,
+ hide_index=True,
+ column_config={
+ 'Date': st.column_config.TextColumn('Date', width='small'),
+ 'Amount': st.column_config.TextColumn('Amount', width='small'),
+ 'Method': st.column_config.TextColumn('Method', width='medium'),
+ 'Description': st.column_config.TextColumn('Description', width='large')
+ }
+ )
+
+ # Add edit/delete buttons for each payment
+ st.markdown("#### Edit Payment Records")
+ for i, payment in enumerate(sorted_history):
+ payment_id = payment.get('id', '')
+ amount = payment.get('amount', 0)
+ date = payment.get('date', '')
+ method = payment.get('method', 'Not Specified')
+ notes = payment.get('notes', '')
+ payment_type = payment.get('type', 'payment')
+
+ # Create a unique key that includes vendor ID, payment ID, context, and a hash of the payment data
+ import hashlib
+ # Create a more robust unique key using vendor ID, payment ID, context, loop index, and payment data
+ vendor_id = vendor.get('id', 'unknown')
+ # Include context and more data in the hash to ensure uniqueness
+ payment_data_string = f"{vendor_id}_{payment_id}_{context}_{amount}_{date}_{method}_{notes}_{payment_type}_{i}"
+ payment_data_hash = hashlib.md5(payment_data_string.encode()).hexdigest()[:12]
+ unique_key_suffix = f"{vendor_id}_{payment_id}_{context}_{i}_{payment_data_hash}"
+
+ # Format date
+ try:
+ from datetime import datetime
+ date_obj = datetime.fromisoformat(date)
+ formatted_date = date_obj.strftime('%Y-%m-%d')
+ except:
+ formatted_date = date
+
+ # Create description
+ if payment_type == 'deposit':
+ description = '💰 Deposit'
+ elif payment_type == 'credit':
+ description = 'Credit/Reimbursement'
+ else:
+ description = '💳 Payment'
+
+ if notes:
+ description += f' - {notes}'
+
+ # Format amount
+ if payment_type == 'credit':
+ formatted_amount = f"-${amount:,.0f}"
+ else:
+ formatted_amount = f"${amount:,.0f}"
+
+ # Display payment info with edit button
+ col1, col2, col3 = st.columns([3, 1, 1])
+ with col1:
+ st.markdown(f"**{formatted_date}** - {description} - {formatted_amount} ({method})")
+ with col2:
+ if st.button("Edit", key=f"edit_payment_{unique_key_suffix}"):
+ st.session_state[f"edit_payment_{payment_id}_{context}"] = True
+ st.rerun()
+ with col3:
+ if st.button("Delete", key=f"delete_payment_{unique_key_suffix}"):
+ st.session_state[f"delete_payment_{payment_id}_{context}"] = True
+ st.rerun()
+
+ # Handle edit action
+ if st.session_state.get(f"edit_payment_{payment_id}_{context}", False):
+ self.edit_payment_record(vendor, payment_id, context)
+
+ # Handle delete action
+ if st.session_state.get(f"delete_payment_{payment_id}_{context}", False):
+ st.warning(f"Are you sure you want to delete this payment record?")
+ col1, col2 = st.columns(2)
+ with col1:
+ if st.button("Yes, Delete", key=f"confirm_delete_payment_{unique_key_suffix}"):
+ # Delete the payment record
+ vendors = self.config_manager.load_json_data('vendors.json')
+ for v in vendors:
+ if v.get('id') == vendor.get('id'):
+ payment_history = v.get('payment_history', [])
+
+ # Find the payment being deleted to check if it's a credit/reimbursement
+ deleted_payment = None
+ for p in payment_history:
+ if p.get('id') == payment_id:
+ deleted_payment = p
+ break
+
+ # Remove the payment record
+ payment_history = [p for p in payment_history if p.get('id') != payment_id]
+ v['payment_history'] = payment_history
+
+ # Recalculate deposit_paid based on updated payment history
+ total_payments = sum(p.get('amount', 0) for p in payment_history if p.get('type') != 'credit')
+ total_credits = sum(p.get('amount', 0) for p in payment_history if p.get('type') == 'credit')
+ v['deposit_paid'] = total_payments - total_credits
+
+ # If deleted payment was a credit/reimbursement, restore total_cost
+ if deleted_payment and deleted_payment.get('type') == 'credit':
+ v['total_cost'] = v.get('total_cost', 0) + deleted_payment.get('amount', 0)
+
+ # If deleted payment was an installment payment, mark the corresponding installment as unpaid
+ if deleted_payment and deleted_payment.get('type') == 'installment':
+ installment_id = deleted_payment.get('installment_id')
+ if installment_id:
+ # Extract installment number from installment_id (format: "installment_1_vendor_id")
+ try:
+ installment_num = int(installment_id.split('_')[1]) - 1 # Convert to 0-based index
+ payment_installments = v.get('payment_installments', [])
+ if 0 <= installment_num < len(payment_installments):
+ payment_installments[installment_num]['paid'] = False
+ payment_installments[installment_num]['paid_date'] = None
+ v['payment_installments'] = payment_installments
+ except (ValueError, IndexError):
+ # If we can't parse the installment_id, skip this step
+ pass
+
+ break
+
+ if self.config_manager.save_json_data('vendors.json', vendors):
+ st.success("Payment record deleted successfully!")
+ # Clear the delete session state
+ if f"delete_payment_{payment_id}_{context}" in st.session_state:
+ del st.session_state[f"delete_payment_{payment_id}_{context}"]
+ st.rerun()
+ else:
+ st.error("Error deleting payment record")
+ with col2:
+ if st.button("Cancel", key=f"cancel_delete_payment_{unique_key_suffix}"):
+ # Clear the delete session state
+ if f"delete_payment_{payment_id}_{context}" in st.session_state:
+ del st.session_state[f"delete_payment_{payment_id}_{context}"]
+ st.rerun()
+
+ # Show summary
+ total_paid = sum(payment.get('amount', 0) for payment in payment_history)
+ st.caption(f"**Total Paid:** ${total_paid:,.0f} across {len(payment_history)} payment(s)")
+ else:
+ st.info("No payment history available.")
+
+ def manage_vendor_tasks(self, vendor):
+ item_type = vendor.get('type', 'Vendor/Service')
+ task_title = f"Tasks for {vendor.get('name', '')}" if item_type == 'Vendor/Service' else f"Tasks for {vendor.get('name', '')}"
+
+ # Add close button
+ col1, col2 = st.columns([4, 1])
+ with col1:
+ st.markdown(f"### {task_title}")
+ with col2:
+ if st.button("Close", key=f"close_tasks_{vendor.get('id', '')}"):
+ # Clear the tasks session state
+ vendor_id = vendor.get('id')
+ if f"show_tasks_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_tasks_{vendor_id}"]
+ st.rerun()
+
+ # Load all tasks
+ all_tasks = self.config_manager.load_json_data('tasks.json')
+ vendor_name = vendor.get('name', '')
+
+ # Filter tasks related to this vendor
+ vendor_id = vendor.get('id', '')
+ vendor_tasks = [task for task in all_tasks if
+ vendor_id == task.get('vendor_id') or
+ vendor_name.lower() in task.get('title', '').lower() or
+ vendor_name.lower() in task.get('description', '').lower()]
+
+ if vendor_tasks:
+ st.markdown(f"#### Current Tasks ({len(vendor_tasks)})")
+ for task in vendor_tasks:
+ self.render_vendor_task_card(task)
+ else:
+ st.info(f"No tasks currently related to {vendor_name}")
+
+ # Add new task for this vendor/item
+ item_type = vendor.get('type', 'Vendor/Service')
+ expander_title = "Add New Task for This Vendor" if item_type == 'Vendor/Service' else "Add New Task for This Item"
+ with st.expander(expander_title, expanded=False):
+ self.render_vendor_task_form(vendor)
+
+ def render_vendor_task_card(self, task):
+ task_id = task.get('id', '')
+ title = task.get('title', 'Untitled Task')
+ description = task.get('description', '')
+ due_date = task.get('due_date', '')
+ priority = task.get('priority', 'Medium')
+ assigned_to = task.get('assigned_to', '')
+ completed = task.get('completed', False)
+
+ # Create a container for the task card
+ with st.container():
+ # Task header with completion status and title
+ status_icon = "✅" if completed else "⏳"
+ st.markdown(f"**{status_icon} {title}**")
+
+ # Task details in columns
+ col1, col2, col3, col4 = st.columns(4)
+
+ with col1:
+ if due_date:
+ st.caption(f"📅 Due: {due_date}")
+ else:
+ st.caption("📅 No due date")
+
+ with col2:
+ # Priority with color coding
+ if priority == "Urgent":
+ st.caption(f"🔴 Priority: {priority}")
+ elif priority == "High":
+ st.caption(f"🔴 Priority: {priority}")
+ elif priority == "Medium":
+ st.caption(f"🟡 Priority: {priority}")
+ else:
+ st.caption(f"🟢 Priority: {priority}")
+
+ with col3:
+ # Assignment information
+ # Handle both old single assignee and new multiple assignees format
+ if isinstance(assigned_to, str):
+ assigned_to_display = assigned_to if assigned_to else "Unassigned"
+ elif isinstance(assigned_to, list):
+ if assigned_to:
+ assigned_to_display = ", ".join(assigned_to)
+ else:
+ assigned_to_display = "Unassigned"
+ else:
+ assigned_to_display = "Unassigned"
+
+ if assigned_to_display and assigned_to_display != "Unassigned":
+ st.caption(f"👤 Assigned: {assigned_to_display}")
+ else:
+ st.caption("👤 Not assigned")
+
+ with col4:
+ if completed:
+ st.caption("✅ Completed")
+ else:
+ st.caption("⏳ In Progress")
+
+ # Description if available
+ if description:
+ st.caption(f"📝 {description}")
+
+ # Add some spacing
+ st.markdown("---")
+
+ # Action buttons below the task card
+ col1, col2, col3 = st.columns([1, 1, 1])
+
+ with col1:
+ if st.button("Edit", key=f"edit_vendor_task_{task_id}", help="Edit task", use_container_width=True):
+ st.session_state[f"editing_vendor_task_{task_id}"] = True
+
+ with col2:
+ if not completed:
+ if st.button("Complete", key=f"complete_vendor_task_{task_id}", help="Mark complete", use_container_width=True):
+ self.toggle_task_completion(task_id, True)
+ else:
+ if st.button("Undo", key=f"undo_vendor_task_{task_id}", help="Mark incomplete", use_container_width=True):
+ self.toggle_task_completion(task_id, False)
+
+ with col3:
+ if st.button("Delete", key=f"delete_vendor_task_{task_id}", help="Delete task", use_container_width=True):
+ self.delete_vendor_task(task_id)
+
+ # Show edit form if editing (outside columns to span full width)
+ if st.session_state.get(f"editing_vendor_task_{task_id}", False):
+ self.render_edit_vendor_task_form(task)
+
+ def render_vendor_task_form(self, vendor):
+ vendor_name = vendor.get('name', '')
+ vendor_category = self.get_category_display_text(vendor)
+
+ # Load custom tags from configuration
+ config = self.config_manager.load_config()
+ custom_settings = config.get('custom_settings', {})
+ custom_tags = custom_settings.get('custom_tags', [])
+
+ with st.form(f"vendor_task_form_{vendor_name}"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ title = st.text_input("Task Title *", placeholder="Enter task title")
+ description = st.text_area("Description", placeholder="Enter task description")
+ due_date = st.date_input("Due Date", value=None)
+ priority = st.selectbox("Priority", ["Low", "Medium", "High", "Urgent"])
+
+ with col2:
+ # Assigned to field with wedding party and task assignees selection
+ wedding_party = self.config_manager.load_json_data('wedding_party.json')
+ wedding_party_names = [member.get('name', '') for member in wedding_party if member.get('name')]
+
+ # Get task assignees from config
+ config = self.config_manager.load_config()
+ custom_settings = config.get('custom_settings', {})
+ task_assignees = custom_settings.get('task_assignees', [])
+
+ # Create combined options for multiselect
+ all_assignee_options = []
+ if wedding_party_names:
+ all_assignee_options.extend([f"Wedding Party: {name}" for name in wedding_party_names])
+ if task_assignees:
+ all_assignee_options.extend([f"Task Assignee: {assignee}" for assignee in task_assignees])
+
+ # Multiple assignees selection
+ selected_assignees = st.multiselect("Assign to (select multiple people)", all_assignee_options, key=f"vendor_assignees_{vendor_name}")
+
+ # Custom assignee text input (for additional people not in the lists)
+ custom_assignee = st.text_input("Additional Custom Assignee", placeholder="Enter additional assignee name (optional)", key=f"vendor_custom_assignee_{vendor_name}")
+
+ # Combine selected assignees and custom assignee
+ assigned_to_list = []
+ for assignee in selected_assignees:
+ if assignee.startswith("Wedding Party: "):
+ assigned_to_list.append(assignee.replace("Wedding Party: ", ""))
+ elif assignee.startswith("Task Assignee: "):
+ assigned_to_list.append(assignee.replace("Task Assignee: ", ""))
+
+ if custom_assignee and custom_assignee.strip():
+ assigned_to_list.append(custom_assignee.strip())
+
+ # Store as list for multiple assignees
+ assigned_to = assigned_to_list
+
+ # Tags selection - use the same system as main tasks
+ # Pre-select relevant tags based on vendor category and type
+ default_tags = []
+ if vendor_category in custom_tags:
+ default_tags.append(vendor_category)
+
+ # Add vendor type as a tag
+ item_type = vendor.get('type', 'Vendor/Service')
+ vendor_type_tag = 'Vendor' if item_type == 'Vendor/Service' else 'Item'
+ if vendor_type_tag not in default_tags:
+ default_tags.append(vendor_type_tag)
+
+ selected_tags = st.multiselect("Tags", custom_tags, default=default_tags)
+
+ button_text = "Add Task" if item_type == 'Vendor/Service' else "Add Task"
+
+ col1, col2 = st.columns(2)
+ with col1:
+ submitted = st.form_submit_button(button_text, type="primary")
+ with col2:
+ cancel_clicked = st.form_submit_button("Cancel")
+
+ if submitted:
+ if title:
+ new_task = {
+ 'id': datetime.now().strftime("%Y%m%d_%H%M%S"),
+ 'title': title,
+ 'description': description,
+ 'due_date': due_date.isoformat() if due_date else None,
+ 'priority': priority,
+ 'group': 'Vendor & Item Management',
+ 'tags': selected_tags,
+ 'assigned_to': assigned_to,
+ 'vendor_id': vendor.get('id', ''),
+ 'completed': False,
+ 'created_date': datetime.now().isoformat(),
+ 'completed_date': None
+ }
+
+ # Load existing tasks and add new one
+ tasks = self.config_manager.load_json_data('tasks.json')
+ tasks.append(new_task)
+
+ if self.config_manager.save_json_data('tasks.json', tasks):
+ st.success("Task added successfully!")
+ st.rerun()
+ else:
+ st.error("Error saving task")
+ else:
+ st.error("Please enter a task title")
+ elif cancel_clicked:
+ st.rerun()
+
+ def record_payment(self, vendor):
+ from datetime import datetime, date
+
+ st.markdown(f"### Record Payment for {vendor.get('name', '')}")
+
+ # Show current payment info
+ current_deposit = vendor.get('deposit_paid', 0)
+ total_cost = vendor.get('total_cost', vendor.get('cost', 0))
+ # Calculate remaining balance using payment history for accuracy
+ payment_history = vendor.get('payment_history', [])
+ total_paid_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') != 'credit')
+ total_credits_from_history = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') == 'credit')
+ remaining_balance = total_cost - total_paid_from_history + total_credits_from_history
+
+ # Check if vendor has payment installments
+ payment_installments = vendor.get('payment_installments', [])
+ has_installments = len(payment_installments) > 1
+
+ if has_installments:
+ # Show installment summary
+ total_installment_amount = sum(inst.get('amount', 0) for inst in payment_installments)
+ paid_installments = sum(1 for inst in payment_installments if inst.get('paid', False))
+ # Use payment history for accurate total paid amount
+ payment_history = vendor.get('payment_history', [])
+ total_paid_amount = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') != 'credit')
+ total_credits = sum(payment.get('amount', 0) for payment in payment_history if payment.get('type') == 'credit')
+
+ st.info(f"**Payment Installments:** {paid_installments}/{len(payment_installments)} installments paid (${total_paid_amount:,.0f} of ${total_installment_amount:,.0f})")
+
+ # Show unpaid installments
+ unpaid_installments = [inst for inst in payment_installments if not inst.get('paid', False)]
+ if unpaid_installments:
+ st.markdown("#### Unpaid Installments:")
+ for i, installment in enumerate(unpaid_installments):
+ amount = installment.get('amount', 0)
+ due_date = installment.get('due_date', '')
+
+ # Check if past due
+ if due_date:
+ try:
+ from datetime import datetime, date
+ due_date_obj = datetime.fromisoformat(due_date).date()
+ today = date.today()
+ if due_date_obj < today:
+ st.warning(f"🔴 **Installment {i+1}:** ${amount:,.0f} - **PAST DUE** (was due {due_date})")
+ elif due_date_obj == today:
+ st.warning(f"🟡 **Installment {i+1}:** ${amount:,.0f} - **DUE TODAY**")
+ else:
+ st.info(f"⏳ **Installment {i+1}:** ${amount:,.0f} - Due {due_date}")
+ except:
+ st.info(f"⏳ **Installment {i+1}:** ${amount:,.0f} - Due {due_date}")
+ else:
+ st.info(f"⏳ **Installment {i+1}:** ${amount:,.0f} - No due date set")
+ else:
+ st.info("Current Status: \$" + str(int(total_paid_from_history)) + " paid of \$" + str(int(total_cost)) + " total (\$" + str(int(remaining_balance)) + " remaining)")
+
+ # Show payment history
+ payment_history = vendor.get('payment_history', [])
+ if payment_history:
+ with st.expander("💳 View Payment History", expanded=False):
+ self.render_payment_history(vendor, context="payment_form")
+
+ with st.form(f"payment_form_{vendor.get('id', '')}"):
+ if has_installments:
+ # For installments, show which installment this payment is for
+ unpaid_installments = [inst for inst in payment_installments if not inst.get('paid', False)]
+ if unpaid_installments:
+ installment_options = []
+ for i, installment in enumerate(unpaid_installments):
+ amount = installment.get('amount', 0)
+ due_date = installment.get('due_date', '')
+ # Find the actual installment number in the original sequence
+ actual_installment_num = None
+ for j, orig_installment in enumerate(payment_installments):
+ if (orig_installment.get('amount') == amount and
+ orig_installment.get('due_date') == installment.get('due_date')):
+ actual_installment_num = j + 1
+ break
+
+ installment_options.append(f"Installment {actual_installment_num}: ${amount:,.0f}" + (f" (Due: {due_date})" if due_date else ""))
+
+ selected_installment = st.selectbox("Select Installment to Pay", installment_options)
+ installment_index = installment_options.index(selected_installment)
+ installment_amount = unpaid_installments[installment_index].get('amount', 0)
+
+ # Pre-fill payment amount with installment amount
+ payment_amount = st.number_input("Payment Amount", min_value=0.0, value=installment_amount, step=100.0, help="Enter the amount paid to the vendor")
+ else:
+ st.success("All installments have been paid!")
+ payment_amount = st.number_input("Additional Payment Amount", min_value=0.0, value=0.0, step=100.0, help="Enter the amount paid to the vendor")
+ else:
+ payment_amount = st.number_input("Payment Amount", min_value=0.0, value=0.0, step=100.0, help="Enter the amount paid to the vendor or received as credit")
+
+ payment_date = st.date_input("Payment Date", value=datetime.now().date())
+ payment_type = st.selectbox("Payment Type", ["Payment", "Credit/Reimbursement"], help="Select 'Credit/Reimbursement' for money received back from the vendor")
+ payment_method = st.selectbox("Payment Method", ["Check", "Credit Card", "Bank Transfer", "Cash", "PayPal", "Venmo", "Zelle", "Reimbursement", "Credit", "Other"])
+ payment_notes = st.text_area("Payment Notes", placeholder="Additional notes about this payment (e.g., check number, transaction ID, reason for credit, etc.)")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ submitted = st.form_submit_button("Record Payment", type="primary")
+ with col2:
+ cancel_clicked = st.form_submit_button("Cancel")
+
+ if submitted:
+ if payment_amount > 0:
+ # Update vendor's payment info
+ vendors = self.config_manager.load_json_data('vendors.json')
+ for v in vendors:
+ if v.get('id') == vendor.get('id'):
+ # Determine if this is a credit/reimbursement
+ is_credit = payment_type == "Credit/Reimbursement"
+
+ # Update net amount paid (subtract for credits, add for payments)
+ # Note: "deposit_paid" represents net amount paid (payments minus credits)
+ # Only update deposit_paid for credits/reimbursements, not regular payments
+ if is_credit:
+ v['deposit_paid'] = max(0, v.get('deposit_paid', 0) - payment_amount)
+ # Adjust total_cost down when reimbursement is added (effective cost is reduced)
+ v['total_cost'] = max(0, v.get('total_cost', 0) - payment_amount)
+ # For regular payments, don't update deposit_paid - let payment history track the total
+
+ # Update payment method and notes
+ v['payment_method'] = payment_method
+ if payment_notes:
+ existing_notes = v.get('payment_notes', '')
+ if existing_notes:
+ v['payment_notes'] = f"{existing_notes}\n\n{payment_date}: {payment_notes}"
+ else:
+ v['payment_notes'] = f"{payment_date}: {payment_notes}"
+
+ # Update installments if applicable
+ installment_info = None
+ if has_installments and unpaid_installments:
+ installment_index = installment_options.index(selected_installment)
+ installment_to_update = unpaid_installments[installment_index]
+
+ # Find and update the installment in the vendor's installments
+ actual_installment_num = None
+ for inst in v.get('payment_installments', []):
+ if (inst.get('amount') == installment_to_update.get('amount') and
+ inst.get('due_date') == installment_to_update.get('due_date') and
+ not inst.get('paid', False)):
+ inst['paid'] = True
+ inst['paid_date'] = payment_date.isoformat()
+ # Find the actual installment number in the original sequence
+ for j, orig_inst in enumerate(v.get('payment_installments', [])):
+ if (orig_inst.get('amount') == installment_to_update.get('amount') and
+ orig_inst.get('due_date') == installment_to_update.get('due_date')):
+ actual_installment_num = j + 1
+ break
+ installment_info = f"Installment {actual_installment_num}"
+ break
+
+ # Add to payment history
+ payment_history = v.get('payment_history', [])
+ payment_record = {
+ 'id': datetime.now().strftime("%Y%m%d_%H%M%S_%f"),
+ 'amount': payment_amount,
+ 'date': payment_date.isoformat(),
+ 'method': payment_method,
+ 'notes': payment_notes,
+ 'installment_info': installment_info,
+ 'type': 'credit' if is_credit else 'payment'
+ }
+ payment_history.append(payment_record)
+ v['payment_history'] = payment_history
+
+ break
+
+ if self.config_manager.save_json_data('vendors.json', vendors):
+ if is_credit:
+ st.success(f"Credit/Reimbursement of ${payment_amount:,.0f} recorded successfully!")
+ else:
+ st.success(f"Payment of ${payment_amount:,.0f} recorded successfully!")
+ # Clear the payment session state
+ vendor_id = vendor.get('id')
+ if f"show_payment_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_payment_{vendor_id}"]
+ st.rerun()
+ else:
+ st.error("Error saving payment information")
+ else:
+ st.error("Please enter a payment amount greater than 0. For credits/reimbursements, enter the positive amount you received.")
+ elif cancel_clicked:
+ # Clear the payment session state
+ vendor_id = vendor.get('id')
+ if f"show_payment_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_payment_{vendor_id}"]
+ st.rerun()
+
+ def edit_payment_record(self, vendor, payment_id, context="main"):
+ """Edit an existing payment record"""
+ from datetime import datetime, date
+
+ payment_history = vendor.get('payment_history', [])
+ payment_to_edit = None
+
+ # Find the payment record to edit
+ for payment in payment_history:
+ if payment.get('id') == payment_id:
+ payment_to_edit = payment
+ break
+
+ if not payment_to_edit:
+ st.error("Payment record not found")
+ return
+
+ st.markdown(f"### Edit Payment Record")
+
+ # Parse existing payment data
+ existing_amount = payment_to_edit.get('amount', 0)
+ existing_date = payment_to_edit.get('date', '')
+ existing_method = payment_to_edit.get('method', 'Not Specified')
+ existing_notes = payment_to_edit.get('notes', '')
+ existing_type = payment_to_edit.get('type', 'payment')
+
+ # Parse date
+ try:
+ if existing_date:
+ existing_date_obj = datetime.fromisoformat(existing_date).date()
+ else:
+ existing_date_obj = date.today()
+ except:
+ existing_date_obj = date.today()
+
+ with st.form(f"edit_payment_form_{payment_id}"):
+ col1, col2 = st.columns(2)
+
+ with col1:
+ payment_amount = st.number_input("Payment Amount", min_value=0.0, value=float(existing_amount), step=100.0, help="Enter the amount paid to the vendor or received as credit")
+ payment_date = st.date_input("Payment Date", value=existing_date_obj)
+ payment_type = st.selectbox("Payment Type", ["Payment", "Credit/Reimbursement"],
+ index=1 if existing_type == 'credit' else 0,
+ help="Select 'Credit/Reimbursement' for money received back from the vendor")
+
+ with col2:
+ payment_method = st.selectbox("Payment Method", ["Check", "Credit Card", "Bank Transfer", "Cash", "PayPal", "Venmo", "Zelle", "Reimbursement", "Credit", "Other"],
+ index=self.get_payment_method_index(existing_method))
+ payment_notes = st.text_area("Payment Notes", value=existing_notes, placeholder="Additional notes about this payment (e.g., check number, transaction ID, reason for credit, etc.)")
+
+ col1, col2, col3 = st.columns(3)
+ with col1:
+ save_clicked = st.form_submit_button("Save Changes", type="primary")
+ with col2:
+ cancel_clicked = st.form_submit_button("Cancel")
+ with col3:
+ delete_clicked = st.form_submit_button("Delete Payment", type="secondary")
+
+ if save_clicked:
+ if payment_amount > 0:
+ # Update the payment record
+ vendors = self.config_manager.load_json_data('vendors.json')
+ for v in vendors:
+ if v.get('id') == vendor.get('id'):
+ payment_history = v.get('payment_history', [])
+
+ # Find the original payment to check for type changes
+ original_payment = None
+ for payment in payment_history:
+ if payment.get('id') == payment_id:
+ original_payment = payment.copy()
+ break
+
+ for payment in payment_history:
+ if payment.get('id') == payment_id:
+ # Check if payment type is changing
+ new_type = 'credit' if payment_type == "Credit/Reimbursement" else 'payment'
+ old_type = payment.get('type', 'payment')
+ old_amount = payment.get('amount', 0)
+
+ # Update payment record
+ payment['amount'] = payment_amount
+ payment['date'] = payment_date.isoformat()
+ payment['method'] = payment_method
+ payment['notes'] = payment_notes
+ payment['type'] = new_type
+
+ # Adjust total_cost if payment type changed
+ if old_type != new_type:
+ if new_type == 'credit' and old_type == 'payment':
+ # Changed from payment to credit - reduce total_cost
+ v['total_cost'] = max(0, v.get('total_cost', 0) - payment_amount)
+ elif new_type == 'payment' and old_type == 'credit':
+ # Changed from credit to payment - restore total_cost
+ v['total_cost'] = v.get('total_cost', 0) + old_amount
+
+ # Adjust total_cost if amount changed and it's a credit
+ elif new_type == 'credit' and old_amount != payment_amount:
+ # Amount changed for credit - adjust total_cost accordingly
+ v['total_cost'] = max(0, v.get('total_cost', 0) - (payment_amount - old_amount))
+
+ break
+
+ # Recalculate deposit_paid based on updated payment history
+ total_payments = sum(p.get('amount', 0) for p in payment_history if p.get('type') != 'credit')
+ total_credits = sum(p.get('amount', 0) for p in payment_history if p.get('type') == 'credit')
+ v['deposit_paid'] = total_payments - total_credits
+
+ break
+
+ if self.config_manager.save_json_data('vendors.json', vendors):
+ st.success("Payment record updated successfully!")
+ # Clear the edit session state
+ if f"edit_payment_{payment_id}_{context}" in st.session_state:
+ del st.session_state[f"edit_payment_{payment_id}_{context}"]
+ st.rerun()
+ else:
+ st.error("Error saving payment information")
+ else:
+ st.error("Please enter a payment amount greater than 0. For credits/reimbursements, enter the positive amount you received.")
+
+ elif delete_clicked:
+ # Delete the payment record
+ vendors = self.config_manager.load_json_data('vendors.json')
+ for v in vendors:
+ if v.get('id') == vendor.get('id'):
+ payment_history = v.get('payment_history', [])
+
+ # Find the payment being deleted to check if it's a credit/reimbursement
+ deleted_payment = None
+ for p in payment_history:
+ if p.get('id') == payment_id:
+ deleted_payment = p
+ break
+
+ # Remove the payment record
+ payment_history = [p for p in payment_history if p.get('id') != payment_id]
+ v['payment_history'] = payment_history
+
+ # Recalculate deposit_paid based on updated payment history
+ total_payments = sum(p.get('amount', 0) for p in payment_history if p.get('type') != 'credit')
+ total_credits = sum(p.get('amount', 0) for p in payment_history if p.get('type') == 'credit')
+ v['deposit_paid'] = total_payments - total_credits
+
+ # If deleted payment was a credit/reimbursement, restore total_cost
+ if deleted_payment and deleted_payment.get('type') == 'credit':
+ v['total_cost'] = v.get('total_cost', 0) + deleted_payment.get('amount', 0)
+
+ # If deleted payment was an installment payment, mark the corresponding installment as unpaid
+ if deleted_payment and deleted_payment.get('type') == 'installment':
+ installment_id = deleted_payment.get('installment_id')
+ if installment_id:
+ # Extract installment number from installment_id (format: "installment_1_vendor_id")
+ try:
+ installment_num = int(installment_id.split('_')[1]) - 1 # Convert to 0-based index
+ payment_installments = v.get('payment_installments', [])
+ if 0 <= installment_num < len(payment_installments):
+ payment_installments[installment_num]['paid'] = False
+ payment_installments[installment_num]['paid_date'] = None
+ v['payment_installments'] = payment_installments
+ except (ValueError, IndexError):
+ # If we can't parse the installment_id, skip this step
+ pass
+
+ break
+
+ if self.config_manager.save_json_data('vendors.json', vendors):
+ st.success("Payment record deleted successfully!")
+ # Clear the edit session state
+ if f"edit_payment_{payment_id}_{context}" in st.session_state:
+ del st.session_state[f"edit_payment_{payment_id}_{context}"]
+ st.rerun()
+ else:
+ st.error("Error deleting payment record")
+
+ elif cancel_clicked:
+ # Clear the edit session state
+ if f"edit_payment_{payment_id}_{context}" in st.session_state:
+ del st.session_state[f"edit_payment_{payment_id}_{context}"]
+ st.rerun()
+
+ def track_item_status(self, item):
+ st.markdown(f"### Track Item Status for {item.get('name', '')}")
+
+ current_status = item.get('status', 'Researching')
+
+ with st.form(f"status_form_{item.get('id', '')}"):
+ status_options = ["Researching", "Ordered", "Shipped", "Delivered", "Not Needed"]
+ current_index = status_options.index(current_status) if current_status in status_options else 0
+ new_status = st.selectbox("Update Status", status_options, index=current_index)
+ status_notes = st.text_area("Status Notes", placeholder="Additional notes about this status update")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ submitted = st.form_submit_button("Update Status", type="primary")
+ with col2:
+ cancel_clicked = st.form_submit_button("Cancel")
+
+ if submitted:
+ # Update item status
+ vendors = self.config_manager.load_json_data('vendors.json')
+ for v in vendors:
+ if v.get('id') == item.get('id'):
+ v['status'] = new_status
+ if status_notes:
+ v['status_notes'] = status_notes
+ break
+
+ if self.config_manager.save_json_data('vendors.json', vendors):
+ st.success(f"Status updated to {new_status}!")
+ # Clear the track session state
+ vendor_id = item.get('id')
+ if f"show_track_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_track_{vendor_id}"]
+ st.rerun()
+ else:
+ st.error("Error saving status update")
+ elif cancel_clicked:
+ # Clear the track session state
+ vendor_id = item.get('id')
+ if f"show_track_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_track_{vendor_id}"]
+ st.rerun()
+
+ def update_vendor_status(self, vendor):
+ """Update the status of a vendor or item"""
+ vendor_name = vendor.get('name', '')
+ item_type = vendor.get('type', 'Vendor/Service')
+ current_status = vendor.get('status', 'Researching')
+
+ st.markdown(f"### Update Status for {vendor_name}")
+
+ with st.form(f"status_update_form_{vendor.get('id', '')}"):
+ # Different status options for vendors vs items
+ if item_type == "Vendor/Service":
+ status_options = ["Researching", "Contacted", "Pending", "Booked", "Not Needed"]
+ else: # Item/Purchase
+ status_options = ["Researching", "Ordered", "Shipped", "Delivered", "Not Needed"]
+
+ # Find current status index
+ current_index = status_options.index(current_status) if current_status in status_options else 0
+ new_status = st.selectbox("Update Status", status_options, index=current_index)
+
+ # Status notes
+ status_notes = st.text_area("Status Notes",
+ value=vendor.get('status_notes', ''),
+ placeholder="Additional notes about this status update")
+
+ # Form buttons
+ col1, col2 = st.columns(2)
+ with col1:
+ submitted = st.form_submit_button("Update Status", type="primary")
+ with col2:
+ cancel_clicked = st.form_submit_button("Cancel")
+
+ if submitted:
+ # Update vendor/item status
+ vendors = self.config_manager.load_json_data('vendors.json')
+ for v in vendors:
+ if v.get('id') == vendor.get('id'):
+ v['status'] = new_status
+ if status_notes:
+ v['status_notes'] = status_notes
+ else:
+ # Remove status_notes if empty
+ v.pop('status_notes', None)
+ break
+
+ if self.config_manager.save_json_data('vendors.json', vendors):
+ st.success(f"Status updated to {new_status}!")
+ # Clear the status session state
+ vendor_id = vendor.get('id')
+ if f"show_status_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_status_{vendor_id}"]
+ st.rerun()
+ else:
+ st.error("Error saving status update")
+ elif cancel_clicked:
+ # Clear the status session state
+ vendor_id = vendor.get('id')
+ if f"show_status_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_status_{vendor_id}"]
+ st.rerun()
+
+ def toggle_task_completion(self, task_id, completed):
+ tasks = self.config_manager.load_json_data('tasks.json')
+ for task in tasks:
+ if task.get('id') == task_id:
+ task['completed'] = completed
+ task['completed_date'] = datetime.now().isoformat() if completed else None
+ break
+
+ self.config_manager.save_json_data('tasks.json', tasks)
+ st.rerun()
+
+ def show_delete_confirmation(self, vendor_id):
+ # Find the vendor to get its name
+ vendors = self.config_manager.load_json_data('vendors.json')
+ vendor_to_delete = None
+ for vendor in vendors:
+ if vendor.get('id') == vendor_id:
+ vendor_to_delete = vendor
+ break
+
+ if vendor_to_delete:
+ vendor_name = vendor_to_delete.get('name', 'Unknown')
+ item_type = vendor_to_delete.get('type', 'Vendor/Service')
+
+ # Show confirmation dialog
+ st.warning(f"Are you sure you want to delete {vendor_name}?")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ if st.button("Yes, Delete", key=f"confirm_delete_{vendor_id}", type="primary"):
+ # Reload vendors to ensure we have the latest data
+ vendors = self.config_manager.load_json_data('vendors.json')
+
+ # Find and remove the vendor from the list
+ original_count = len(vendors)
+ updated_vendors = [vendor for vendor in vendors if vendor.get('id') != vendor_id]
+
+ # Check if the vendor was actually removed
+ if len(updated_vendors) < original_count:
+ # Save the updated list
+ if self.config_manager.save_json_data('vendors.json', updated_vendors):
+ st.success(f"{item_type} '{vendor_name}' deleted successfully!")
+ # Clear the delete confirmation state
+ if f"show_delete_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_delete_{vendor_id}"]
+ # Force a page refresh to show updated data
+ st.rerun()
+ else:
+ st.error("Error saving changes to vendors file")
+ else:
+ st.error(f"Could not find vendor with ID {vendor_id} to delete")
+
+ with col2:
+ if st.button("Cancel", key=f"cancel_delete_{vendor_id}"):
+ # Clear the delete confirmation state
+ if f"show_delete_{vendor_id}" in st.session_state:
+ del st.session_state[f"show_delete_{vendor_id}"]
+ st.rerun()
+
+ def delete_vendor_task(self, task_id):
+ """Delete a vendor task from the tasks.json file"""
+ try:
+ # Load existing tasks
+ tasks = self.config_manager.load_json_data('tasks.json')
+
+ # Find and remove the task
+ original_count = len(tasks)
+ tasks = [task for task in tasks if task.get('id') != task_id]
+
+ if len(tasks) < original_count:
+ # Save the updated tasks
+ if self.config_manager.save_json_data('tasks.json', tasks):
+ st.success("Task deleted successfully!")
+ st.rerun()
+ else:
+ st.error("Error saving changes to tasks file")
+ else:
+ st.error("Task not found")
+
+ except Exception as e:
+ st.error(f"Error deleting task: {str(e)}")
+
+ def render_edit_vendor_task_form(self, task):
+ """Render the edit form for a vendor task"""
+ task_id = task.get('id', '')
+ vendor_id = task.get('vendor_id', '')
+
+ # Get vendor information from vendor_id
+ vendors = self.config_manager.load_json_data('vendors.json')
+ vendor = next((v for v in vendors if v.get('id') == vendor_id), {})
+ vendor_name = vendor.get('name', '')
+ vendor_category = self.get_category_display_text(vendor)
+
+ # Load custom tags from configuration
+ config = self.config_manager.load_config()
+ custom_settings = config.get('custom_settings', {})
+ custom_tags = custom_settings.get('custom_tags', [])
+
+ with st.form(f"edit_vendor_task_form_{task_id}"):
+ st.markdown("### Edit Task")
+
+ col1, col2 = st.columns(2)
+
+ with col1:
+ # Pre-fill form with existing task data
+ title = st.text_input("Task Title *", value=task.get('title', ''), placeholder="Enter task title", key=f"edit_vendor_title_{task_id}")
+ description = st.text_area("Description", value=task.get('description', ''), placeholder="Enter task description", key=f"edit_vendor_description_{task_id}")
+
+ # Handle due date
+ due_date_str = task.get('due_date', '')
+ due_date = None
+ if due_date_str:
+ try:
+ from datetime import datetime
+ due_date = datetime.strptime(due_date_str, '%Y-%m-%d').date()
+ except:
+ due_date = None
+
+ due_date = st.date_input("Due Date", value=due_date, key=f"edit_vendor_due_date_{task_id}")
+ priority = st.selectbox("Priority", ["Low", "Medium", "High", "Urgent"],
+ index=["Low", "Medium", "High", "Urgent"].index(task.get('priority', 'Medium')), key=f"edit_vendor_priority_{task_id}")
+
+ with col2:
+ # Assigned to field with multiple assignee support
+ wedding_party = self.config_manager.load_json_data('wedding_party.json')
+ wedding_party_names = [member.get('name', '') for member in wedding_party if member.get('name')]
+
+ # Get custom task assignees from config
+ custom_settings = config.get('custom_settings', {})
+ task_assignees = custom_settings.get('task_assignees', [])
+
+ # Get current assigned_to value (handle both old single assignee and new multiple assignees)
+ current_assigned_to = task.get('assigned_to', '')
+
+ # Handle backward compatibility - convert single assignee to list
+ if isinstance(current_assigned_to, str):
+ if current_assigned_to:
+ current_assignees = [current_assigned_to]
+ else:
+ current_assignees = []
+ elif isinstance(current_assigned_to, list):
+ current_assignees = current_assigned_to
+ else:
+ current_assignees = []
+
+ # Create options for multiselect
+ assignee_options = []
+ for name in wedding_party_names:
+ assignee_options.append(f"Wedding Party: {name}")
+ for name in task_assignees:
+ assignee_options.append(f"Task Assignee: {name}")
+
+ # Pre-select current assignees
+ selected_assignees = []
+ for assignee in current_assignees:
+ if assignee in wedding_party_names:
+ selected_assignees.append(f"Wedding Party: {assignee}")
+ elif assignee in task_assignees:
+ selected_assignees.append(f"Task Assignee: {assignee}")
+
+ # Multiselect for assignees
+ selected_assignees = st.multiselect("Assign to (select multiple)", assignee_options, default=selected_assignees, key=f"edit_vendor_assignees_{task_id}")
+
+ # Custom assignee text input for additional assignees
+ custom_assignee_text = ""
+ for assignee in current_assignees:
+ if assignee not in wedding_party_names and assignee not in task_assignees:
+ if custom_assignee_text:
+ custom_assignee_text += f", {assignee}"
+ else:
+ custom_assignee_text = assignee
+
+ custom_assignee = st.text_input("Additional Custom Assignees", value=custom_assignee_text, placeholder="Enter additional assignee names (comma-separated)", key=f"edit_vendor_custom_assignee_{task_id}")
+
+ # Combine selected assignees and custom assignees
+ assigned_to_list = []
+ for assignee in selected_assignees:
+ if assignee.startswith("Wedding Party: "):
+ assigned_to_list.append(assignee.replace("Wedding Party: ", ""))
+ elif assignee.startswith("Task Assignee: "):
+ assigned_to_list.append(assignee.replace("Task Assignee: ", ""))
+
+ # Parse custom assignees (comma-separated)
+ if custom_assignee and custom_assignee.strip():
+ custom_list = [name.strip() for name in custom_assignee.split(',') if name.strip()]
+ assigned_to_list.extend(custom_list)
+
+ # Store as list for multiple assignees
+ assigned_to = assigned_to_list
+
+ # Tags selection - pre-select existing tags
+ existing_tags = task.get('tags', [])
+ selected_tags = st.multiselect("Tags", custom_tags, default=existing_tags, key=f"edit_vendor_tags_{task_id}")
+
+ col1, col2 = st.columns(2)
+ with col1:
+ submitted = st.form_submit_button("Update Task", type="primary")
+ with col2:
+ cancel_clicked = st.form_submit_button("Cancel")
+
+ if submitted:
+ if title:
+ # Update the task
+ updated_task = {
+ 'id': task_id,
+ 'title': title,
+ 'description': description,
+ 'due_date': due_date.isoformat() if due_date else None,
+ 'priority': priority,
+ 'group': 'Vendor & Item Management',
+ 'tags': selected_tags,
+ 'assigned_to': assigned_to,
+ 'vendor_id': vendor_id,
+ 'completed': task.get('completed', False),
+ 'created_date': task.get('created_date', ''),
+ 'completed_date': task.get('completed_date', None)
+ }
+
+ # Load existing tasks and update
+ tasks = self.config_manager.load_json_data('tasks.json')
+ for i, t in enumerate(tasks):
+ if t.get('id') == task_id:
+ tasks[i] = updated_task
+ break
+
+ if self.config_manager.save_json_data('tasks.json', tasks):
+ st.success("Task updated successfully!")
+ # Clear the editing state
+ if f"editing_vendor_task_{task_id}" in st.session_state:
+ del st.session_state[f"editing_vendor_task_{task_id}"]
+ st.rerun()
+ else:
+ st.error("Error saving task")
+ else:
+ st.error("Please enter a task title")
+ elif cancel_clicked:
+ # Clear the editing state
+ if f"editing_vendor_task_{task_id}" in st.session_state:
+ del st.session_state[f"editing_vendor_task_{task_id}"]
+ st.rerun()