import streamlit as st import json import html from datetime import datetime, date from config_manager import ConfigManager class TasksManager: def __init__(self): self.config_manager = ConfigManager() self.vendors_cache = None def get_vendor_name(self, vendor_id): """Get vendor name by vendor ID""" if not self.vendors_cache or not vendor_id: return None for vendor in self.vendors_cache: if vendor.get('id') == vendor_id: return vendor.get('name') return None def get_vendor_contact_info(self, vendor_id): """Get vendor contact information by vendor ID""" if not self.vendors_cache or not vendor_id: return None for vendor in self.vendors_cache: if vendor.get('id') == vendor_id: vendor_type = vendor.get('type', 'Vendor/Service') if vendor_type == 'Vendor/Service': # For vendors, return their contact information return { 'contact_person': vendor.get('contact_person', ''), 'phone': vendor.get('phone', ''), 'email': vendor.get('email', ''), 'website': vendor.get('website', ''), 'address': vendor.get('address', '') } else: # For items, return seller contact information return { 'contact_person': '', # Items don't have contact person 'phone': vendor.get('seller_phone', ''), 'email': vendor.get('seller_email', ''), 'website': vendor.get('seller_website', ''), 'address': '' # Items don't have address } return None def render(self, config): st.markdown("## ✅ Task Management") # Load tasks and vendors tasks = self.config_manager.load_json_data('tasks.json') self.vendors_cache = self.config_manager.load_json_data('vendors.json') custom_settings = config.get('custom_settings', {}) custom_tags = custom_settings.get('custom_tags', []) # Get event-based task groups wedding_events = config.get('wedding_events', []) event_names = [event['name'] for event in wedding_events] if wedding_events else [] # Add general planning categories task_groups = event_names + ['General Planning', 'Vendor Management', 'Vendor & Item Management', 'Wedding Party', 'Guest Management', 'Timeline'] # Task creation section with st.expander("➕ Add New Task", expanded=False): self.render_task_form(task_groups, custom_tags) # View toggle and filters col1, col2, col3, col4, col5 = st.columns(5) with col1: view_mode = st.radio("View Mode", ["Detailed View", "Checklist View"], horizontal=True) with col2: filter_group = st.selectbox("Filter by Group", ["All"] + task_groups) with col3: filter_status = st.selectbox("Filter by Status", ["All", "Completed", "Incomplete"]) with col4: # Get unique assignees from tasks (handle both single and multiple assignees) assignees = set() for task in tasks: assigned_to = task.get('assigned_to', '') if isinstance(assigned_to, str) and assigned_to.strip(): assignees.add(assigned_to.strip()) elif isinstance(assigned_to, list): for assignee in assigned_to: if assignee and assignee.strip(): assignees.add(assignee.strip()) assignee_list = sorted(list(assignees)) filter_assignees = st.multiselect("Filter by Assignees", assignee_list, help="Select one or more assignees to filter tasks") with col5: sort_by = st.selectbox("Sort by", ["Due Date", "Created Date", "Title", "Group"]) # Filter and sort tasks filtered_tasks = self.filter_tasks(tasks, filter_group, filter_status, filter_assignees) sorted_tasks = self.sort_tasks(filtered_tasks, sort_by) # Display tasks based on view mode if sorted_tasks: st.markdown(f"### Tasks ({len(sorted_tasks)} total)") if view_mode == "Checklist View": self.render_checklist_view(sorted_tasks) else: # Group tasks by their group/category for detailed view grouped_tasks = {} for task in sorted_tasks: group = task.get('group', 'Uncategorized') if group not in grouped_tasks: grouped_tasks[group] = [] grouped_tasks[group].append(task) # Display tasks grouped by category for group_name, tasks in grouped_tasks.items(): st.markdown(f"## {group_name} ({len(tasks)} tasks)") for task in tasks: self.render_task_card(task, task_groups, custom_tags) else: st.info("No tasks found. Create your first task above!") def render_checklist_view(self, tasks): """Render tasks in a compact checklist format for easy reading""" # Display all tasks in a single checklist without grouping st.markdown("#### All Tasks") # Create a container for the checklist with st.container(): for task in tasks: self.render_checklist_item(task) def render_checklist_item(self, task): """Render a single task as a checklist item with interactive checkbox""" task_id = task.get('id', '') title = task.get('title', 'Untitled Task') description = task.get('description', '') due_date = task.get('due_date', '') assigned_to = task.get('assigned_to', '') tags = task.get('tags', []) completed = task.get('completed', False) vendor_id = task.get('vendor_id', '') vendor_name = self.get_vendor_name(vendor_id) if vendor_id else None # 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" # Create a compact checklist item with st.container(): # Use a horizontal layout with better spacing col1, col2, col3, col4 = st.columns([0.3, 3.2, 1, 1]) with col1: # Interactive checkbox for completion status with label new_completed = st.checkbox( " ", # Single space as label to provide spacing value=completed, key=f"checklist_{task_id}", help="Click to toggle completion status" ) # If completion status changed, update the task if new_completed != completed: self.toggle_task_completion(task_id, new_completed) with col2: # Task title and description with proper spacing if completed: st.markdown(f"~~**{title}**~~") else: st.markdown(f"**{title}**") if description: st.caption(f"📝 {description}") # Display tags if they exist if tags and len(tags) > 0: st.caption(f"🏷️ {', '.join(tags)}") with col3: # Due date only if due_date: st.caption(f"📅 {due_date}") with col4: # Assigned to and vendor if assigned_to_display and assigned_to_display != "Unassigned": st.caption(f"👤 {assigned_to_display}") if vendor_name: st.caption(f"🏢 {vendor_name}") # Add a subtle separator st.markdown("---") def render_task_form(self, task_groups, custom_tags): with st.form("task_form"): col1, col2 = st.columns(2) with col1: title = st.text_input("Task Title *", placeholder="Enter task title") # Show event-based groups first, then general categories if task_groups: group = st.selectbox("Event/Category", task_groups, help="Select the wedding event or general category this task relates to") else: group = st.selectbox("Category", ["General Planning"]) due_date = st.date_input("Due Date", value=None) priority = st.selectbox("Priority", ["Low", "Medium", "High", "Urgent"]) with col2: description = st.text_area("Description", placeholder="Enter task description") # 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="create_assignees") # 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="create_custom_assignee") # 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 selected_tags = st.multiselect("Tags", custom_tags, default=[]) submitted = st.form_submit_button("Create Task", type="primary") if submitted: if title: new_task = { 'id': datetime.now().strftime("%Y%m%d_%H%M%S"), 'title': title, 'description': description, 'group': group, 'due_date': due_date.isoformat() if due_date else None, 'priority': priority, 'assigned_to': assigned_to, 'tags': selected_tags, '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 created successfully!") st.rerun() else: st.error("Error saving task") else: st.error("Please enter a task title") def filter_tasks(self, tasks, filter_group, filter_status, filter_assignees): filtered = tasks.copy() # Filter by group if filter_group != "All": filtered = [task for task in filtered if task.get('group') == filter_group] # Filter by status if filter_status == "Completed": filtered = [task for task in filtered if task.get('completed', False)] elif filter_status == "Incomplete": filtered = [task for task in filtered if not task.get('completed', False)] # Filter by assignees (handle both single and multiple assignees) if filter_assignees: # If any assignees are selected filtered_tasks = [] for task in filtered: assigned_to = task.get('assigned_to', '') task_assignees = [] # Extract assignees from task (handle both old single and new multiple assignees format) if isinstance(assigned_to, str) and assigned_to.strip(): task_assignees = [assigned_to.strip()] elif isinstance(assigned_to, list): task_assignees = [assignee.strip() for assignee in assigned_to if assignee and assignee.strip()] # Check if any of the task's assignees match any of the selected filter assignees if any(assignee in filter_assignees for assignee in task_assignees): filtered_tasks.append(task) filtered = filtered_tasks return filtered def sort_tasks(self, tasks, sort_by): if sort_by == "Due Date": return sorted(tasks, key=lambda x: x.get('due_date') or '9999-12-31') elif sort_by == "Created Date": return sorted(tasks, key=lambda x: x.get('created_date', ''), reverse=True) elif sort_by == "Title": return sorted(tasks, key=lambda x: x.get('title', '').lower()) elif sort_by == "Group": return sorted(tasks, key=lambda x: x.get('group', '')) else: return tasks def render_task_card(self, task, task_groups, custom_tags): task_id = task.get('id', '') title = task.get('title', 'Untitled Task') description = task.get('description', '') group = task.get('group', 'Uncategorized') due_date = task.get('due_date', '') priority = task.get('priority', 'Medium') assigned_to = task.get('assigned_to', '') tags = task.get('tags', []) completed = task.get('completed', False) vendor_id = task.get('vendor_id', '') vendor_name = self.get_vendor_name(vendor_id) if vendor_id else None vendor_contact_info = self.get_vendor_contact_info(vendor_id) if vendor_id else None # 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" # Create a container for the task card with st.container(): # Task header with completion status and title - make this the most prominent status_icon = "✅" if completed else "⏳" st.markdown(f"### {status_icon} {title}") # Task details in columns col1, col2, col3 = st.columns(3) with col1: if due_date: st.caption(f"📅 Due: {due_date}") else: st.caption("📅 No due date") if vendor_name: st.caption(f"🏢 Vendor: {vendor_name}") 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}") if assigned_to_display and assigned_to_display != "Unassigned": st.caption(f"👤 Assigned: {assigned_to_display}") with col3: st.caption(f"📁 Group: {group}") if tags and len(tags) > 0: st.caption(f"🏷️ Tags: {', '.join(tags)}") # Description if available if description: st.caption(f"📝 {description}") # Vendor contact information if available if vendor_contact_info and vendor_name: contact_info_items = [] # Determine if this is a vendor or item based on contact person is_vendor = vendor_contact_info.get('contact_person', '') != '' contact_type = "Vendor Contact" if is_vendor else "Seller Contact" if is_vendor and vendor_contact_info.get('contact_person'): contact_info_items.append(f"**Contact Person:** {vendor_contact_info['contact_person']}") if vendor_contact_info.get('phone'): contact_info_items.append(f"**Phone:** {vendor_contact_info['phone']}") if vendor_contact_info.get('email'): contact_info_items.append(f"**Email:** {vendor_contact_info['email']}") if vendor_contact_info.get('website'): contact_info_items.append(f"**Website:** [{vendor_contact_info['website']}]({vendor_contact_info['website']})") if is_vendor and vendor_contact_info.get('address'): contact_info_items.append(f"**Address:** {vendor_contact_info['address']}") if contact_info_items: st.markdown(f"**{contact_type} Information:**") for info in contact_info_items: st.markdown(f"{info}", unsafe_allow_html=True) # Add some spacing st.markdown("---") # Action buttons below the task card col1, col2, col3, col4 = st.columns([1, 1, 1, 1]) with col1: if st.button("Edit", key=f"edit_{task_id}", help="Edit task", use_container_width=True): st.session_state[f"editing_task_{task_id}"] = True with col2: if st.button("Duplicate", key=f"duplicate_{task_id}", help="Duplicate task", use_container_width=True): self.duplicate_task(task_id) with col3: if completed: if st.button("Undo", key=f"undo_{task_id}", help="Mark incomplete", use_container_width=True): self.toggle_task_completion(task_id, False) else: if st.button("Complete", key=f"complete_{task_id}", help="Mark complete", use_container_width=True): self.toggle_task_completion(task_id, True) with col4: if st.button("Delete", key=f"delete_{task_id}", help="Delete task", use_container_width=True): self.delete_task(task_id) # Show edit form if editing (outside columns to span full width) if st.session_state.get(f"editing_task_{task_id}", False): self.render_edit_task_form(task, task_groups, custom_tags) 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 delete_task(self, task_id): tasks = self.config_manager.load_json_data('tasks.json') tasks = [task for task in tasks if task.get('id') != task_id] self.config_manager.save_json_data('tasks.json', tasks) st.rerun() def duplicate_task(self, task_id): tasks = self.config_manager.load_json_data('tasks.json') # Find the task to duplicate original_task = None for task in tasks: if task.get('id') == task_id: original_task = task break if original_task: # Create a duplicate with new ID and modified title duplicated_task = original_task.copy() duplicated_task['id'] = datetime.now().strftime("%Y%m%d_%H%M%S_%f") duplicated_task['title'] = f"{original_task.get('title', 'Untitled Task')}" duplicated_task['completed'] = False duplicated_task['completed_date'] = None duplicated_task['created_date'] = datetime.now().isoformat() # Add the duplicated task to the list tasks.append(duplicated_task) if self.config_manager.save_json_data('tasks.json', tasks): st.success("Task duplicated successfully!") st.rerun() else: st.error("Error saving duplicated task") else: st.error("Task not found") def render_edit_task_form(self, task, task_groups, custom_tags): task_id = task.get('id', '') st.markdown("### Edit Task") with st.form(f"edit_task_form_{task_id}"): col1, col2 = st.columns(2) with col1: title = st.text_input("Task Title *", value=task.get('title', ''), key=f"edit_title_{task_id}") # Show event-based groups first, then general categories if task_groups: current_group = task.get('group', '') group_index = 0 if current_group in task_groups: group_index = task_groups.index(current_group) group = st.selectbox("Event/Category", task_groups, index=group_index, key=f"edit_group_{task_id}") else: group = st.selectbox("Category", ["General Planning"], key=f"edit_group_{task_id}") due_date_str = task.get('due_date', '') due_date = None if due_date_str: try: due_date = datetime.fromisoformat(due_date_str).date() except: due_date = None due_date = st.date_input("Due Date", value=due_date, key=f"edit_due_date_{task_id}") priority_options = ["Low", "Medium", "High", "Urgent"] current_priority = task.get('priority', 'Medium') priority_index = priority_options.index(current_priority) if current_priority in priority_options else 1 priority = st.selectbox("Priority", priority_options, index=priority_index, key=f"edit_priority_{task_id}") with col2: description = st.text_area("Description", value=task.get('description', ''), key=f"edit_description_{task_id}") # 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', []) # 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 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]) # Determine initial selected values initial_selected = [] custom_assignees = [] for assignee in current_assignees: if assignee in wedding_party_names: initial_selected.append(f"Wedding Party: {assignee}") elif assignee in task_assignees: initial_selected.append(f"Task Assignee: {assignee}") else: custom_assignees.append(assignee) # Multiple assignees selection selected_assignees = st.multiselect("Assign to (select multiple people)", all_assignee_options, default=initial_selected, key=f"edit_assignees_{task_id}") # Custom assignee text input (for additional people not in the lists) custom_assignee_text = ", ".join(custom_assignees) if custom_assignees else "" custom_assignee = st.text_input("Additional Custom Assignees", value=custom_assignee_text, placeholder="Enter additional assignee names (comma-separated)", key=f"edit_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 current_tags = task.get('tags', []) # Filter current tags to only include those that exist in custom_tags valid_current_tags = [tag for tag in current_tags if tag in custom_tags] selected_tags = st.multiselect("Tags", custom_tags, default=valid_current_tags, key=f"edit_tags_{task_id}") # Form buttons col1, col2 = st.columns(2) with col1: save_clicked = st.form_submit_button("Save Changes", type="primary") with col2: cancel_clicked = st.form_submit_button("Cancel") if save_clicked: if title: # Update the task updated_task = { 'id': task_id, 'title': title, 'description': description, 'group': group, 'due_date': due_date.isoformat() if due_date else None, 'priority': priority, 'assigned_to': assigned_to, 'tags': selected_tags, 'completed': task.get('completed', False), 'created_date': task.get('created_date', datetime.now().isoformat()), 'completed_date': task.get('completed_date', None), 'vendor_id': task.get('vendor_id', '') # Preserve vendor_id if it exists } # Load existing tasks and update the specific one 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!") st.session_state[f"editing_task_{task_id}"] = False st.rerun() else: st.error("Error saving task") else: st.error("Please enter a task title") if cancel_clicked: st.session_state[f"editing_task_{task_id}"] = False st.rerun()