Kikulika commited on
Commit
8f0dea4
·
verified ·
1 Parent(s): f56c995

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +396 -274
app.py CHANGED
@@ -1,10 +1,10 @@
1
- import streamlit as st
2
  import pandas as pd
3
  from datetime import datetime, date
4
  import os
5
  import sys
6
  import random
7
  import json
 
8
 
9
  # Disable Streamlit's telemetry and usage statistics
10
  os.environ['STREAMLIT_ANALYTICS_ENABLED'] = 'false'
@@ -279,40 +279,115 @@ st.markdown("""
279
  margin-top: 10px;
280
  margin-bottom: 10px;
281
  }
282
- </style>
283
-
284
- <script>
285
- document.addEventListener('DOMContentLoaded', function() {
286
- // Task card click handler setup
287
- document.querySelectorAll('.task-card').forEach(card => {
288
- card.addEventListener('click', function() {
289
- const taskId = this.getAttribute('data-task-id');
290
- openTaskModal(taskId);
291
- });
292
- });
293
 
294
- // Close modal when clicking the X or outside the modal
295
- document.querySelectorAll('.modal-close, .modal-overlay').forEach(el => {
296
- el.addEventListener('click', function(e) {
297
- if (e.target === this) {
298
- closeAllModals();
299
- }
300
- });
301
- });
 
 
 
 
 
 
 
 
 
302
 
303
- function openTaskModal(taskId) {
304
- document.getElementById(`modal-${taskId}`).style.display = 'block';
 
 
 
 
 
 
 
305
  }
306
 
307
- function closeAllModals() {
308
- document.querySelectorAll('.modal-overlay').forEach(modal => {
309
- modal.style.display = 'none';
310
- });
 
 
 
311
  }
312
- });
313
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  """, unsafe_allow_html=True)
315
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  # Status color mapping
317
  STATUS_COLORS = {
318
  "To Do": "todo",
@@ -321,10 +396,6 @@ STATUS_COLORS = {
321
  "Backlog": "backlog"
322
  }
323
 
324
- # Set up paths
325
- TASKS_FILE = "./data/tasks.json" # Store in data directory for better permissions
326
- ASSIGNEE_FILE = "Assignee.txt" # Direct path to Assignee.txt
327
-
328
  def create_empty_df():
329
  """Create an empty DataFrame with the required columns"""
330
  return pd.DataFrame(columns=[
@@ -353,29 +424,15 @@ def load_tasks():
353
 
354
  # Try loading from file
355
  try:
356
- # Try the primary file location
357
- if os.path.exists(TASKS_FILE):
358
  with open(TASKS_FILE, 'r') as f:
359
  tasks_data = json.load(f)
360
  df = pd.DataFrame(tasks_data)
 
361
  else:
362
- # Try alternative locations if main file doesn't exist
363
- alt_locations = [
364
- "./tasks.json",
365
- os.path.join(os.path.expanduser("~"), "temp_tasks.json"),
366
- "/tmp/tasks.json"
367
- ]
368
-
369
- for location in alt_locations:
370
- if os.path.exists(location):
371
- with open(location, 'r') as f:
372
- tasks_data = json.load(f)
373
- df = pd.DataFrame(tasks_data)
374
- st.info(f"Loaded tasks from alternative location: {location}")
375
- break
376
- else:
377
- # If no files found, create empty dataframe
378
- return create_empty_df()
379
 
380
  # Process the dataframe
381
  if not df.empty:
@@ -398,7 +455,7 @@ def load_tasks():
398
  return create_empty_df()
399
 
400
  def save_tasks():
401
- """Save tasks to JSON file with better error handling and directory creation"""
402
  try:
403
  # Convert DataFrame to a list of dictionaries
404
  tasks_dict = st.session_state.tasks.to_dict(orient='records')
@@ -409,34 +466,44 @@ def save_tasks():
409
  if isinstance(value, (datetime, date)):
410
  task[key] = value.isoformat()
411
 
412
- # Ensure the directory exists
413
- os.makedirs(os.path.dirname(TASKS_FILE), exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
- # Try multiple file locations if permission denied
416
- try:
417
- # Save to JSON file
418
- with open(TASKS_FILE, 'w') as f:
419
- json.dump(tasks_dict, f, indent=2)
420
- return True
421
- except PermissionError:
422
- # Try saving to a temp file in a user-writable location
423
- temp_file = os.path.join(os.path.expanduser("~"), "temp_tasks.json")
424
- with open(temp_file, 'w') as f:
425
- json.dump(tasks_dict, f, indent=2)
426
- st.info(f"Tasks saved to alternative location: {temp_file}")
427
- return True
428
  except Exception as e:
429
  print(f"Error saving tasks: {str(e)}")
430
- # Using a session state to store tasks in memory as fallback
431
  st.session_state.in_memory_tasks = tasks_dict
432
- st.warning(f"Could not save to file: {str(e)}. Tasks stored in memory instead.")
 
433
  return False
434
 
435
  def load_assignees():
436
- """Load assignees from Assignee.txt with strict requirement"""
437
- # Always log file contents for debugging
438
- log_files_in_directory()
439
-
440
  # Check direct path first
441
  if os.path.exists(ASSIGNEE_FILE):
442
  return parse_assignee_file(ASSIGNEE_FILE)
@@ -448,15 +515,30 @@ def load_assignees():
448
  os.path.join(os.getcwd(), "Assignee.txt"),
449
  "/data/Assignee.txt",
450
  "../Assignee.txt",
 
 
451
  ]
452
 
453
  for location in possible_locations:
454
  if os.path.exists(location):
 
455
  return parse_assignee_file(location)
456
 
457
- # If we reach here, the file wasn't found anywhere
458
- st.error("Assignee.txt file not found. Please create this file in the app directory.")
459
- return ["ERROR: Please create Assignee.txt file"]
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
  def parse_assignee_file(file_path):
462
  """Parse the assignee file and extract names"""
@@ -484,35 +566,14 @@ def parse_assignee_file(file_path):
484
  print(f"Found {len(assignees)} assignees: {assignees}")
485
 
486
  if not assignees:
487
- st.warning(f"No valid assignee names found in {file_path}. File exists but may be empty or improperly formatted.")
488
- return ["ERROR: Empty or invalid Assignee.txt"]
489
 
490
  return assignees
491
  except Exception as e:
492
  st.error(f"Error reading assignee file: {str(e)}")
493
  print(f"Error reading {file_path}: {str(e)}")
494
- return ["ERROR: Cannot read Assignee.txt"]
495
-
496
- def log_files_in_directory():
497
- """Log files in current directory for debugging"""
498
- try:
499
- cwd = os.getcwd()
500
- files = os.listdir(cwd)
501
- print(f"Files in {cwd}: {files}")
502
-
503
- # Also check parent directory
504
- parent = os.path.dirname(cwd)
505
- if os.path.exists(parent):
506
- parent_files = os.listdir(parent)
507
- print(f"Files in parent directory {parent}: {parent_files}")
508
-
509
- # Check app directory if different
510
- app_dir = "/app"
511
- if os.path.exists(app_dir) and app_dir != cwd:
512
- app_files = os.listdir(app_dir)
513
- print(f"Files in {app_dir}: {app_files}")
514
- except Exception as e:
515
- print(f"Error listing directory contents: {str(e)}")
516
 
517
  # Generate a new task ID
518
  def generate_task_id():
@@ -534,6 +595,7 @@ def update_task(task_idx, field, value):
534
  """Update a task field and save changes"""
535
  st.session_state.tasks.at[task_idx, field] = value
536
  save_tasks()
 
537
 
538
  def delete_task(task_id):
539
  """Delete a task by ID"""
@@ -558,19 +620,21 @@ if 'in_memory_tasks' not in st.session_state:
558
  if 'using_memory_storage' not in st.session_state:
559
  st.session_state.using_memory_storage = False
560
 
561
- # Create a data directory for file storage
562
- try:
563
- os.makedirs(os.path.dirname(TASKS_FILE), exist_ok=True)
564
- except Exception as e:
565
- print(f"Could not create data directory: {str(e)}")
566
- # If we can't create the directory, set a flag to use in-memory storage
567
  st.session_state.using_memory_storage = True
568
 
569
- # Load assignees directly from the file
570
  assignee_list = load_assignees()
571
 
 
572
  if st.session_state.using_memory_storage:
573
- st.markdown('<div style="background-color: #fff3cd; color: #856404; padding: 8px; border-radius: 4px; font-size: 12px; margin-bottom: 10px;">Using in-memory storage due to file permission issues. Tasks may not persist after refresh.</div>', unsafe_allow_html=True)
 
 
574
 
575
  # Sidebar for new tasks
576
  with st.sidebar:
@@ -613,18 +677,221 @@ with st.sidebar:
613
  st.session_state.tasks,
614
  pd.DataFrame([new_task])
615
  ], ignore_index=True)
616
- save_tasks()
617
- # Show success but don't mention storage
618
- st.success("Task added!")
 
 
619
  except Exception as e:
620
  st.error(f"Error adding task: {str(e)}")
621
- # Still add to session state even if file save fails
622
- st.session_state.in_memory_tasks = st.session_state.tasks.to_dict(orient='records')
623
- st.info("Task was added to memory but might not persist after refresh.")
624
  else:
625
  st.warning("Please enter a task title")
 
 
 
 
 
 
 
 
 
 
 
 
 
626
 
627
- # Display tasks in a horizontal grid layout
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  if st.session_state.tasks.empty:
629
  st.info("No tasks yet. Add your first task using the sidebar.")
630
  else:
@@ -660,14 +927,23 @@ else:
660
  date_started_str = task['Date Started'].strftime('%m/%d') if 'Date Started' in task and pd.notna(task['Date Started']) else "Not set"
661
  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"
662
 
663
- # Create the card HTML with dates and make it clickable - more compact
664
  card_html = f"""
665
- <div class="task-card" data-task-id="{task_id}" onclick="document.getElementById('modal-{task_id}').style.display='block';">
666
  <div class="card-header {status_class}">
667
  <span class="task-id">{task_id}</span>
668
- <span>{task['Status']}</span>
 
 
 
 
 
 
 
 
 
669
  </div>
670
- <div class="task-title">{task['Title']}</div>
671
  <div class="task-assignee">{task['Assignee']}</div>
672
  <div class="task-dates">
673
  <span class="date-display">Start: {date_started_str}</span>
@@ -683,158 +959,4 @@ else:
683
  st.markdown('</div>', unsafe_allow_html=True)
684
 
685
  # Close container
686
- st.markdown('</div>', unsafe_allow_html=True)
687
-
688
- # Create task modals for each task with more compact display
689
- for idx, task in st.session_state.tasks.iterrows():
690
- task_id = task['ID'] if 'ID' in task and pd.notna(task['ID']) else f"#{idx+1}"
691
- status_class = STATUS_COLORS.get(task['Status'], "backlog")
692
-
693
- # Format dates for display in modal - more compact format
694
- date_started_str = task['Date Started'].strftime('%m/%d/%Y') if 'Date Started' in task and pd.notna(task['Date Started']) else "Not set"
695
- 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"
696
- due_date_str = task['Due Date'].strftime('%m/%d/%Y') if 'Due Date' in task and pd.notna(task['Due Date']) else "Not set"
697
-
698
- # Create modal HTML with improved layout
699
- modal_html = f"""
700
- <div id="modal-{task_id}" class="modal-overlay">
701
- <div class="modal-content">
702
- <div class="modal-header">
703
- <div class="modal-title">{task['Title']} <span class="status-pill {status_class}">{task['Status']}</span></div>
704
- <span class="modal-close" onclick="document.getElementById('modal-{task_id}').style.display='none';">&times;</span>
705
- </div>
706
- <div class="modal-body">
707
- <div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
708
- <div><strong>Task ID:</strong> {task_id}</div>
709
- <div><strong>Assignee:</strong> {task['Assignee']}</div>
710
- </div>
711
-
712
- <div style="margin-bottom: 15px;">
713
- <strong>Description:</strong><br>
714
- <div style="background: #f9f9f9; padding: 8px; border-radius: 4px; margin-top: 5px;">
715
- {task['Description'] if task['Description'] else "No description provided."}
716
- </div>
717
- </div>
718
-
719
- <div style="display: flex; justify-content: space-between; margin-bottom: 15px; font-size: 13px;">
720
- <div><strong>Date Started:</strong><br>{date_started_str}</div>
721
- <div><strong>Date to Finish:</strong><br>{date_to_finish_str}</div>
722
- <div><strong>Due Date:</strong><br>{due_date_str}</div>
723
- </div>
724
-
725
- <div id="task-status-{task_id}" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee;"></div>
726
- </div>
727
- </div>
728
- </div>
729
- """
730
-
731
- # Render the modal
732
- st.markdown(modal_html, unsafe_allow_html=True)
733
-
734
- # Add task status update widget inside the task modal - improved layout
735
- status_placeholder = st.empty()
736
- with status_placeholder:
737
- # Hide this with CSS and use JavaScript to move it into the modal
738
- st.markdown(f"<div id='status-update-{task_id}' style='display:none;'>", unsafe_allow_html=True)
739
-
740
- # Create two columns for the status update controls
741
- col1, col2 = st.columns([3, 1])
742
-
743
- with col1:
744
- new_status = st.selectbox(
745
- f"Update Status",
746
- ["Backlog", "To Do", "In Progress", "Done"],
747
- index=["Backlog", "To Do", "In Progress", "Done"].index(task['Status']) if task['Status'] in ["Backlog", "To Do", "In Progress", "Done"] else 0,
748
- key=f"modal_status_{idx}"
749
- )
750
-
751
- with col2:
752
- if st.button("Update", key=f"update_btn_{idx}"):
753
- try:
754
- update_task(idx, 'Status', new_status)
755
- st.success("Updated!")
756
- # Use JavaScript to close the modal and refresh
757
- st.markdown(f"""
758
- <script>
759
- setTimeout(function() {{
760
- document.getElementById('modal-{task_id}').style.display = 'none';
761
- window.location.reload();
762
- }}, 500);
763
- </script>
764
- """, unsafe_allow_html=True)
765
- except Exception as e:
766
- st.error(f"Update failed: {str(e)}")
767
-
768
- # Delete button with confirmation
769
- if st.button("Delete Task", key=f"delete_btn_{idx}"):
770
- confirm = st.checkbox("Confirm deletion?", key=f"confirm_delete_{idx}")
771
- if confirm:
772
- try:
773
- if delete_task(task_id):
774
- st.success(f"Task {task_id} deleted!")
775
- # Use JavaScript to close the modal and refresh
776
- st.markdown(f"""
777
- <script>
778
- setTimeout(function() {{
779
- document.getElementById('modal-{task_id}').style.display = 'none';
780
- window.location.reload();
781
- }}, 500);
782
- </script>
783
- """, unsafe_allow_html=True)
784
- else:
785
- st.error(f"Failed to delete task {task_id}")
786
- except Exception as e:
787
- st.error(f"Delete failed: {str(e)}")
788
-
789
- st.markdown("</div>", unsafe_allow_html=True)
790
-
791
- # JavaScript to move the status update widget into the modal
792
- st.markdown(f"""
793
- <script>
794
- // Move the status widget into the modal
795
- document.addEventListener('DOMContentLoaded', function() {{
796
- var statusEl = document.getElementById('status-update-{task_id}');
797
- var targetEl = document.getElementById('task-status-{task_id}');
798
- if (statusEl && targetEl) {{
799
- statusEl.style.display = 'block';
800
- targetEl.appendChild(statusEl);
801
- }}
802
- }});
803
- </script>
804
- """, unsafe_allow_html=True)
805
-
806
- # JavaScript to make task modals work properly in Streamlit
807
- st.markdown("""
808
- <script>
809
- // Ensure modals work properly with Streamlit's update system
810
- function setupTaskModals() {
811
- document.querySelectorAll('.task-card').forEach(card => {
812
- card.addEventListener('click', function(e) {
813
- e.preventDefault();
814
- const taskId = this.getAttribute('data-task-id');
815
- document.getElementById(`modal-${taskId}`).style.display = 'block';
816
- });
817
- });
818
-
819
- document.querySelectorAll('.modal-close, .modal-overlay').forEach(el => {
820
- el.addEventListener('click', function(e) {
821
- if (e.target === this) {
822
- document.querySelectorAll('.modal-overlay').forEach(modal => {
823
- modal.style.display = 'none';
824
- });
825
- }
826
- });
827
- });
828
- }
829
-
830
- // Run setup on initial load and after Streamlit updates
831
- document.addEventListener('DOMContentLoaded', setupTaskModals);
832
-
833
- // For Streamlit's reactive updates
834
- const observer = new MutationObserver(function(mutations) {
835
- setupTaskModals();
836
- });
837
-
838
- observer.observe(document.body, { childList: true, subtree: true });
839
- </script>
840
- """, unsafe_allow_html=True)
 
 
1
  import pandas as pd
2
  from datetime import datetime, date
3
  import os
4
  import sys
5
  import random
6
  import json
7
+ import tempfile
8
 
9
  # Disable Streamlit's telemetry and usage statistics
10
  os.environ['STREAMLIT_ANALYTICS_ENABLED'] = 'false'
 
279
  margin-top: 10px;
280
  margin-bottom: 10px;
281
  }
 
 
 
 
 
 
 
 
 
 
 
282
 
283
+ /* Inline status dropdown on task cards */
284
+ .task-status-dropdown {
285
+ position: relative;
286
+ display: inline-block;
287
+ }
288
+
289
+ .status-dropdown-btn {
290
+ background-color: transparent;
291
+ border: none;
292
+ cursor: pointer;
293
+ font-size: 12px;
294
+ color: #fff;
295
+ text-align: right;
296
+ padding: 0;
297
+ margin: 0;
298
+ width: auto;
299
+ }
300
 
301
+ .status-dropdown-content {
302
+ display: none;
303
+ position: absolute;
304
+ right: 0;
305
+ background-color: #f9f9f9;
306
+ min-width: 120px;
307
+ box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
308
+ z-index: 100;
309
+ border-radius: 4px;
310
  }
311
 
312
+ .status-dropdown-content a {
313
+ color: black;
314
+ padding: 8px 10px;
315
+ text-decoration: none;
316
+ display: block;
317
+ font-size: 12px;
318
+ text-align: left;
319
  }
320
+
321
+ .status-dropdown-content a:hover {
322
+ background-color: #f1f1f1;
323
+ }
324
+
325
+ .task-status-dropdown:hover .status-dropdown-content {
326
+ display: block;
327
+ }
328
+
329
+ /* Status colors for dropdown menu */
330
+ .status-option-todo {
331
+ border-left: 4px solid var(--todo-color);
332
+ }
333
+
334
+ .status-option-progress {
335
+ border-left: 4px solid var(--progress-color);
336
+ }
337
+
338
+ .status-option-done {
339
+ border-left: 4px solid var(--done-color);
340
+ }
341
+
342
+ .status-option-backlog {
343
+ border-left: 4px solid var(--backlog-color);
344
+ }
345
+ </style>
346
  """, unsafe_allow_html=True)
347
 
348
+ # Set up paths with more reliable locations
349
+ USER_HOME = os.path.expanduser("~")
350
+ TEMP_DIR = tempfile.gettempdir()
351
+
352
+ # Define multiple possible locations in order of preference
353
+ TASKS_FILE_LOCATIONS = [
354
+ os.path.join(os.getcwd(), "data", "tasks.json"), # Local data directory
355
+ os.path.join(os.getcwd(), "tasks.json"), # Current directory
356
+ os.path.join(USER_HOME, "streamlit_tasks.json"), # User's home directory
357
+ os.path.join(TEMP_DIR, "streamlit_tasks.json") # System temp directory
358
+ ]
359
+
360
+ ASSIGNEE_FILE = "Assignee.txt" # Direct path to Assignee.txt
361
+
362
+ # Function to find a writable location
363
+ def get_writable_path(locations):
364
+ # First check if any existing location is writable
365
+ for loc in locations:
366
+ if os.path.exists(loc):
367
+ if os.access(os.path.dirname(loc), os.W_OK):
368
+ return loc
369
+
370
+ # Then try to find a writable directory for a new file
371
+ for loc in locations:
372
+ dir_path = os.path.dirname(loc)
373
+ try:
374
+ if not os.path.exists(dir_path):
375
+ os.makedirs(dir_path, exist_ok=True)
376
+ # Test file creation
377
+ test_file = os.path.join(dir_path, ".write_test")
378
+ with open(test_file, 'w') as f:
379
+ f.write("test")
380
+ os.remove(test_file)
381
+ return loc
382
+ except (PermissionError, OSError):
383
+ continue
384
+
385
+ # If all else fails, use in-memory storage
386
+ return None
387
+
388
+ # Get the best writable path
389
+ TASKS_FILE = get_writable_path(TASKS_FILE_LOCATIONS)
390
+
391
  # Status color mapping
392
  STATUS_COLORS = {
393
  "To Do": "todo",
 
396
  "Backlog": "backlog"
397
  }
398
 
 
 
 
 
399
  def create_empty_df():
400
  """Create an empty DataFrame with the required columns"""
401
  return pd.DataFrame(columns=[
 
424
 
425
  # Try loading from file
426
  try:
427
+ # Try the selected file location
428
+ if TASKS_FILE and os.path.exists(TASKS_FILE):
429
  with open(TASKS_FILE, 'r') as f:
430
  tasks_data = json.load(f)
431
  df = pd.DataFrame(tasks_data)
432
+ print(f"Loaded tasks from: {TASKS_FILE}")
433
  else:
434
+ # If no tasks file exists, create empty dataframe
435
+ return create_empty_df()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
 
437
  # Process the dataframe
438
  if not df.empty:
 
455
  return create_empty_df()
456
 
457
  def save_tasks():
458
+ """Save tasks to JSON file with better error handling"""
459
  try:
460
  # Convert DataFrame to a list of dictionaries
461
  tasks_dict = st.session_state.tasks.to_dict(orient='records')
 
466
  if isinstance(value, (datetime, date)):
467
  task[key] = value.isoformat()
468
 
469
+ # Check if we have a writable file path
470
+ if TASKS_FILE:
471
+ try:
472
+ # Ensure the directory exists
473
+ os.makedirs(os.path.dirname(TASKS_FILE), exist_ok=True)
474
+
475
+ # Save to JSON file
476
+ with open(TASKS_FILE, 'w') as f:
477
+ json.dump(tasks_dict, f, indent=2)
478
+
479
+ print(f"Successfully saved tasks to: {TASKS_FILE}")
480
+ # Reset in-memory flag if we succeeded in saving to file
481
+ st.session_state.using_memory_storage = False
482
+ return True
483
+ except Exception as e:
484
+ print(f"Error saving to {TASKS_FILE}: {str(e)}")
485
+ # Fall back to in-memory storage
486
 
487
+ # Store in memory as fallback
488
+ st.session_state.in_memory_tasks = tasks_dict
489
+ st.session_state.using_memory_storage = True
490
+
491
+ if not TASKS_FILE:
492
+ st.warning("No writable location found. Tasks stored in memory instead.")
493
+ else:
494
+ st.warning(f"Could not save to file: {TASKS_FILE}. Tasks stored in memory instead.")
495
+
496
+ return False
 
 
 
497
  except Exception as e:
498
  print(f"Error saving tasks: {str(e)}")
499
+ # Store in memory as fallback
500
  st.session_state.in_memory_tasks = tasks_dict
501
+ st.session_state.using_memory_storage = True
502
+ st.warning(f"Error saving tasks: {str(e)}. Tasks stored in memory instead.")
503
  return False
504
 
505
  def load_assignees():
506
+ """Load assignees from Assignee.txt with better error handling"""
 
 
 
507
  # Check direct path first
508
  if os.path.exists(ASSIGNEE_FILE):
509
  return parse_assignee_file(ASSIGNEE_FILE)
 
515
  os.path.join(os.getcwd(), "Assignee.txt"),
516
  "/data/Assignee.txt",
517
  "../Assignee.txt",
518
+ os.path.join(USER_HOME, "Assignee.txt"),
519
+ os.path.join(TEMP_DIR, "Assignee.txt")
520
  ]
521
 
522
  for location in possible_locations:
523
  if os.path.exists(location):
524
+ print(f"Found assignee file at: {location}")
525
  return parse_assignee_file(location)
526
 
527
+ # If file not found, create a default one in a writable location
528
+ try:
529
+ default_assignees = ["Team Member 1", "Team Member 2", "Team Member 3"]
530
+ 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")
531
+
532
+ with open(default_location, "w") as f:
533
+ for assignee in default_assignees:
534
+ f.write(f"{assignee}\n")
535
+
536
+ print(f"Created default assignee file at: {default_location}")
537
+ return default_assignees
538
+ except Exception as e:
539
+ print(f"Error creating default assignee file: {str(e)}")
540
+ # Return default list if all else fails
541
+ return ["Team Member 1", "Team Member 2", "Team Member 3"]
542
 
543
  def parse_assignee_file(file_path):
544
  """Parse the assignee file and extract names"""
 
566
  print(f"Found {len(assignees)} assignees: {assignees}")
567
 
568
  if not assignees:
569
+ st.warning(f"No valid assignee names found in {file_path}. Using default assignees.")
570
+ return ["Team Member 1", "Team Member 2", "Team Member 3"]
571
 
572
  return assignees
573
  except Exception as e:
574
  st.error(f"Error reading assignee file: {str(e)}")
575
  print(f"Error reading {file_path}: {str(e)}")
576
+ return ["Team Member 1", "Team Member 2", "Team Member 3"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
 
578
  # Generate a new task ID
579
  def generate_task_id():
 
595
  """Update a task field and save changes"""
596
  st.session_state.tasks.at[task_idx, field] = value
597
  save_tasks()
598
+ return True
599
 
600
  def delete_task(task_id):
601
  """Delete a task by ID"""
 
620
  if 'using_memory_storage' not in st.session_state:
621
  st.session_state.using_memory_storage = False
622
 
623
+ # Show storage location information
624
+ if TASKS_FILE:
625
+ print(f"Using task storage location: {TASKS_FILE}")
626
+ else:
627
+ print("No writable location found. Using in-memory storage only.")
 
628
  st.session_state.using_memory_storage = True
629
 
630
+ # Load assignees
631
  assignee_list = load_assignees()
632
 
633
+ # Display storage status notification if using memory storage
634
  if st.session_state.using_memory_storage:
635
+ st.markdown('<div style="background-color: #fff3cd; color: #856404; padding: 8px; border-radius: 4px; font-size: 12px; margin-bottom: 10px;">Using in-memory storage. Tasks saved but may not persist after app restart.</div>', unsafe_allow_html=True)
636
+ elif TASKS_FILE:
637
+ st.markdown(f'<div style="background-color: #d4edda; color: #155724; padding: 8px; border-radius: 4px; font-size: 12px; margin-bottom: 10px;">Tasks saved to disk at: {TASKS_FILE}</div>', unsafe_allow_html=True)
638
 
639
  # Sidebar for new tasks
640
  with st.sidebar:
 
677
  st.session_state.tasks,
678
  pd.DataFrame([new_task])
679
  ], ignore_index=True)
680
+ save_result = save_tasks()
681
+ # Show success message
682
+ st.success("Task added successfully!")
683
+ # Clear form after adding
684
+ st.experimental_rerun()
685
  except Exception as e:
686
  st.error(f"Error adding task: {str(e)}")
 
 
 
687
  else:
688
  st.warning("Please enter a task title")
689
+ # Create hidden buttons for handling status updates from the cards
690
+ for idx, task in st.session_state.tasks.iterrows():
691
+ task_id = task['ID'] if 'ID' in task and pd.notna(task['ID']) else f"#{idx+1}"
692
+ task_id_clean = str(task_id).replace('#', '')
693
+
694
+ for status in ["Backlog", "To Do", "In Progress", "Done"]:
695
+ status_key = status.replace(' ', '-')
696
+ button_key = f"update-status-{task_id_clean}-{status_key}"
697
+
698
+ # Create a hidden button that will be triggered by JavaScript
699
+ if st.button("", key=button_key, help="Hidden button for status update", disabled=task['Status'] == status):
700
+ update_task(idx, 'Status', status)
701
+ st.experimental_rerun()
702
 
703
+ # Add JavaScript for modal and status update functionality
704
+ st.markdown("""
705
+ <script>
706
+ // Function to open task details modal
707
+ function openTaskDetails(taskId) {
708
+ console.log('Opening task details for: ' + taskId);
709
+ const modal = document.getElementById(`modal-${taskId}`);
710
+ if (modal) {
711
+ modal.style.display = 'block';
712
+ // Move status widget into modal
713
+ const statusEl = document.getElementById(`status-update-${taskId}`);
714
+ const targetEl = document.getElementById(`task-status-${taskId}`);
715
+ if (statusEl && targetEl) {
716
+ statusEl.classList.remove('status-hidden');
717
+ targetEl.appendChild(statusEl);
718
+ }
719
+ }
720
+ }
721
+
722
+ // Function to close task modal
723
+ function closeTaskModal(taskId) {
724
+ const modal = document.getElementById(`modal-${taskId}`);
725
+ if (modal) {
726
+ modal.style.display = 'none';
727
+ }
728
+ }
729
+
730
+ // Close modal when clicking outside
731
+ document.addEventListener('click', function(event) {
732
+ if (event.target.classList.contains('modal-overlay')) {
733
+ event.target.style.display = 'none';
734
+ }
735
+ });
736
+
737
+ // Function to update task status from dropdown
738
+ function updateTaskStatus(taskId, newStatus) {
739
+ console.log(`Updating task ${taskId} to status: ${newStatus}`);
740
+
741
+ // Clean the task ID for use in the button ID
742
+ const taskIdClean = taskId.replace('#', '');
743
+
744
+ // Format the status for the button ID
745
+ const statusKey = newStatus.replace(' ', '-');
746
+
747
+ // Construct button ID based on task and status
748
+ const buttonId = `update-status-${taskIdClean}-${statusKey}`;
749
+
750
+ // Find and click the corresponding hidden button
751
+ const button = document.querySelector(`button[data-testid="${buttonId}"]`);
752
+ if (button) {
753
+ button.click();
754
+ } else {
755
+ console.error(`Button not found: ${buttonId}`);
756
+ }
757
+ }
758
+
759
+ // Setup task cards and modals after DOM is fully loaded
760
+ function setupTaskInteractions() {
761
+ console.log('Setting up task interactions');
762
+
763
+ // Make sure task cards are clickable
764
+ document.querySelectorAll('.task-card .task-title').forEach(title => {
765
+ title.addEventListener('click', function() {
766
+ const taskId = this.closest('.task-card').getAttribute('data-task-id');
767
+ openTaskDetails(taskId);
768
+ });
769
+ });
770
+ }
771
+
772
+ // Run setup on page load
773
+ document.addEventListener('DOMContentLoaded', function() {
774
+ console.log('DOM loaded, setting up task board');
775
+ setTimeout(setupTaskInteractions, 500);
776
+ });
777
+
778
+ // Also run after Streamlit reruns
779
+ const observer = new MutationObserver(function(mutations) {
780
+ for (const mutation of mutations) {
781
+ if (mutation.type === 'childList' && mutation.addedNodes.length) {
782
+ setTimeout(setupTaskInteractions, 200);
783
+ break;
784
+ }
785
+ }
786
+ });
787
+
788
+ // Start observing the document body for DOM changes
789
+ document.addEventListener('DOMContentLoaded', function() {
790
+ observer.observe(document.body, { childList: true, subtree: true });
791
+ });
792
+ </script>
793
+ """, unsafe_allow_html=True)# Create task modals for each task with more compact display
794
+ for idx, task in st.session_state.tasks.iterrows():
795
+ task_id = task['ID'] if 'ID' in task and pd.notna(task['ID']) else f"#{idx+1}"
796
+ status_class = STATUS_COLORS.get(task['Status'], "backlog")
797
+
798
+ # Format dates for display in modal - more compact format
799
+ date_started_str = task['Date Started'].strftime('%m/%d/%Y') if 'Date Started' in task and pd.notna(task['Date Started']) else "Not set"
800
+ 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"
801
+ due_date_str = task['Due Date'].strftime('%m/%d/%Y') if 'Due Date' in task and pd.notna(task['Due Date']) else "Not set"
802
+
803
+ # Create modal HTML with improved layout
804
+ modal_html = f"""
805
+ <div id="modal-{task_id}" class="modal-overlay">
806
+ <div class="modal-content">
807
+ <div class="modal-header">
808
+ <div class="modal-title">{task['Title']} <span class="status-pill {status_class}">{task['Status']}</span></div>
809
+ <span class="modal-close" onclick="closeTaskModal('{task_id}');">&times;</span>
810
+ </div>
811
+ <div class="modal-body">
812
+ <div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
813
+ <div><strong>Task ID:</strong> {task_id}</div>
814
+ <div><strong>Assignee:</strong> {task['Assignee']}</div>
815
+ </div>
816
+
817
+ <div style="margin-bottom: 15px;">
818
+ <strong>Description:</strong><br>
819
+ <div style="background: #f9f9f9; padding: 8px; border-radius: 4px; margin-top: 5px;">
820
+ {task['Description'] if task['Description'] else "No description provided."}
821
+ </div>
822
+ </div>
823
+
824
+ <div style="display: flex; justify-content: space-between; margin-bottom: 15px; font-size: 13px;">
825
+ <div><strong>Date Started:</strong><br>{date_started_str}</div>
826
+ <div><strong>Date to Finish:</strong><br>{date_to_finish_str}</div>
827
+ <div><strong>Due Date:</strong><br>{due_date_str}</div>
828
+ </div>
829
+
830
+ <div id="task-status-{task_id}" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee;"></div>
831
+ </div>
832
+ </div>
833
+ </div>
834
+ """
835
+
836
+ # Render the modal
837
+ st.markdown(modal_html, unsafe_allow_html=True)
838
+
839
+ # Add task status update widget inside the task modal - improved layout
840
+ status_placeholder = st.empty()
841
+ with status_placeholder:
842
+ # Hide this with CSS and use JavaScript to move it into the modal
843
+ st.markdown(f"<div id='status-update-{task_id}' class='status-hidden'>", unsafe_allow_html=True)
844
+
845
+ # Create two columns for the status update controls
846
+ col1, col2 = st.columns([3, 1])
847
+
848
+ with col1:
849
+ new_status = st.selectbox(
850
+ f"Update Status",
851
+ ["Backlog", "To Do", "In Progress", "Done"],
852
+ index=["Backlog", "To Do", "In Progress", "Done"].index(task['Status']) if task['Status'] in ["Backlog", "To Do", "In Progress", "Done"] else 0,
853
+ key=f"modal_status_{task_id}"
854
+ )
855
+
856
+ with col2:
857
+ if st.button("Update", key=f"update_btn_{task_id}"):
858
+ try:
859
+ update_task(idx, 'Status', new_status)
860
+ st.success("Updated!")
861
+ # Use JavaScript to close the modal and refresh
862
+ st.markdown(f"""
863
+ <script>
864
+ setTimeout(function() {{
865
+ document.getElementById('modal-{task_id}').style.display = 'none';
866
+ window.location.reload();
867
+ }}, 500);
868
+ </script>
869
+ """, unsafe_allow_html=True)
870
+ except Exception as e:
871
+ st.error(f"Update failed: {str(e)}")
872
+
873
+ # Delete button with confirmation
874
+ if st.button("Delete Task", key=f"delete_btn_{task_id}"):
875
+ confirm = st.checkbox("Confirm deletion?", key=f"confirm_delete_{task_id}")
876
+ if confirm:
877
+ try:
878
+ if delete_task(task_id):
879
+ st.success(f"Task {task_id} deleted!")
880
+ # Use JavaScript to close the modal and refresh
881
+ st.markdown(f"""
882
+ <script>
883
+ setTimeout(function() {{
884
+ document.getElementById('modal-{task_id}').style.display = 'none';
885
+ window.location.reload();
886
+ }}, 500);
887
+ </script>
888
+ """, unsafe_allow_html=True)
889
+ else:
890
+ st.error(f"Failed to delete task {task_id}")
891
+ except Exception as e:
892
+ st.error(f"Delete failed: {str(e)}")
893
+
894
+ st.markdown("</div>", unsafe_allow_html=True)# Display tasks in a horizontal grid layout
895
  if st.session_state.tasks.empty:
896
  st.info("No tasks yet. Add your first task using the sidebar.")
897
  else:
 
927
  date_started_str = task['Date Started'].strftime('%m/%d') if 'Date Started' in task and pd.notna(task['Date Started']) else "Not set"
928
  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"
929
 
930
+ # Create the card HTML with inline status dropdown
931
  card_html = f"""
932
+ <div class="task-card" data-task-id="{task_id}">
933
  <div class="card-header {status_class}">
934
  <span class="task-id">{task_id}</span>
935
+ <div class="task-status-dropdown">
936
+ <button class="status-dropdown-btn">⋮</button>
937
+ <div class="status-dropdown-content">
938
+ <a href="#" class="status-option-backlog" onclick="updateTaskStatus('{task_id}', 'Backlog'); return false;">Backlog</a>
939
+ <a href="#" class="status-option-todo" onclick="updateTaskStatus('{task_id}', 'To Do'); return false;">To Do</a>
940
+ <a href="#" class="status-option-progress" onclick="updateTaskStatus('{task_id}', 'In Progress'); return false;">In Progress</a>
941
+ <a href="#" class="status-option-done" onclick="updateTaskStatus('{task_id}', 'Done'); return false;">Done</a>
942
+ <a href="#" onclick="openTaskDetails('{task_id}'); return false;">View Details</a>
943
+ </div>
944
+ </div>
945
  </div>
946
+ <div class="task-title" onclick="openTaskDetails('{task_id}');">{task['Title']}</div>
947
  <div class="task-assignee">{task['Assignee']}</div>
948
  <div class="task-dates">
949
  <span class="date-display">Start: {date_started_str}</span>
 
959
  st.markdown('</div>', unsafe_allow_html=True)
960
 
961
  # Close container
962
+ st.markdown('</div>', unsafe_allow_html=True)import streamlit as st