Files
nimbusflow/backend/cli/interactive.py

381 lines
17 KiB
Python

"""
Interactive CLI interface for NimbusFlow.
"""
import sys
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .base import NimbusFlowCLI
from .commands import (
cmd_members_list, cmd_members_show,
cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept, cmd_schedules_decline, cmd_schedules_schedule,
cmd_services_list,
)
# ANSI color codes
class Colors:
RESET = '\033[0m'
BOLD = '\033[1m'
DIM = '\033[2m'
BLUE = '\033[94m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
CYAN = '\033[96m'
WHITE = '\033[97m'
GREY = '\033[90m'
BG_GREY = '\033[100m'
# Special combinations
HEADER = '\033[1m\033[96m' # Bold Cyan
SUCCESS = '\033[1m\033[92m' # Bold Green
ERROR = '\033[1m\033[91m' # Bold Red
WARNING = '\033[1m\033[93m' # Bold Yellow
INPUT_BOX = '\033[90m' # Grey
def create_input_box(prompt: str, width: int = 60) -> str:
"""Create a grey box around input prompt."""
box_top = f"{Colors.INPUT_BOX}" + "" * (width - 2) + f"{Colors.RESET}"
prompt_line = f"{Colors.INPUT_BOX}{Colors.RESET} {prompt}"
padding = width - len(prompt) - 3
if padding > 0:
prompt_line += " " * padding + f"{Colors.INPUT_BOX}{Colors.RESET}"
else:
prompt_line += f" {Colors.INPUT_BOX}{Colors.RESET}"
box_bottom = f"{Colors.INPUT_BOX}" + "" * (width - 2) + f"{Colors.RESET}"
return f"\n{box_top}\n{prompt_line}\n{box_bottom}"
def create_simple_input_box(prompt: str) -> str:
"""Create a simple grey box around input prompt."""
return f"\n{Colors.INPUT_BOX}┌─ {prompt} ─┐{Colors.RESET}"
def clear_screen():
"""Clear the terminal screen."""
print("\033[2J\033[H")
def display_welcome():
"""Display welcome screen."""
print("\033[2J\033[H") # Clear screen and move cursor to top
# NimbusFlow branding
welcome_text = """
╔════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ ███╗ ██╗██╗███╗ ███╗██████╗ ██╗ ██╗███████╗ ███████╗██╗ ██████╗ ██╗ ██╗ ║
║ ████╗ ██║██║████╗ ████║██╔══██╗██║ ██║██╔════╝ ██╔════╝██║ ██╔═══██╗██║ ██║ ║
║ ██╔██╗ ██║██║██╔████╔██║██████╔╝██║ ██║███████╗ █████╗ ██║ ██║ ██║██║ █╗ ██║ ║
║ ██║╚██╗██║██║██║╚██╔╝██║██╔══██╗██║ ██║╚════██║ ██╔══╝ ██║ ██║ ██║██║███╗██║ ║
║ ██║ ╚████║██║██║ ╚═╝ ██║██████╔╝╚██████╔╝███████║ ██║ ███████╗╚██████╔╝╚███╔███╔╝ ║
║ ╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝ ║
║ ║
║ 🎵 Scheduling System 🎵 ║
╚════════════════════════════════════════════════════════════════════════════════════════════╝
"""
print(welcome_text)
print()
def display_main_menu():
"""Display the main menu options."""
print(f"{Colors.HEADER}Main Menu{Colors.RESET}")
print(f"{Colors.GREY}" * 50 + f"{Colors.RESET}")
print()
print(f" {Colors.CYAN}1.{Colors.RESET} {Colors.BOLD}Members{Colors.RESET} Manage choir members")
print(f" {Colors.CYAN}2.{Colors.RESET} {Colors.BOLD}Schedules{Colors.RESET} View and manage schedules")
print(f" {Colors.CYAN}3.{Colors.RESET} {Colors.BOLD}Services{Colors.RESET} Manage services and events")
print(f" {Colors.CYAN}4.{Colors.RESET} {Colors.BOLD}Exit{Colors.RESET} Close NimbusFlow CLI")
print()
def display_members_menu():
"""Display members submenu."""
print(f"\n{Colors.HEADER}Members{Colors.RESET}")
print(f"{Colors.GREY}" * 50 + f"{Colors.RESET}")
print()
print(f" {Colors.CYAN}1.{Colors.RESET} List all members")
print(f" {Colors.CYAN}2.{Colors.RESET} List active members only")
print(f" {Colors.CYAN}3.{Colors.RESET} List by classification")
print(f" {Colors.CYAN}4.{Colors.RESET} Show member details")
print(f" {Colors.CYAN}5.{Colors.RESET} {Colors.DIM}Back to main menu{Colors.RESET}")
print()
def display_schedules_menu():
"""Display schedules submenu."""
print(f"\n{Colors.HEADER}Schedules{Colors.RESET}")
print(f"{Colors.GREY}" * 50 + f"{Colors.RESET}")
print()
print(f" {Colors.CYAN}1.{Colors.RESET} List all schedules")
print(f" {Colors.CYAN}2.{Colors.RESET} List pending schedules")
print(f" {Colors.CYAN}3.{Colors.RESET} List accepted schedules")
print(f" {Colors.CYAN}4.{Colors.RESET} List declined schedules")
print(f" {Colors.CYAN}5.{Colors.RESET} Show schedule details")
print(f" {Colors.CYAN}6.{Colors.RESET} {Colors.GREEN}Accept a schedule{Colors.RESET}")
print(f" {Colors.CYAN}7.{Colors.RESET} {Colors.RED}Decline a schedule{Colors.RESET}")
print(f" {Colors.CYAN}8.{Colors.RESET} {Colors.YELLOW}Schedule next member for service{Colors.RESET}")
print(f" {Colors.CYAN}9.{Colors.RESET} {Colors.DIM}Back to main menu{Colors.RESET}")
print()
def display_services_menu():
"""Display services submenu."""
print(f"\n{Colors.HEADER}Services{Colors.RESET}")
print(f"{Colors.GREY}" * 50 + f"{Colors.RESET}")
print()
print(f" {Colors.CYAN}1.{Colors.RESET} List all services")
print(f" {Colors.CYAN}2.{Colors.RESET} List upcoming services")
print(f" {Colors.CYAN}3.{Colors.RESET} List services by date")
print(f" {Colors.CYAN}4.{Colors.RESET} {Colors.DIM}Back to main menu{Colors.RESET}")
print()
def get_user_choice(max_options: int) -> int:
"""Get user menu choice with validation."""
while True:
try:
print(create_simple_input_box(f"Enter your choice (1-{max_options})"))
choice = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip()
if not choice:
continue
choice_int = int(choice)
if 1 <= choice_int <= max_options:
return choice_int
else:
print(f"{Colors.ERROR}Please enter a number between 1 and {max_options}{Colors.RESET}")
except ValueError:
print(f"{Colors.ERROR}Please enter a valid number{Colors.RESET}")
except (KeyboardInterrupt, EOFError):
print(f"\n{Colors.WARNING}Goodbye!{Colors.RESET}")
sys.exit(0)
def get_text_input(prompt: str, required: bool = True) -> str:
"""Get text input from user."""
while True:
try:
print(create_simple_input_box(prompt))
value = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip()
if value or not required:
return value
print(f"{Colors.ERROR}This field is required{Colors.RESET}")
except (KeyboardInterrupt, EOFError):
print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}")
return ""
def get_date_input(prompt: str = "Enter date (YYYY-MM-DD)") -> str:
"""Get date input from user."""
while True:
try:
print(create_simple_input_box(prompt))
date_str = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip()
if not date_str:
print(f"{Colors.ERROR}Date is required{Colors.RESET}")
continue
# Basic date format validation
if len(date_str) == 10 and date_str.count('-') == 2:
parts = date_str.split('-')
if len(parts[0]) == 4 and len(parts[1]) == 2 and len(parts[2]) == 2:
return date_str
print(f"{Colors.ERROR}Please use format YYYY-MM-DD (e.g., 2025-09-07){Colors.RESET}")
except (KeyboardInterrupt, EOFError):
print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}")
return ""
class MockArgs:
"""Mock args object for interactive commands."""
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def handle_members_menu(cli: "NimbusFlowCLI"):
"""Handle members menu interactions."""
while True:
clear_screen()
display_members_menu()
choice = get_user_choice(5)
if choice == 1: # List all members
clear_screen()
print(f"{Colors.SUCCESS}Listing all members...{Colors.RESET}\n")
cmd_members_list(cli, MockArgs(active=False, classification=None))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 2: # List active members
clear_screen()
print(f"{Colors.SUCCESS}Listing active members...{Colors.RESET}\n")
cmd_members_list(cli, MockArgs(active=True, classification=None))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 3: # List by classification
clear_screen()
classification = get_text_input("Enter classification (Soprano, Alto / Mezzo, Tenor, Baritone)", True)
if classification:
clear_screen()
print(f"{Colors.SUCCESS}Listing {classification} members...{Colors.RESET}\n")
cmd_members_list(cli, MockArgs(active=False, classification=classification))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 4: # Show member details
clear_screen()
member_id = get_text_input("Enter member ID", True)
if member_id.isdigit():
clear_screen()
print(f"{Colors.SUCCESS}Showing details for member {member_id}...{Colors.RESET}\n")
cmd_members_show(cli, MockArgs(member_id=int(member_id)))
else:
print(f"{Colors.ERROR}Invalid member ID{Colors.RESET}")
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 5: # Back to main menu
break
def handle_schedules_menu(cli: "NimbusFlowCLI"):
"""Handle schedules menu interactions."""
while True:
clear_screen()
display_schedules_menu()
choice = get_user_choice(9)
if choice == 1: # List all schedules
clear_screen()
print(f"{Colors.SUCCESS}Listing all schedules...{Colors.RESET}\n")
cmd_schedules_list(cli, MockArgs(service_id=None, status=None))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 2: # List pending schedules
clear_screen()
print(f"{Colors.WARNING}Listing pending schedules...{Colors.RESET}\n")
cmd_schedules_list(cli, MockArgs(service_id=None, status="pending"))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 3: # List accepted schedules
clear_screen()
print(f"{Colors.SUCCESS}Listing accepted schedules...{Colors.RESET}\n")
cmd_schedules_list(cli, MockArgs(service_id=None, status="accepted"))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 4: # List declined schedules
clear_screen()
print(f"{Colors.ERROR}Listing declined schedules...{Colors.RESET}\n")
cmd_schedules_list(cli, MockArgs(service_id=None, status="declined"))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 5: # Show schedule details
clear_screen()
schedule_id = get_text_input("Enter schedule ID", True)
if schedule_id.isdigit():
clear_screen()
print(f"{Colors.SUCCESS}Showing details for schedule {schedule_id}...{Colors.RESET}\n")
cmd_schedules_show(cli, MockArgs(schedule_id=int(schedule_id)))
else:
print(f"{Colors.ERROR}Invalid schedule ID{Colors.RESET}")
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 6: # Accept schedule
clear_screen()
date = get_date_input("Enter date for interactive accept")
if date:
clear_screen()
print(f"{Colors.SUCCESS}Starting interactive accept for {date}...{Colors.RESET}\n")
cmd_schedules_accept(cli, MockArgs(date=date, schedule_id=None))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 7: # Decline schedule
clear_screen()
date = get_date_input("Enter date for interactive decline")
if date:
clear_screen()
reason = get_text_input("Enter decline reason (optional)", False)
clear_screen()
print(f"{Colors.ERROR}Starting interactive decline for {date}...{Colors.RESET}\n")
cmd_schedules_decline(cli, MockArgs(date=date, schedule_id=None, reason=reason))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 8: # Schedule next member
clear_screen()
date = get_date_input("Enter date to schedule for")
if date:
clear_screen()
print(f"{Colors.WARNING}Starting scheduling for {date}...{Colors.RESET}\n")
cmd_schedules_schedule(cli, MockArgs(service_id=None, date=date, classifications=None))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 9: # Back to main menu
break
def handle_services_menu(cli: "NimbusFlowCLI"):
"""Handle services menu interactions."""
while True:
clear_screen()
display_services_menu()
choice = get_user_choice(4)
if choice == 1: # List all services
clear_screen()
print(f"{Colors.SUCCESS}Listing all services...{Colors.RESET}\n")
cmd_services_list(cli, MockArgs(date=None, upcoming=False, limit=None))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 2: # List upcoming services
clear_screen()
print(f"{Colors.WARNING}Listing upcoming services...{Colors.RESET}\n")
cmd_services_list(cli, MockArgs(date=None, upcoming=True, limit=20))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 3: # List by date
clear_screen()
date = get_date_input("Enter date to filter services")
if date:
clear_screen()
print(f"{Colors.SUCCESS}Listing services for {date}...{Colors.RESET}\n")
cmd_services_list(cli, MockArgs(date=date, upcoming=False, limit=None))
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
elif choice == 4: # Back to main menu
break
def run_interactive_mode(cli: "NimbusFlowCLI"):
"""Run the main interactive CLI mode."""
display_welcome()
print(f"{Colors.HEADER}Welcome to the NimbusFlow Interactive CLI{Colors.RESET}")
print(f"{Colors.DIM}Navigate through menus to manage your choir scheduling system.{Colors.RESET}")
print()
input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")
while True:
clear_screen() # Clear screen
display_main_menu()
choice = get_user_choice(4)
if choice == 1: # Members
handle_members_menu(cli)
elif choice == 2: # Schedules
handle_schedules_menu(cli)
elif choice == 3: # Services
handle_services_menu(cli)
elif choice == 4: # Exit
clear_screen()
print(f"\n{Colors.SUCCESS}Thank you for using NimbusFlow!{Colors.RESET}")
print(f"{Colors.DIM}Goodbye!{Colors.RESET}")
break