diff --git a/backend/cli/commands/members.py b/backend/cli/commands/members.py index b7b2b5d..de280c7 100644 --- a/backend/cli/commands/members.py +++ b/backend/cli/commands/members.py @@ -8,13 +8,11 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from backend.cli.base import NimbusFlowCLI -from backend.cli.utils import format_member_row +from backend.cli.utils import format_member_row, create_table_header, create_table_separator, TableColors def cmd_members_list(cli: "NimbusFlowCLI", args) -> None: """List all members with optional filters.""" - print("Listing members...") - # Get all classifications for lookup classifications = cli.classification_repo.list_all() classification_map = {c.ClassificationId: c.ClassificationName for c in classifications} @@ -29,7 +27,7 @@ def cmd_members_list(cli: "NimbusFlowCLI", args) -> None: break if classification_id is None: - print(f"āŒ Classification '{args.classification}' not found") + print(f"{TableColors.ERROR}Classification '{args.classification}' not found{TableColors.RESET}") return members = cli.member_repo.get_by_classification_ids([classification_id]) @@ -39,26 +37,28 @@ def cmd_members_list(cli: "NimbusFlowCLI", args) -> None: members = cli.member_repo.list_all() if not members: - print("No members found.") + print(f"{TableColors.DIM}No members found.{TableColors.RESET}") return - # Print header - print(f"\n{'ID':<3} | {'First Name':<12} | {'Last Name':<15} | {'Classification':<12} | {'Active':<6} | {'Email'}") - print("-" * 80) + # Print styled header + print() + print(create_table_header("ID ", "First Name ", "Last Name ", "Classification ", "Status ", "Email")) + print(create_table_separator(90)) # Print members for member in members: classification_name = classification_map.get(member.ClassificationId) print(format_member_row(member, classification_name)) - print(f"\nTotal: {len(members)} members") + print() + print(f"{TableColors.SUCCESS}Total: {len(members)} members{TableColors.RESET}") def cmd_members_show(cli: "NimbusFlowCLI", args) -> None: """Show detailed information about a specific member.""" member = cli.member_repo.get_by_id(args.member_id) if not member: - print(f"āŒ Member with ID {args.member_id} not found") + print(f"{TableColors.ERROR}Member with ID {args.member_id} not found{TableColors.RESET}") return # Get classification name @@ -66,20 +66,24 @@ def cmd_members_show(cli: "NimbusFlowCLI", args) -> None: if member.ClassificationId: classification = cli.classification_repo.get_by_id(member.ClassificationId) - print(f"\nšŸ“‹ Member Details (ID: {member.MemberId})") - print("-" * 50) - print(f"Name: {member.FirstName} {member.LastName}") - print(f"Email: {member.Email or 'N/A'}") - print(f"Phone: {member.PhoneNumber or 'N/A'}") - print(f"Classification: {classification.ClassificationName if classification else 'N/A'}") - print(f"Active: {'Yes' if member.IsActive else 'No'}") - print(f"Notes: {member.Notes or 'N/A'}") + print(f"\n{TableColors.HEADER}Member Details (ID: {member.MemberId}){TableColors.RESET}") + print(f"{TableColors.BORDER}{'─' * 50}{TableColors.RESET}") + print(f"{TableColors.BOLD}Name:{TableColors.RESET} {member.FirstName} {member.LastName}") + print(f"{TableColors.BOLD}Email:{TableColors.RESET} {member.Email or f'{TableColors.DIM}N/A{TableColors.RESET}'}") + print(f"{TableColors.BOLD}Phone:{TableColors.RESET} {member.PhoneNumber or f'{TableColors.DIM}N/A{TableColors.RESET}'}") + print(f"{TableColors.BOLD}Classification:{TableColors.RESET} {TableColors.YELLOW}{classification.ClassificationName if classification else f'{TableColors.DIM}N/A'}{TableColors.RESET}") - print(f"\nā° Schedule History:") - print(f"Last Scheduled: {member.LastScheduledAt or 'Never'}") - print(f"Last Accepted: {member.LastAcceptedAt or 'Never'}") - print(f"Last Declined: {member.LastDeclinedAt or 'Never'}") - print(f"Decline Streak: {member.DeclineStreak}") + active_status = f"{TableColors.SUCCESS}Yes{TableColors.RESET}" if member.IsActive else f"{TableColors.ERROR}No{TableColors.RESET}" + print(f"{TableColors.BOLD}Active:{TableColors.RESET} {active_status}") + print(f"{TableColors.BOLD}Notes:{TableColors.RESET} {member.Notes or f'{TableColors.DIM}N/A{TableColors.RESET}'}") + + print(f"\n{TableColors.HEADER}Schedule History:{TableColors.RESET}") + print(f"{TableColors.BOLD}Last Scheduled:{TableColors.RESET} {member.LastScheduledAt or f'{TableColors.DIM}Never{TableColors.RESET}'}") + print(f"{TableColors.BOLD}Last Accepted:{TableColors.RESET} {member.LastAcceptedAt or f'{TableColors.DIM}Never{TableColors.RESET}'}") + print(f"{TableColors.BOLD}Last Declined:{TableColors.RESET} {member.LastDeclinedAt or f'{TableColors.DIM}Never{TableColors.RESET}'}") + + decline_color = TableColors.ERROR if member.DeclineStreak > 0 else TableColors.SUCCESS + print(f"{TableColors.BOLD}Decline Streak:{TableColors.RESET} {decline_color}{member.DeclineStreak}{TableColors.RESET}") def setup_members_parser(subparsers) -> None: diff --git a/backend/cli/commands/schedules.py b/backend/cli/commands/schedules.py index 5629892..6778dcc 100644 --- a/backend/cli/commands/schedules.py +++ b/backend/cli/commands/schedules.py @@ -10,13 +10,11 @@ if TYPE_CHECKING: from backend.cli.base import NimbusFlowCLI from backend.models.enums import ScheduleStatus -from backend.cli.utils import format_schedule_row +from backend.cli.utils import format_schedule_row, create_table_header, create_table_separator, TableColors def cmd_schedules_list(cli: "NimbusFlowCLI", args) -> None: """List schedules with optional filters.""" - print("Listing schedules...") - schedules = cli.schedule_repo.list_all() # Apply filters @@ -28,11 +26,11 @@ def cmd_schedules_list(cli: "NimbusFlowCLI", args) -> None: status_enum = ScheduleStatus.from_raw(args.status.lower()) schedules = [s for s in schedules if s.Status == status_enum.value] except ValueError: - print(f"āŒ Invalid status '{args.status}'. Valid options: pending, accepted, declined") + print(f"{TableColors.ERROR}Invalid status '{args.status}'. Valid options: pending, accepted, declined{TableColors.RESET}") return if not schedules: - print("No schedules found.") + print(f"{TableColors.DIM}No schedules found.{TableColors.RESET}") return # Get member and service info for display @@ -44,9 +42,10 @@ def cmd_schedules_list(cli: "NimbusFlowCLI", args) -> None: type_name = service_type_map.get(service.ServiceTypeId, "Unknown") service_map[service.ServiceId] = f"{type_name} {service.ServiceDate}" - # Print header - print(f"\n{'ID':<3} | {'Status':<10} | {'Member':<20} | {'Service':<15} | {'Scheduled'}") - print("-" * 80) + # Print styled header + print() + print(create_table_header("ID ", "Status ", "Member ", "Service ", "Scheduled")) + print(create_table_separator(95)) # Print schedules for schedule in schedules: @@ -54,14 +53,15 @@ def cmd_schedules_list(cli: "NimbusFlowCLI", args) -> None: service_info = service_map.get(schedule.ServiceId, f"ID:{schedule.ServiceId}") print(format_schedule_row(schedule, member_name, service_info)) - print(f"\nTotal: {len(schedules)} schedules") + print() + print(f"{TableColors.SUCCESS}Total: {len(schedules)} schedules{TableColors.RESET}") def cmd_schedules_show(cli: "NimbusFlowCLI", args) -> None: """Show detailed information about a specific schedule.""" schedule = cli.schedule_repo.get_by_id(args.schedule_id) if not schedule: - print(f"āŒ Schedule with ID {args.schedule_id} not found") + print(f"{TableColors.ERROR}Schedule with ID {args.schedule_id} not found{TableColors.RESET}") return # Get related information @@ -71,17 +71,39 @@ def cmd_schedules_show(cli: "NimbusFlowCLI", args) -> None: if service: service_type = cli.service_type_repo.get_by_id(service.ServiceTypeId) - print(f"\nšŸ“… Schedule Details (ID: {schedule.ScheduleId})") - print("-" * 50) - print(f"Member: {member.FirstName} {member.LastName} (ID: {member.MemberId})" if member else f"Member ID: {schedule.MemberId}") - print(f"Service: {service_type.TypeName if service_type else 'Unknown'} on {service.ServiceDate if service else 'Unknown'}") - print(f"Status: {schedule.Status.upper()}") - print(f"Scheduled At: {schedule.ScheduledAt}") - print(f"Accepted At: {schedule.AcceptedAt or 'N/A'}") - print(f"Declined At: {schedule.DeclinedAt or 'N/A'}") - print(f"Expires At: {schedule.ExpiresAt or 'N/A'}") + print(f"\n{TableColors.HEADER}Schedule Details (ID: {schedule.ScheduleId}){TableColors.RESET}") + print(f"{TableColors.BORDER}{'─' * 50}{TableColors.RESET}") + + # Member info with color + if member: + print(f"{TableColors.BOLD}Member:{TableColors.RESET} {TableColors.BOLD}{member.FirstName} {member.LastName}{TableColors.RESET} {TableColors.DIM}(ID: {member.MemberId}){TableColors.RESET}") + else: + print(f"{TableColors.BOLD}Member:{TableColors.RESET} {TableColors.DIM}Member ID: {schedule.MemberId}{TableColors.RESET}") + + # Service info with color + if service_type and service: + print(f"{TableColors.BOLD}Service:{TableColors.RESET} {TableColors.YELLOW}{service_type.TypeName}{TableColors.RESET} on {TableColors.BLUE}{service.ServiceDate}{TableColors.RESET}") + else: + print(f"{TableColors.BOLD}Service:{TableColors.RESET} {TableColors.DIM}Unknown{TableColors.RESET}") + + # Status with appropriate color + status_enum = ScheduleStatus.from_raw(schedule.Status) + if status_enum == ScheduleStatus.PENDING: + status_color = TableColors.WARNING + elif status_enum == ScheduleStatus.ACCEPTED: + status_color = TableColors.SUCCESS + elif status_enum == ScheduleStatus.DECLINED: + status_color = TableColors.ERROR + else: + status_color = TableColors.DIM + + print(f"{TableColors.BOLD}Status:{TableColors.RESET} {status_color}{schedule.Status.upper()}{TableColors.RESET}") + print(f"{TableColors.BOLD}Scheduled At:{TableColors.RESET} {schedule.ScheduledAt or f'{TableColors.DIM}N/A{TableColors.RESET}'}") + print(f"{TableColors.BOLD}Accepted At:{TableColors.RESET} {schedule.AcceptedAt or f'{TableColors.DIM}N/A{TableColors.RESET}'}") + print(f"{TableColors.BOLD}Declined At:{TableColors.RESET} {schedule.DeclinedAt or f'{TableColors.DIM}N/A{TableColors.RESET}'}") + print(f"{TableColors.BOLD}Expires At:{TableColors.RESET} {schedule.ExpiresAt or f'{TableColors.DIM}N/A{TableColors.RESET}'}") if schedule.DeclineReason: - print(f"Decline Reason: {schedule.DeclineReason}") + print(f"{TableColors.BOLD}Decline Reason:{TableColors.RESET} {TableColors.ERROR}{schedule.DeclineReason}{TableColors.RESET}") def cmd_schedules_accept(cli: "NimbusFlowCLI", args) -> None: diff --git a/backend/cli/commands/services.py b/backend/cli/commands/services.py index 3f41f1e..5dc0699 100644 --- a/backend/cli/commands/services.py +++ b/backend/cli/commands/services.py @@ -10,12 +10,11 @@ if TYPE_CHECKING: from backend.cli.base import NimbusFlowCLI from backend.models.enums import ScheduleStatus +from backend.cli.utils import format_service_row, create_table_header, create_table_separator, TableColors def cmd_services_list(cli: "NimbusFlowCLI", args) -> None: """List services with optional filters.""" - print("Listing services...") - # Apply filters if args.upcoming: services = cli.service_repo.upcoming(limit=args.limit or 50) @@ -26,7 +25,7 @@ def cmd_services_list(cli: "NimbusFlowCLI", args) -> None: all_services = cli.service_repo.list_all() services = [s for s in all_services if s.ServiceDate == target_date] except ValueError: - print(f"āŒ Invalid date format '{args.date}'. Use YYYY-MM-DD") + print(f"{TableColors.ERROR}Invalid date format '{args.date}'. Use YYYY-MM-DD{TableColors.RESET}") return else: services = cli.service_repo.list_all() @@ -34,7 +33,7 @@ def cmd_services_list(cli: "NimbusFlowCLI", args) -> None: services = services[:args.limit] if not services: - print("No services found.") + print(f"{TableColors.DIM}No services found.{TableColors.RESET}") return # Get service type names for display @@ -57,22 +56,19 @@ def cmd_services_list(cli: "NimbusFlowCLI", args) -> None: 'total': len(service_schedules) } - # Print header - print(f"\n{'ID':<3} | {'Date':<12} | {'Service Type':<12} | {'Total':<5} | {'Pending':<7} | {'Accepted':<8} | {'Declined'}") - print("-" * 85) + # Print styled header + print() + print(create_table_header("ID ", "Date ", "Service Type ", "Total", "Pending", "Accepted", "Declined")) + print(create_table_separator(85)) # Print services for service in sorted(services, key=lambda s: s.ServiceDate): type_name = service_type_map.get(service.ServiceTypeId, "Unknown") counts = schedule_counts.get(service.ServiceId, {'total': 0, 'pending': 0, 'accepted': 0, 'declined': 0}) - - # Format date properly - date_str = str(service.ServiceDate) if service.ServiceDate else "N/A" - - print(f"{service.ServiceId:<3} | {date_str:<12} | {type_name:<12} | " - f"{counts['total']:<5} | {counts['pending']:<7} | {counts['accepted']:<8} | {counts['declined']}") + print(format_service_row(service, type_name, counts)) - print(f"\nTotal: {len(services)} services") + print() + print(f"{TableColors.SUCCESS}Total: {len(services)} services{TableColors.RESET}") def setup_services_parser(subparsers) -> None: diff --git a/backend/cli/interactive.py b/backend/cli/interactive.py index 0b2c21e..0b9b362 100644 --- a/backend/cli/interactive.py +++ b/backend/cli/interactive.py @@ -15,46 +15,71 @@ from .commands import ( 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 load_ascii_art() -> str: - """Load ASCII art from file.""" - ascii_file = Path(__file__).parent / "ascii.txt" - if ascii_file.exists(): - with open(ascii_file, 'r', encoding='utf-8') as f: - content = f.read() - # Use only the cloud portion (middle section) for better display - lines = content.strip().split('\n') - if len(lines) > 30: - # Take a smaller portion of the cloud art - start = len(lines) // 3 - end = start + 15 - return '\n'.join(lines[start:end]) - return content - return "šŸŽµ NimbusFlow šŸŽµ" # Fallback if file doesn't exist def display_welcome(): - """Display welcome screen with ASCII art.""" + """Display welcome screen.""" print("\033[2J\033[H") # Clear screen and move cursor to top - # Display the cloud ASCII art - ascii_art = load_ascii_art() - print(ascii_art) - print() - - # Add the NimbusFlow branding + # NimbusFlow branding welcome_text = """ -╔═══════════════════════════════════════════════════════════════════════════════╗ -ā•‘ ā•‘ -ā•‘ ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā•— ā•‘ -ā•‘ ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā• ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā•‘ -ā•‘ ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā•— ā–ˆā–ˆā•‘ ā•‘ -ā•‘ ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā•‘ -ā•‘ ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā•šā•ā• ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā•šā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā•”ā• ā•‘ -ā•‘ ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•šā•ā•ā• ā•‘ -ā•‘ ā•‘ -ā•‘ šŸŽµ Choir Scheduling System šŸŽµ ā•‘ -ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• +╔════════════════════════════════════════════════════════════════════════════════════════════╗ +ā•‘ ā•‘ +ā•‘ ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā•—ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•— ā–ˆā–ˆā•— ā•‘ +ā•‘ ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā•ā•ā• ā–ˆā–ˆā•”ā•ā•ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā•‘ +ā•‘ ā–ˆā–ˆā•”ā–ˆā–ˆā•— ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā–ˆā–ˆā–ˆā•— ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā–ˆā•— ā–ˆā–ˆā•‘ ā•‘ +ā•‘ ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•—ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā•šā–ˆā–ˆā•”ā•ā–ˆā–ˆā•‘ā–ˆā–ˆā•”ā•ā•ā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā•šā•ā•ā•ā•ā–ˆā–ˆā•‘ ā–ˆā–ˆā•”ā•ā•ā• ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā•—ā–ˆā–ˆā•‘ ā•‘ +ā•‘ ā–ˆā–ˆā•‘ ā•šā–ˆā–ˆā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ā–ˆā–ˆā•‘ ā•šā•ā• ā–ˆā–ˆā•‘ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•‘ ā–ˆā–ˆā•‘ ā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•—ā•šā–ˆā–ˆā–ˆā–ˆā–ˆā–ˆā•”ā•ā•šā–ˆā–ˆā–ˆā•”ā–ˆā–ˆā–ˆā•”ā• ā•‘ +ā•‘ ā•šā•ā• ā•šā•ā•ā•ā•ā•šā•ā•ā•šā•ā• ā•šā•ā•ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā• ā•šā•ā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•ā•ā• ā•šā•ā•ā•ā•šā•ā•ā• ā•‘ +ā•‘ ā•‘ +ā•‘ šŸŽµ Scheduling System šŸŽµ ā•‘ +ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• """ print(welcome_text) print() @@ -62,59 +87,55 @@ def display_welcome(): def display_main_menu(): """Display the main menu options.""" - print("šŸŽÆ " + "="*60) - print(" MAIN MENU - What would you like to manage?") - print("šŸŽÆ " + "="*60) + print(f"{Colors.HEADER}Main Menu{Colors.RESET}") + print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}") print() - print(" 1ļøāƒ£ šŸ‘„ Members - Manage choir members") - print(" 2ļøāƒ£ šŸ“… Schedules - View and manage schedules") - print(" 3ļøāƒ£ šŸŽ¼ Services - Manage services and events") - print(" 4ļøāƒ£ āŒ Exit - Close NimbusFlow CLI") + 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("\nšŸ‘„ " + "="*50) - print(" MEMBERS MENU") - print("šŸ‘„ " + "="*50) + print(f"\n{Colors.HEADER}Members{Colors.RESET}") + print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}") print() - print(" 1ļøāƒ£ šŸ“‹ List all members") - print(" 2ļøāƒ£ āœ… List active members only") - print(" 3ļøāƒ£ šŸŽµ List by classification") - print(" 4ļøāƒ£ šŸ‘¤ Show member details") - print(" 5ļøāƒ£ šŸ”™ Back to main menu") + 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("\nšŸ“… " + "="*50) - print(" SCHEDULES MENU") - print("šŸ“… " + "="*50) + print(f"\n{Colors.HEADER}Schedules{Colors.RESET}") + print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}") print() - print(" 1ļøāƒ£ šŸ“‹ List all schedules") - print(" 2ļøāƒ£ ā³ List pending schedules") - print(" 3ļøāƒ£ āœ… List accepted schedules") - print(" 4ļøāƒ£ āŒ List declined schedules") - print(" 5ļøāƒ£ šŸ‘¤ Show schedule details") - print(" 6ļøāƒ£ ✨ Accept a schedule (interactive)") - print(" 7ļøāƒ£ 🚫 Decline a schedule (interactive)") - print(" 8ļøāƒ£ šŸ“… Schedule next member for service") - print(" 9ļøāƒ£ šŸ”™ Back to main menu") + 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("\nšŸŽ¼ " + "="*50) - print(" SERVICES MENU") - print("šŸŽ¼ " + "="*50) + print(f"\n{Colors.HEADER}Services{Colors.RESET}") + print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}") print() - print(" 1ļøāƒ£ šŸ“‹ List all services") - print(" 2ļøāƒ£ šŸ”® List upcoming services") - print(" 3ļøāƒ£ šŸ“… List services by date") - print(" 4ļøāƒ£ šŸ”™ Back to main menu") + 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() @@ -122,18 +143,19 @@ def get_user_choice(max_options: int) -> int: """Get user menu choice with validation.""" while True: try: - choice = input(f"šŸŽÆ Enter your choice (1-{max_options}): ").strip() + 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"āŒ Please enter a number between 1 and {max_options}") + print(f"{Colors.ERROR}Please enter a number between 1 and {max_options}{Colors.RESET}") except ValueError: - print("āŒ Please enter a valid number") + print(f"{Colors.ERROR}Please enter a valid number{Colors.RESET}") except (KeyboardInterrupt, EOFError): - print("\nšŸ›‘ Goodbye!") + print(f"\n{Colors.WARNING}Goodbye!{Colors.RESET}") sys.exit(0) @@ -141,12 +163,13 @@ def get_text_input(prompt: str, required: bool = True) -> str: """Get text input from user.""" while True: try: - value = input(f"šŸ“ {prompt}: ").strip() + print(create_simple_input_box(prompt)) + value = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip() if value or not required: return value - print("āŒ This field is required") + print(f"{Colors.ERROR}This field is required{Colors.RESET}") except (KeyboardInterrupt, EOFError): - print("\nšŸ›‘ Operation cancelled") + print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}") return "" @@ -154,18 +177,19 @@ def get_date_input(prompt: str = "Enter date (YYYY-MM-DD)") -> str: """Get date input from user.""" while True: try: - date_str = input(f"šŸ“… {prompt}: ").strip() + print(create_simple_input_box(prompt)) + date_str = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip() if not date_str: - print("āŒ Date is required") + 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("āŒ Please use format YYYY-MM-DD (e.g., 2025-09-07)") + print(f"{Colors.ERROR}Please use format YYYY-MM-DD (e.g., 2025-09-07){Colors.RESET}") except (KeyboardInterrupt, EOFError): - print("\nšŸ›‘ Operation cancelled") + print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}") return "" @@ -179,34 +203,41 @@ class MockArgs: 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 - print("\nšŸ” Listing all members...") + clear_screen() + print(f"{Colors.SUCCESS}Listing all members...{Colors.RESET}\n") cmd_members_list(cli, MockArgs(active=False, classification=None)) - input("\nāøļø Press Enter to continue...") + input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 2: # List active members - print("\nšŸ” Listing active members...") + clear_screen() + print(f"{Colors.SUCCESS}Listing active members...{Colors.RESET}\n") cmd_members_list(cli, MockArgs(active=True, classification=None)) - input("\nāøļø Press Enter to continue...") + 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: - print(f"\nšŸ” Listing {classification} members...") + clear_screen() + print(f"{Colors.SUCCESS}Listing {classification} members...{Colors.RESET}\n") cmd_members_list(cli, MockArgs(active=False, classification=classification)) - input("\nāøļø Press Enter to continue...") + 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(): - print(f"\nšŸ” Showing details for member {member_id}...") + 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("āŒ Invalid member ID") - input("\nāøļø Press Enter to continue...") + 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 @@ -215,59 +246,73 @@ def handle_members_menu(cli: "NimbusFlowCLI"): 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 - print("\nšŸ” Listing 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("\nāøļø Press Enter to continue...") + input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 2: # List pending schedules - print("\nšŸ” Listing 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("\nāøļø Press Enter to continue...") + input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 3: # List accepted schedules - print("\nšŸ” Listing 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("\nāøļø Press Enter to continue...") + input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 4: # List declined schedules - print("\nšŸ” Listing 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("\nāøļø Press Enter to continue...") + 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(): - print(f"\nšŸ” Showing details for schedule {schedule_id}...") + 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("āŒ Invalid schedule ID") - input("\nāøļø Press Enter to continue...") + 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: - print(f"\n✨ Starting interactive accept for {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("\nāøļø Press Enter to continue...") + 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) - print(f"\n🚫 Starting interactive decline for {date}...") + 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("\nāøļø Press Enter to continue...") + 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: - print(f"\nšŸ“… Starting scheduling for {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("\nāøļø Press Enter to continue...") + input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 9: # Back to main menu break @@ -276,25 +321,30 @@ def handle_schedules_menu(cli: "NimbusFlowCLI"): 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 - print("\nšŸ” Listing 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("\nāøļø Press Enter to continue...") + input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 2: # List upcoming services - print("\nšŸ” Listing 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("\nāøļø Press Enter to continue...") + 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: - print(f"\nšŸ” Listing services for {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("\nāøļø Press Enter to continue...") + input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}") elif choice == 4: # Back to main menu break @@ -304,13 +354,13 @@ def run_interactive_mode(cli: "NimbusFlowCLI"): """Run the main interactive CLI mode.""" display_welcome() - print("šŸŽ‰ Welcome to the NimbusFlow Interactive CLI!") - print(" Navigate through menus to manage your choir scheduling system.") + 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("āøļø Press Enter to continue...") + input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}") while True: - print("\033[2J\033[H") # Clear screen + clear_screen() # Clear screen display_main_menu() choice = get_user_choice(4) @@ -325,6 +375,7 @@ def run_interactive_mode(cli: "NimbusFlowCLI"): handle_services_menu(cli) elif choice == 4: # Exit - print("\nšŸŽµ Thank you for using NimbusFlow!") - print(" Have a wonderful day! 🌟") + clear_screen() + print(f"\n{Colors.SUCCESS}Thank you for using NimbusFlow!{Colors.RESET}") + print(f"{Colors.DIM}Goodbye!{Colors.RESET}") break \ No newline at end of file diff --git a/backend/cli/utils.py b/backend/cli/utils.py index ebc1cb6..28c4860 100644 --- a/backend/cli/utils.py +++ b/backend/cli/utils.py @@ -6,22 +6,59 @@ from typing import Optional from datetime import datetime from backend.models.enums import ScheduleStatus +# ANSI color codes for table formatting +class TableColors: + 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' + + # Table-specific colors + 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 + BORDER = '\033[90m' # Grey + def format_member_row(member, classification_name: Optional[str] = None) -> str: - """Format a member for table display.""" - active = "āœ“" if member.IsActive else "āœ—" + """Format a member for table display with colors.""" + # Color coding for active status + if member.IsActive: + active = f"{TableColors.SUCCESS}āœ“ Active{TableColors.RESET}" + name_color = TableColors.BOLD + else: + active = f"{TableColors.DIM}āœ— Inactive{TableColors.RESET}" + name_color = TableColors.DIM + classification = classification_name or "N/A" - return f"{member.MemberId:3d} | {member.FirstName:<12} | {member.LastName:<15} | {classification:<12} | {active:^6} | {member.Email or 'N/A'}" + email = member.Email or f"{TableColors.DIM}N/A{TableColors.RESET}" + + return (f"{TableColors.CYAN}{member.MemberId:3d}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} " + f"{name_color}{member.FirstName:<12}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} " + f"{name_color}{member.LastName:<15}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} " + f"{TableColors.YELLOW}{classification:<12}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} " + f"{active:<19} {TableColors.BORDER}│{TableColors.RESET} {email}") def format_schedule_row(schedule, member_name: str = "", service_info: str = "") -> str: - """Format a schedule for table display.""" - status_symbols = { - ScheduleStatus.PENDING: "ā³", - ScheduleStatus.ACCEPTED: "āœ…", - ScheduleStatus.DECLINED: "āŒ" - } - status_symbol = status_symbols.get(ScheduleStatus.from_raw(schedule.Status), "ā“") + """Format a schedule for table display with colors.""" + # Color-coded status formatting + status_enum = ScheduleStatus.from_raw(schedule.Status) + if status_enum == ScheduleStatus.PENDING: + status_display = f"{TableColors.WARNING}ā³ Pending{TableColors.RESET}" + elif status_enum == ScheduleStatus.ACCEPTED: + status_display = f"{TableColors.SUCCESS}āœ… Accepted{TableColors.RESET}" + elif status_enum == ScheduleStatus.DECLINED: + status_display = f"{TableColors.ERROR}āŒ Declined{TableColors.RESET}" + else: + status_display = f"{TableColors.DIM}ā“ Unknown{TableColors.RESET}" # Handle ScheduledAt - could be datetime object or string from DB if schedule.ScheduledAt: @@ -36,6 +73,50 @@ def format_schedule_row(schedule, member_name: str = "", service_info: str = "") # If it's already a datetime object scheduled_date = schedule.ScheduledAt.strftime("%Y-%m-%d %H:%M") else: - scheduled_date = "N/A" + scheduled_date = f"{TableColors.DIM}N/A{TableColors.RESET}" - return f"{schedule.ScheduleId:3d} | {status_symbol} {schedule.Status:<8} | {member_name:<20} | {service_info:<15} | {scheduled_date}" \ No newline at end of file + member_display = member_name if member_name else f"{TableColors.DIM}Unknown{TableColors.RESET}" + service_display = service_info if service_info else f"{TableColors.DIM}Unknown{TableColors.RESET}" + + return (f"{TableColors.CYAN}{schedule.ScheduleId:3d}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} " + f"{status_display:<20} {TableColors.BORDER}│{TableColors.RESET} " + f"{TableColors.BOLD}{member_display:<20}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} " + f"{TableColors.BLUE}{service_display:<20}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} {scheduled_date}") + + +def create_table_header(*columns) -> str: + """Create a styled table header.""" + header_parts = [] + for i, column in enumerate(columns): + if i == 0: + header_parts.append(f"{TableColors.HEADER}{column}{TableColors.RESET}") + else: + header_parts.append(f" {TableColors.BORDER}│{TableColors.RESET} {TableColors.HEADER}{column}{TableColors.RESET}") + return "".join(header_parts) + + +def create_table_separator(total_width: int) -> str: + """Create a styled table separator line.""" + return f"{TableColors.BORDER}{'─' * total_width}{TableColors.RESET}" + + +def format_service_row(service, type_name: str, counts: dict) -> str: + """Format a service for table display with colors.""" + # Format date properly + date_str = str(service.ServiceDate) if service.ServiceDate else f"{TableColors.DIM}N/A{TableColors.RESET}" + + # Color-code the counts + total_display = f"{TableColors.BOLD}{counts['total']}{TableColors.RESET}" if counts['total'] > 0 else f"{TableColors.DIM}0{TableColors.RESET}" + + pending_display = f"{TableColors.WARNING}{counts['pending']}{TableColors.RESET}" if counts['pending'] > 0 else f"{TableColors.DIM}0{TableColors.RESET}" + + accepted_display = f"{TableColors.SUCCESS}{counts['accepted']}{TableColors.RESET}" if counts['accepted'] > 0 else f"{TableColors.DIM}0{TableColors.RESET}" + + declined_display = f"{TableColors.ERROR}{counts['declined']}{TableColors.RESET}" if counts['declined'] > 0 else f"{TableColors.DIM}0{TableColors.RESET}" + + return (f"{TableColors.CYAN}{service.ServiceId:<3}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} " + f"{TableColors.BOLD}{date_str:<12}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} " + f"{TableColors.YELLOW}{type_name:<12}{TableColors.RESET} {TableColors.BORDER}│{TableColors.RESET} " + f"{total_display:<5} {TableColors.BORDER}│{TableColors.RESET} " + f"{pending_display:<7} {TableColors.BORDER}│{TableColors.RESET} " + f"{accepted_display:<8} {TableColors.BORDER}│{TableColors.RESET} {declined_display}") \ No newline at end of file