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"" 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"" html += "" html += "
{col}
{value}
" 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()