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