| import streamlit as st |
| import pandas as pd |
| from datetime import datetime, date |
| import os |
| import sys |
| import random |
| import json |
| import tempfile |
|
|
| |
| os.environ['STREAMLIT_ANALYTICS_ENABLED'] = 'false' |
| os.environ['STREAMLIT_METRICS_ENABLED'] = 'false' |
| os.environ['STREAMLIT_BROWSER_GATHER_USAGE_STATS'] = '0' |
| os.environ['STREAMLIT_SERVER_HEADLESS'] = 'true' |
|
|
| |
| st.set_page_config( |
| page_title="Task Board", |
| page_icon="๐", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
|
|
| |
| st.markdown(""" |
| <style> |
| /* Main theme colors */ |
| :root { |
| --primary-color: #0A2647; |
| --secondary-color: #144272; |
| --highlight-color: #2C74B3; |
| --header-bg: #0f2942; |
| --card-bg: #ffffff; |
| --border-color: #e0e0e0; |
| --text-color: #333; |
| --todo-color: #03A9F4; |
| --progress-color: #FF5722; |
| --done-color: #4CAF50; |
| --backlog-color: #8BC34A; |
| } |
| |
| /* Fix for scrolling and layout issues */ |
| body { |
| overflow-x: hidden; |
| } |
| |
| .stApp { |
| max-width: 100vw !important; |
| overflow-x: hidden !important; |
| } |
| |
| /* Task Grid Layout */ |
| .task-container { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 16px; |
| align-items: flex-start; |
| margin-bottom: 20px; |
| } |
| |
| .task-card { |
| width: 200px; |
| min-height: 140px; |
| border: 1px solid var(--border-color); |
| border-radius: 4px; |
| overflow: hidden; |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); |
| display: flex; |
| flex-direction: column; |
| background-color: var(--card-bg); |
| transition: transform 0.2s, box-shadow 0.2s; |
| cursor: pointer; |
| margin-bottom: 10px; |
| } |
| |
| .task-card:hover { |
| transform: translateY(-3px); |
| box-shadow: 0 4px 8px rgba(0,0,0,0.2); |
| } |
| |
| .card-header { |
| padding: 8px; |
| color: white; |
| font-weight: bold; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| .card-header.todo { |
| background-color: var(--todo-color); |
| } |
| |
| .card-header.in-progress { |
| background-color: var(--progress-color); |
| } |
| |
| .card-header.done { |
| background-color: var(--done-color); |
| } |
| |
| .card-header.backlog { |
| background-color: var(--backlog-color); |
| } |
| |
| .task-id { |
| font-size: 14px; |
| font-weight: bold; |
| } |
| |
| .task-title { |
| padding: 8px; |
| font-weight: bold; |
| color: var(--text-color); |
| font-size: 13px; |
| height: 35px; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| |
| .task-assignee { |
| padding: 0 8px; |
| color: #666; |
| font-size: 12px; |
| font-style: italic; |
| margin-top: auto; |
| margin-bottom: 5px; |
| } |
| |
| .task-dates { |
| padding: 0 8px 8px; |
| color: #666; |
| font-size: 10px; |
| display: flex; |
| justify-content: space-between; |
| } |
| |
| /* Task Details Container */ |
| .task-details-container { |
| border: 1px solid var(--border-color); |
| border-radius: 8px; |
| padding: 20px; |
| margin-top: 16px; |
| background-color: white; |
| box-shadow: 0 2px 10px rgba(0,0,0,0.08); |
| } |
| |
| .task-details-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 16px; |
| padding-bottom: 12px; |
| border-bottom: 1px solid #eee; |
| } |
| |
| .task-details-title { |
| font-size: 24px; |
| font-weight: 600; |
| color: var(--primary-color); |
| margin: 0; |
| } |
| |
| .task-details-section { |
| margin-bottom: 16px; |
| } |
| |
| .task-details-section h3 { |
| font-size: 18px; |
| margin-bottom: 8px; |
| color: var(--secondary-color); |
| } |
| |
| .task-meta-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
| gap: 16px; |
| margin-bottom: 20px; |
| } |
| |
| .task-meta-item { |
| padding: 8px 12px; |
| border-radius: 4px; |
| background-color: #f5f5f5; |
| } |
| |
| .task-meta-label { |
| font-size: 12px; |
| color: #666; |
| margin-bottom: 4px; |
| } |
| |
| .task-meta-value { |
| font-size: 16px; |
| font-weight: 500; |
| } |
| |
| .task-description { |
| background-color: #f9f9f9; |
| padding: 16px; |
| border-radius: 4px; |
| margin-bottom: 20px; |
| min-height: 100px; |
| } |
| |
| /* Modal Dialog Styling */ |
| .modal-overlay { |
| display: none; |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background-color: rgba(0, 0, 0, 0.5); |
| z-index: 1000; |
| backdrop-filter: blur(2px); |
| } |
| |
| .modal-content { |
| position: fixed; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| background-color: white; |
| padding: 20px; |
| border-radius: 5px; |
| width: 80%; |
| max-width: 500px; |
| max-height: 80vh; |
| overflow-y: auto; |
| z-index: 1001; |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
| } |
| |
| .modal-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 15px; |
| padding-bottom: 10px; |
| border-bottom: 1px solid #eee; |
| } |
| |
| .modal-close { |
| cursor: pointer; |
| font-size: 20px; |
| color: #999; |
| } |
| |
| .modal-title { |
| font-size: 18px; |
| font-weight: 600; |
| color: var(--primary-color); |
| } |
| |
| /* Form styling */ |
| .sidebar .block-container { |
| padding-top: 1rem; |
| } |
| |
| /* Remove default Streamlit formatting */ |
| #MainMenu {visibility: hidden;} |
| footer {visibility: hidden;} |
| .block-container {padding-top: 0.5rem !important; padding-bottom: 0rem !important;} |
| |
| /* Fix for streamlit container size */ |
| .css-18e3th9 { |
| padding-top: 0rem !important; |
| padding-bottom: 0rem !important; |
| padding-left: 1rem; |
| padding-right: 1rem; |
| } |
| |
| /* Fix for the main content area */ |
| .css-1d391kg { |
| width: 100%; |
| max-width: 100%; |
| } |
| |
| /* Ensure columns are properly sized */ |
| [data-testid="stHorizontalBlock"] { |
| width: 100%; |
| gap: 10px; |
| margin-top: 0 !important; |
| padding-top: 0 !important; |
| } |
| |
| /* Button styling */ |
| .stButton > button { |
| background-color: var(--secondary-color); |
| color: white; |
| border: none; |
| padding: 8px 12px; |
| border-radius: 4px; |
| font-weight: 500; |
| } |
| |
| .stButton > button:hover { |
| background-color: var(--primary-color); |
| } |
| |
| /* Full width buttons */ |
| .full-width-button button { |
| width: 100%; |
| } |
| |
| /* Sidebar styling */ |
| .sidebar-title { |
| font-size: 1.2rem; |
| font-weight: 600; |
| margin-bottom: 1rem; |
| color: var(--primary-color); |
| } |
| |
| /* Hide duplicated elements */ |
| .status-hidden { |
| display: none; |
| } |
| |
| /* Status Pills */ |
| .status-pill { |
| display: inline-block; |
| padding: 3px 8px; |
| border-radius: 12px; |
| font-size: 10px; |
| font-weight: 500; |
| color: white; |
| margin-left: 8px; |
| } |
| |
| .status-pill.todo { |
| background-color: var(--todo-color); |
| } |
| |
| .status-pill.in-progress { |
| background-color: var(--progress-color); |
| } |
| |
| .status-pill.done { |
| background-color: var(--done-color); |
| } |
| |
| .status-pill.backlog { |
| background-color: var(--backlog-color); |
| } |
| |
| /* Date display in task card */ |
| .date-display { |
| font-size: 9px; |
| color: #777; |
| } |
| |
| /* Fix for error messages */ |
| .stAlert { |
| margin-top: 10px; |
| margin-bottom: 10px; |
| } |
| |
| /* Task progress bar */ |
| .progress-container { |
| width: 100%; |
| background-color: #f1f1f1; |
| border-radius: 4px; |
| margin-top: 8px; |
| } |
| |
| .progress-bar { |
| height: 6px; |
| border-radius: 4px; |
| } |
| |
| .progress-bar.todo { |
| background-color: var(--todo-color); |
| width: 0%; |
| } |
| |
| .progress-bar.in-progress { |
| background-color: var(--progress-color); |
| width: 50%; |
| } |
| |
| .progress-bar.done { |
| background-color: var(--done-color); |
| width: 100%; |
| } |
| |
| .progress-bar.backlog { |
| background-color: var(--backlog-color); |
| width: 10%; |
| } |
| |
| /* Task details sections */ |
| .section-divider { |
| height: 1px; |
| background-color: #eee; |
| margin: 15px 0; |
| } |
| |
| /* Improved header layout */ |
| h1 { |
| margin-top: 0 !important; |
| padding-top: 0 !important; |
| margin-bottom: 16px !important; |
| } |
| |
| /* Board container for better layout */ |
| .board-container { |
| margin-top: 0 !important; |
| padding: 0 !important; |
| } |
| |
| /* Column headers */ |
| .column-header { |
| font-size: 16px; |
| margin: 0 0 10px 0; |
| padding: 8px; |
| background-color: #f5f5f5; |
| border-radius: 4px; |
| text-align: center; |
| } |
| |
| /* Task board header */ |
| .task-board-header { |
| display: flex; |
| align-items: center; |
| margin-bottom: 16px; |
| padding-bottom: 10px; |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .task-board-title { |
| font-size: 24px; |
| font-weight: 600; |
| color: var(--primary-color); |
| margin: 0; |
| padding: 0; |
| } |
| |
| /* Fix for streamlit elements */ |
| .stTextInput, .stTextArea, .stSelectbox, .stDateInput { |
| padding-bottom: 10px !important; |
| } |
| |
| div[data-testid="stVerticalBlock"] { |
| gap: 0 !important; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
| |
| USER_HOME = os.path.expanduser("~") |
| TEMP_DIR = tempfile.gettempdir() |
|
|
| |
| TASKS_FILE_LOCATIONS = [ |
| os.path.join(os.getcwd(), "data", "tasks.json"), |
| os.path.join(os.getcwd(), "tasks.json"), |
| os.path.join(USER_HOME, "streamlit_tasks.json"), |
| os.path.join(TEMP_DIR, "streamlit_tasks.json") |
| ] |
|
|
| ASSIGNEE_FILE = "Assignee.txt" |
|
|
| |
| def get_writable_path(locations): |
| |
| for loc in locations: |
| if os.path.exists(loc): |
| if os.access(os.path.dirname(loc), os.W_OK): |
| return loc |
| |
| |
| for loc in locations: |
| dir_path = os.path.dirname(loc) |
| try: |
| if not os.path.exists(dir_path): |
| os.makedirs(dir_path, exist_ok=True) |
| |
| test_file = os.path.join(dir_path, ".write_test") |
| with open(test_file, 'w') as f: |
| f.write("test") |
| os.remove(test_file) |
| return loc |
| except (PermissionError, OSError): |
| continue |
| |
| |
| return None |
|
|
| |
| TASKS_FILE = get_writable_path(TASKS_FILE_LOCATIONS) |
|
|
| |
| STATUS_COLORS = { |
| "To Do": "todo", |
| "In Progress": "in-progress", |
| "Done": "done", |
| "Backlog": "backlog" |
| } |
|
|
| def create_empty_df(): |
| """Create an empty DataFrame with the required columns""" |
| return pd.DataFrame(columns=[ |
| 'ID', 'Title', 'Description', 'Assignee', 'Status', |
| 'Date Started', 'Date to Finish', 'Due Date', 'Documents' |
| ]) |
| |
| def load_tasks(): |
| """Load tasks from JSON file with better error handling""" |
| |
| if 'in_memory_tasks' in st.session_state: |
| try: |
| df = pd.DataFrame(st.session_state.in_memory_tasks) |
| |
| |
| if not df.empty: |
| date_cols = ['Date Started', 'Date to Finish', 'Due Date'] |
| for col in date_cols: |
| if col in df.columns: |
| df[col] = pd.to_datetime(df[col]).dt.date |
| |
| if not df.empty: |
| return df |
| except Exception as e: |
| print(f"Error loading in-memory tasks: {str(e)}") |
| |
| |
| try: |
| |
| if TASKS_FILE and os.path.exists(TASKS_FILE): |
| with open(TASKS_FILE, 'r') as f: |
| tasks_data = json.load(f) |
| df = pd.DataFrame(tasks_data) |
| print(f"Loaded tasks from: {TASKS_FILE}") |
| else: |
| |
| return create_empty_df() |
| |
| |
| if not df.empty: |
| |
| date_cols = ['Date Started', 'Date to Finish', 'Due Date'] |
| for col in date_cols: |
| if col in df.columns: |
| df[col] = pd.to_datetime(df[col]).dt.date |
| |
| |
| if 'ID' not in df.columns: |
| df['ID'] = [f"#{i}" for i in range(1, len(df) + 1)] |
| |
| return df |
| else: |
| return create_empty_df() |
| |
| except Exception as e: |
| print(f"Error loading tasks from files: {str(e)}") |
| return create_empty_df() |
|
|
| def save_tasks(): |
| """Save tasks to JSON file with better error handling""" |
| try: |
| |
| tasks_dict = st.session_state.tasks.to_dict(orient='records') |
| |
| |
| for task in tasks_dict: |
| for key, value in task.items(): |
| if isinstance(value, (datetime, date)): |
| task[key] = value.isoformat() |
| |
| |
| if TASKS_FILE: |
| try: |
| |
| os.makedirs(os.path.dirname(TASKS_FILE), exist_ok=True) |
| |
| |
| with open(TASKS_FILE, 'w') as f: |
| json.dump(tasks_dict, f, indent=2) |
| |
| print(f"Successfully saved tasks to: {TASKS_FILE}") |
| |
| st.session_state.using_memory_storage = False |
| return True |
| except Exception as e: |
| print(f"Error saving to {TASKS_FILE}: {str(e)}") |
| |
| |
| |
| st.session_state.in_memory_tasks = tasks_dict |
| st.session_state.using_memory_storage = True |
| |
| if not TASKS_FILE: |
| st.warning("No writable location found. Tasks stored in memory instead.") |
| else: |
| st.warning(f"Could not save to file: {TASKS_FILE}. Tasks stored in memory instead.") |
| |
| return False |
| except Exception as e: |
| print(f"Error saving tasks: {str(e)}") |
| |
| try: |
| st.session_state.in_memory_tasks = tasks_dict |
| except: |
| pass |
| st.session_state.using_memory_storage = True |
| st.warning(f"Error saving tasks: {str(e)}. Tasks stored in memory instead.") |
| return False |
|
|
| def load_assignees(): |
| """Load assignees from Assignee.txt with better error handling""" |
| |
| if os.path.exists(ASSIGNEE_FILE): |
| return parse_assignee_file(ASSIGNEE_FILE) |
| |
| |
| possible_locations = [ |
| "./Assignee.txt", |
| "/app/Assignee.txt", |
| os.path.join(os.getcwd(), "Assignee.txt"), |
| "/data/Assignee.txt", |
| "../Assignee.txt", |
| os.path.join(USER_HOME, "Assignee.txt"), |
| os.path.join(TEMP_DIR, "Assignee.txt") |
| ] |
| |
| for location in possible_locations: |
| if os.path.exists(location): |
| print(f"Found assignee file at: {location}") |
| return parse_assignee_file(location) |
| |
| |
| try: |
| default_assignees = ["Team Member 1", "Team Member 2", "Team Member 3"] |
| default_location = os.path.join(USER_HOME, "Assignee.txt") if os.access(USER_HOME, os.W_OK) else os.path.join(TEMP_DIR, "Assignee.txt") |
| |
| with open(default_location, "w") as f: |
| for assignee in default_assignees: |
| f.write(f"{assignee}\n") |
| |
| print(f"Created default assignee file at: {default_location}") |
| return default_assignees |
| except Exception as e: |
| print(f"Error creating default assignee file: {str(e)}") |
| |
| return ["Team Member 1", "Team Member 2", "Team Member 3"] |
|
|
| def parse_assignee_file(file_path): |
| """Parse the assignee file and extract names""" |
| try: |
| with open(file_path, "r") as f: |
| content = f.read() |
| |
| print(f"Content of {file_path}:\n{content}") |
| |
| |
| assignees = [] |
| lines = [line.strip() for line in content.split('\n') if line.strip()] |
| |
| for line in lines: |
| if '-' in line: |
| |
| name_part = line.split('-')[0].strip() |
| if name_part: |
| assignees.append(name_part) |
| elif line: |
| |
| assignees.append(line) |
| |
| |
| print(f"Found {len(assignees)} assignees: {assignees}") |
| |
| if not assignees: |
| st.warning(f"No valid assignee names found in {file_path}. Using default assignees.") |
| return ["Team Member 1", "Team Member 2", "Team Member 3"] |
| |
| return assignees |
| except Exception as e: |
| st.error(f"Error reading assignee file: {str(e)}") |
| print(f"Error reading {file_path}: {str(e)}") |
| return ["Team Member 1", "Team Member 2", "Team Member 3"] |
|
|
| |
| def generate_task_id(): |
| if len(st.session_state.tasks) == 0: |
| return "#1" |
| |
| |
| try: |
| existing_ids = [int(task_id.replace('#', '')) for task_id in st.session_state.tasks['ID'].tolist() if isinstance(task_id, str) and task_id.startswith('#')] |
| if existing_ids: |
| return f"#{max(existing_ids) + 1}" |
| else: |
| return "#1" |
| except: |
| |
| return f"#{random.randint(1, 100)}" |
|
|
| def update_task(task_idx, field, value): |
| """Update a task field and save changes""" |
| st.session_state.tasks.at[task_idx, field] = value |
| save_tasks() |
| return True |
|
|
| def delete_task(task_id): |
| """Delete a task by ID""" |
| task_idx = st.session_state.tasks[st.session_state.tasks['ID'] == task_id].index |
| if not task_idx.empty: |
| st.session_state.tasks = st.session_state.tasks.drop(task_idx).reset_index(drop=True) |
| save_tasks() |
| return True |
| return False |
|
|
| |
| if 'tasks' not in st.session_state: |
| st.session_state.tasks = load_tasks() |
| |
| if 'selected_task_id' not in st.session_state: |
| st.session_state.selected_task_id = None |
| |
| if 'in_memory_tasks' not in st.session_state: |
| st.session_state.in_memory_tasks = [] |
| |
| |
| if 'using_memory_storage' not in st.session_state: |
| st.session_state.using_memory_storage = False |
|
|
| |
| if TASKS_FILE: |
| print(f"Using task storage location: {TASKS_FILE}") |
| else: |
| print("No writable location found. Using in-memory storage only.") |
| st.session_state.using_memory_storage = True |
|
|
| |
| assignee_list = load_assignees() |
|
|
| |
| st.markdown(""" |
| <div class="task-board-header"> |
| <h1 class="task-board-title">๐ Task Board</h1> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| with st.sidebar: |
| st.markdown('<div class="sidebar-title">โ Add New Task</div>', unsafe_allow_html=True) |
| title = st.text_input("Task Title", key="new_title") |
| description = st.text_area("Description", key="new_desc", height=100) |
| |
| |
| col1, col2 = st.columns(2) |
| with col1: |
| assignee = st.selectbox("Assignee", assignee_list, key="new_assignee") |
| with col2: |
| status = st.selectbox("Status", ["Backlog", "To Do", "In Progress", "Done"], key="new_status") |
| |
| |
| col3, col4 = st.columns(2) |
| with col3: |
| date_started = st.date_input("Date Started", value=datetime.now().date(), key="new_date_start") |
| with col4: |
| date_to_finish = st.date_input("Date to Finish", value=datetime.now().date(), key="new_date_finish") |
| |
| due_date = st.date_input("Due Date", key="new_due_date") |
| |
| |
| if st.button("Add Task", key="add_btn"): |
| if title: |
| try: |
| task_id = generate_task_id() |
| new_task = { |
| 'ID': task_id, |
| 'Title': title, |
| 'Description': description, |
| 'Assignee': assignee, |
| 'Status': status, |
| 'Date Started': date_started, |
| 'Date to Finish': date_to_finish, |
| 'Due Date': due_date |
| } |
| st.session_state.tasks = pd.concat([ |
| st.session_state.tasks, |
| pd.DataFrame([new_task]) |
| ], ignore_index=True) |
| save_result = save_tasks() |
| |
| st.success("Task added successfully!") |
| |
| st.rerun() |
| except Exception as e: |
| st.error(f"Error adding task: {str(e)}") |
| else: |
| st.warning("Please enter a task title") |
|
|
| |
| with st.sidebar: |
| st.markdown('<div class="sidebar-title">๐ Update Task Status</div>', unsafe_allow_html=True) |
| |
| |
| if not st.session_state.tasks.empty: |
| |
| task_options = [f"{task['ID']} - {task['Title']}" for _, task in st.session_state.tasks.iterrows()] |
| selected_task = st.selectbox("Select Task", task_options, key="status_update_task") |
| |
| |
| selected_id = selected_task.split(" - ")[0] if selected_task else None |
| |
| if selected_id: |
| |
| task_idx = st.session_state.tasks[st.session_state.tasks['ID'] == selected_id].index |
| |
| if not task_idx.empty: |
| task_idx = task_idx[0] |
| current_status = st.session_state.tasks.at[task_idx, 'Status'] |
| |
| |
| new_status = st.selectbox("New Status", |
| ["Backlog", "To Do", "In Progress", "Done"], |
| index=["Backlog", "To Do", "In Progress", "Done"].index(current_status) |
| if current_status in ["Backlog", "To Do", "In Progress", "Done"] else 0, |
| key="new_status_select") |
| |
| if st.button("Update Status", key="update_status_btn"): |
| if new_status != current_status: |
| update_task(task_idx, 'Status', new_status) |
| st.success(f"Updated task {selected_id} status to {new_status}") |
| st.rerun() |
|
|
| |
| |
| url_task_id = st.query_params.get('task_id', None) |
|
|
| |
| if url_task_id: |
| st.session_state.selected_task_id = url_task_id |
| |
| |
| selected_task_id = st.session_state.selected_task_id |
| |
| |
| if selected_task_id: |
| |
| task_df = st.session_state.tasks[st.session_state.tasks['ID'] == selected_task_id] |
| |
| if not task_df.empty: |
| |
| if st.button("โ Back to Board", key="back_btn"): |
| st.session_state.selected_task_id = None |
| st.rerun() |
| |
| |
| task = task_df.iloc[0] |
| status_class = STATUS_COLORS.get(task['Status'], "backlog") |
| |
| |
| st.markdown('<div class="task-details-container">', unsafe_allow_html=True) |
| |
| |
| st.markdown(f""" |
| <div class="task-details-header"> |
| <h2 class="task-details-title">{task['Title']} |
| <span class="status-pill {status_class}">{task['Status']}</span> |
| </h2> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| st.markdown('<div class="task-meta-grid">', unsafe_allow_html=True) |
| |
| |
| st.markdown(f""" |
| <div class="task-meta-item"> |
| <div class="task-meta-label">Task ID</div> |
| <div class="task-meta-value">{selected_task_id}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| st.markdown(f""" |
| <div class="task-meta-item"> |
| <div class="task-meta-label">Assignee</div> |
| <div class="task-meta-value">{task['Assignee']}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| date_started_str = task['Date Started'].strftime('%m/%d/%Y') if 'Date Started' in task and pd.notna(task['Date Started']) else "Not set" |
| date_to_finish_str = task['Date to Finish'].strftime('%m/%d/%Y') if 'Date to Finish' in task and pd.notna(task['Date to Finish']) else "Not set" |
| due_date_str = task['Due Date'].strftime('%m/%d/%Y') if 'Due Date' in task and pd.notna(task['Due Date']) else "Not set" |
| |
| |
| st.markdown(f""" |
| <div class="task-meta-item"> |
| <div class="task-meta-label">Date Started</div> |
| <div class="task-meta-value">{date_started_str}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| st.markdown(f""" |
| <div class="task-meta-item"> |
| <div class="task-meta-label">Date to Finish</div> |
| <div class="task-meta-value">{date_to_finish_str}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| st.markdown(f""" |
| <div class="task-meta-item"> |
| <div class="task-meta-label">Due Date</div> |
| <div class="task-meta-value">{due_date_str}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| st.markdown('</div>', unsafe_allow_html=True) |
| |
| |
| st.markdown(f""" |
| <div class="progress-container"> |
| <div class="progress-bar {status_class}"></div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| st.markdown('<div class="section-divider"></div>', unsafe_allow_html=True) |
| st.markdown('<div class="task-details-section">', unsafe_allow_html=True) |
| st.markdown('<h3>Edit Task</h3>', unsafe_allow_html=True) |
|
|
| |
| new_description = st.text_area("Edit Description", |
| value=task['Description'] if task['Description'] else '', |
| height=150, |
| key=f"edit_desc_{selected_task_id}") |
|
|
| |
| uploaded_files = st.file_uploader("Upload Documents", |
| accept_multiple_files=True, |
| key=f"upload_{selected_task_id}") |
| if uploaded_files: |
| |
| upload_status = st.empty() |
| |
| |
| for uploaded_file in uploaded_files: |
| |
| upload_status.info(f"Processing {uploaded_file.name}...") |
| |
| try: |
| |
| file_content = uploaded_file.getvalue() |
| |
| |
| task_doc_dir = os.path.join(TEMP_DIR, f"task_docs_{selected_task_id.replace('#', '')}") |
| os.makedirs(task_doc_dir, exist_ok=True) |
| |
| |
| safe_filename = uploaded_file.name.replace(" ", "_").replace("/", "_") |
| file_path = os.path.join(task_doc_dir, safe_filename) |
| |
| |
| with open(file_path, "wb") as f: |
| f.write(file_content) |
| |
| |
| if 'Documents' not in task or not isinstance(task['Documents'], list): |
| task_df.at[task_df.index[0], 'Documents'] = [safe_filename] |
| else: |
| docs = task['Documents'] if isinstance(task['Documents'], list) else [] |
| if safe_filename not in docs: |
| docs.append(safe_filename) |
| task_df.at[task_df.index[0], 'Documents'] = docs |
| |
| |
| st.session_state.tasks.loc[task_df.index[0]] = task_df.iloc[0] |
| save_tasks() |
| |
| upload_status.success(f"Uploaded {uploaded_file.name} successfully!") |
| except Exception as e: |
| |
| upload_status.error(f"Error uploading {uploaded_file.name}. Please try a smaller file or a different filename.") |
| |
| print(f"Upload error details: {str(e)}") |
| |
| |
| st.success("Upload processing complete") |
|
|
| |
| if 'Documents' in task and isinstance(task['Documents'], list) and len(task['Documents']) > 0: |
| st.markdown('<div class="section-divider"></div>', unsafe_allow_html=True) |
| st.markdown('<div class="task-details-section">', unsafe_allow_html=True) |
| st.markdown('<h3>Attached Documents</h3>', unsafe_allow_html=True) |
| |
| for doc in task['Documents']: |
| |
| doc_dir = os.path.join(os.path.dirname(TASKS_FILE), "task_documents") |
| task_doc_dir = os.path.join(doc_dir, f"task_{selected_task_id.replace('#', '')}") |
| file_path = os.path.join(task_doc_dir, doc) |
| |
| |
| if os.path.exists(file_path): |
| col1, col2 = st.columns([3, 1]) |
| with col1: |
| st.markdown(f"๐ **{doc}**") |
| with col2: |
| with open(file_path, "rb") as file: |
| st.download_button( |
| label="Download", |
| data=file, |
| file_name=doc, |
| key=f"download_{doc}_{selected_task_id}" |
| ) |
| |
| st.markdown('</div>', unsafe_allow_html=True) |
|
|
| st.markdown('</div>', unsafe_allow_html=True) |
| |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| |
| new_status = st.selectbox( |
| "Update Status", |
| ["Backlog", "To Do", "In Progress", "Done"], |
| index=["Backlog", "To Do", "In Progress", "Done"].index(task['Status']) if task['Status'] in ["Backlog", "To Do", "In Progress", "Done"] else 0 |
| ) |
| |
| if st.button("Save Status Change", use_container_width=True): |
| if new_status != task['Status']: |
| task_idx = task_df.index[0] |
| update_task(task_idx, 'Status', new_status) |
| st.success(f"Updated task status to {new_status}") |
| st.rerun() |
| |
| with col2: |
| if st.button("Delete Task", use_container_width=True): |
| if delete_task(selected_task_id): |
| st.success(f"Task {selected_task_id} deleted!") |
| |
| for key in list(st.query_params.keys()): |
| del st.query_params[key] |
| st.rerun() |
| else: |
| st.error(f"Failed to delete task {selected_task_id}") |
| |
| st.markdown('</div>', unsafe_allow_html=True) |
| |
| |
| st.markdown('</div>', unsafe_allow_html=True) |
| else: |
| st.error(f"Task {selected_task_id} not found") |
| |
| if st.button("Back to Board"): |
| for key in list(st.query_params.keys()): |
| del st.query_params[key] |
| st.rerun() |
|
|
| else: |
| |
| if st.session_state.tasks.empty: |
| st.info("No tasks yet. Add your first task using the sidebar.") |
| else: |
| |
| statuses = ["To Do", "In Progress", "Done", "Backlog"] |
| |
| |
| st.markdown('<div style="margin-bottom: 0; padding-bottom: 0;">', unsafe_allow_html=True) |
| |
| |
| cols = st.columns(len(statuses)) |
| |
| |
| for i, status_name in enumerate(statuses): |
| with cols[i]: |
| st.markdown(f'<h3 style="font-size: 16px; margin-bottom: 10px;">{status_name}</h3>', unsafe_allow_html=True) |
| |
| |
| status_tasks = st.session_state.tasks[st.session_state.tasks['Status'] == status_name] |
| |
| if status_tasks.empty: |
| st.markdown("<div style='text-align:center; color:#999; padding:10px; font-size:12px;'>No tasks</div>", unsafe_allow_html=True) |
| else: |
| |
| st.markdown('<div class="task-container">', unsafe_allow_html=True) |
| |
| |
| for idx, task in status_tasks.iterrows(): |
| status_class = STATUS_COLORS.get(task['Status'], "backlog") |
| task_id = task['ID'] if 'ID' in task and pd.notna(task['ID']) else f"#{idx+1}" |
| |
| |
| date_started_str = task['Date Started'].strftime('%m/%d') if 'Date Started' in task and pd.notna(task['Date Started']) else "Not set" |
| date_to_finish_str = task['Date to Finish'].strftime('%m/%d') if 'Date to Finish' in task and pd.notna(task['Date to Finish']) else "Not set" |
|
|
| |
| with st.container(): |
| col1, col2 = st.columns([4, 1]) |
| with col1: |
| st.markdown(f""" |
| <div class="task-card" data-task-id="{task_id}"> |
| <div class="card-header {status_class}"> |
| <span class="task-id">{task_id}</span> |
| </div> |
| <div class="task-title">{task['Title']}</div> |
| <div class="task-assignee">{task['Assignee']}</div> |
| <div class="task-dates"> |
| <span class="date-display">Start: {date_started_str}</span> |
| <span class="date-display">Finish: {date_to_finish_str}</span> |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| with col2: |
| if st.button("View", key=f"view_{task_id}"): |
| st.session_state.selected_task_id = task_id |
| st.rerun() |
|
|
| |
| st.markdown(""" |
| <script> |
| // Make task cards clickable - will navigate to the task details view |
| document.addEventListener('DOMContentLoaded', function() { |
| console.log('Setting up task board interactions'); |
| // Add any additional JavaScript for enhancing the task board |
| }); |
| </script> |
| """, unsafe_allow_html=True) |