""" Schedule-related CLI commands. """ import argparse from datetime import date from typing import TYPE_CHECKING if TYPE_CHECKING: from backend.cli.base import NimbusFlowCLI from backend.models.enums import ScheduleStatus 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.""" schedules = cli.schedule_repo.list_all() # Apply filters if args.service_id: schedules = [s for s in schedules if s.ServiceId == args.service_id] if args.status: try: status_enum = ScheduleStatus.from_raw(args.status.lower()) schedules = [s for s in schedules if s.Status == status_enum.value] except ValueError: print(f"{TableColors.ERROR}Invalid status '{args.status}'. Valid options: pending, accepted, declined{TableColors.RESET}") return if not schedules: print(f"{TableColors.DIM}No schedules found.{TableColors.RESET}") return # Get member and service info for display member_map = {m.MemberId: f"{m.FirstName} {m.LastName}" for m in cli.member_repo.list_all()} service_map = {} service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()} for service in cli.service_repo.list_all(): type_name = service_type_map.get(service.ServiceTypeId, "Unknown") service_map[service.ServiceId] = f"{type_name} {service.ServiceDate}" # Print styled header print() print(create_table_header("ID ", "Status ", "Member ", "Service ", "Scheduled")) print(create_table_separator(95)) # Print schedules for schedule in schedules: member_name = member_map.get(schedule.MemberId, f"ID:{schedule.MemberId}") service_info = service_map.get(schedule.ServiceId, f"ID:{schedule.ServiceId}") print(format_schedule_row(schedule, member_name, service_info)) 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"{TableColors.ERROR}Schedule with ID {args.schedule_id} not found{TableColors.RESET}") return # Get related information member = cli.member_repo.get_by_id(schedule.MemberId) service = cli.service_repo.get_by_id(schedule.ServiceId) service_type = None if service: service_type = cli.service_type_repo.get_by_id(service.ServiceTypeId) 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"{TableColors.BOLD}Decline Reason:{TableColors.RESET} {TableColors.ERROR}{schedule.DeclineReason}{TableColors.RESET}") def cmd_schedules_accept(cli: "NimbusFlowCLI", args) -> None: """Accept a scheduled position.""" # Interactive mode with date parameter if hasattr(args, 'date') and args.date: try: target_date = date.fromisoformat(args.date) except ValueError: print(f"āŒ Invalid date format '{args.date}'. Use YYYY-MM-DD") return # Find services for the specified date all_services = cli.service_repo.list_all() services_on_date = [s for s in all_services if s.ServiceDate == target_date] if not services_on_date: print(f"āŒ No services found for {args.date}") return # Get service types for display service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()} # Show available services for the date print(f"\nšŸ“… Services available on {args.date}:") print("-" * 40) for i, service in enumerate(services_on_date, 1): type_name = service_type_map.get(service.ServiceTypeId, "Unknown") print(f"{i}. {type_name} (Service ID: {service.ServiceId})") # Let user select service try: choice = input(f"\nSelect service (1-{len(services_on_date)}): ").strip() if not choice or not choice.isdigit(): print("āŒ Invalid selection") return service_index = int(choice) - 1 if service_index < 0 or service_index >= len(services_on_date): print("āŒ Invalid selection") return selected_service = services_on_date[service_index] except (KeyboardInterrupt, EOFError): print("\nšŸ›‘ Cancelled") return # Find pending schedules for this service all_schedules = cli.schedule_repo.list_all() pending_schedules = [ s for s in all_schedules if s.ServiceId == selected_service.ServiceId and s.Status == ScheduleStatus.PENDING.value ] if not pending_schedules: service_type_name = service_type_map.get(selected_service.ServiceTypeId, "Unknown") print(f"āŒ No pending schedules found for {service_type_name} on {args.date}") return # Get member info for display member_map = {m.MemberId: m for m in cli.member_repo.list_all()} # Show available members to accept print(f"\nšŸ‘„ Members scheduled for {service_type_map.get(selected_service.ServiceTypeId, 'Unknown')} on {args.date}:") print("-" * 60) for i, schedule in enumerate(pending_schedules, 1): member = member_map.get(schedule.MemberId) if member: print(f"{i}. {member.FirstName} {member.LastName} (Schedule ID: {schedule.ScheduleId})") else: print(f"{i}. Unknown Member (ID: {schedule.MemberId}) (Schedule ID: {schedule.ScheduleId})") # Let user select member try: choice = input(f"\nSelect member to accept (1-{len(pending_schedules)}): ").strip() if not choice or not choice.isdigit(): print("āŒ Invalid selection") return member_index = int(choice) - 1 if member_index < 0 or member_index >= len(pending_schedules): print("āŒ Invalid selection") return selected_schedule = pending_schedules[member_index] except (KeyboardInterrupt, EOFError): print("\nšŸ›‘ Cancelled") return # Accept the selected schedule schedule_to_accept = selected_schedule # Direct mode with schedule ID elif hasattr(args, 'schedule_id') and args.schedule_id: schedule_to_accept = cli.schedule_repo.get_by_id(args.schedule_id) if not schedule_to_accept: print(f"āŒ Schedule with ID {args.schedule_id} not found") return else: print("āŒ Either --date or schedule_id must be provided") return # Common validation and acceptance logic if schedule_to_accept.Status == ScheduleStatus.ACCEPTED.value: print(f"āš ļø Schedule {schedule_to_accept.ScheduleId} is already accepted") return if schedule_to_accept.Status == ScheduleStatus.DECLINED.value: print(f"āš ļø Schedule {schedule_to_accept.ScheduleId} was previously declined") return # Get member and service info for display member = cli.member_repo.get_by_id(schedule_to_accept.MemberId) service = cli.service_repo.get_by_id(schedule_to_accept.ServiceId) service_type = None if service: service_type = cli.service_type_repo.get_by_id(service.ServiceTypeId) # Mark the schedule as accepted cli.schedule_repo.mark_accepted(schedule_to_accept.ScheduleId) # Update member's acceptance timestamp cli.member_repo.set_last_accepted(schedule_to_accept.MemberId) print(f"āœ… Schedule {schedule_to_accept.ScheduleId} accepted successfully!") if member and service and service_type: print(f" Member: {member.FirstName} {member.LastName}") print(f" Service: {service_type.TypeName} on {service.ServiceDate}") def cmd_schedules_decline(cli: "NimbusFlowCLI", args) -> None: """Decline a scheduled position.""" # Interactive mode with date parameter if hasattr(args, 'date') and args.date: try: target_date = date.fromisoformat(args.date) except ValueError: print(f"āŒ Invalid date format '{args.date}'. Use YYYY-MM-DD") return # Find services for the specified date all_services = cli.service_repo.list_all() services_on_date = [s for s in all_services if s.ServiceDate == target_date] if not services_on_date: print(f"āŒ No services found for {args.date}") return # Get service types for display service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()} # Show available services for the date print(f"\nšŸ“… Services available on {args.date}:") print("-" * 40) for i, service in enumerate(services_on_date, 1): type_name = service_type_map.get(service.ServiceTypeId, "Unknown") print(f"{i}. {type_name} (Service ID: {service.ServiceId})") # Let user select service try: choice = input(f"\nSelect service (1-{len(services_on_date)}): ").strip() if not choice or not choice.isdigit(): print("āŒ Invalid selection") return service_index = int(choice) - 1 if service_index < 0 or service_index >= len(services_on_date): print("āŒ Invalid selection") return selected_service = services_on_date[service_index] except (KeyboardInterrupt, EOFError): print("\nšŸ›‘ Cancelled") return # Find pending schedules for this service all_schedules = cli.schedule_repo.list_all() pending_schedules = [ s for s in all_schedules if s.ServiceId == selected_service.ServiceId and s.Status == ScheduleStatus.PENDING.value ] if not pending_schedules: service_type_name = service_type_map.get(selected_service.ServiceTypeId, "Unknown") print(f"āŒ No pending schedules found for {service_type_name} on {args.date}") return # Get member info for display member_map = {m.MemberId: m for m in cli.member_repo.list_all()} # Show available members to decline print(f"\nšŸ‘„ Members scheduled for {service_type_map.get(selected_service.ServiceTypeId, 'Unknown')} on {args.date}:") print("-" * 60) for i, schedule in enumerate(pending_schedules, 1): member = member_map.get(schedule.MemberId) if member: print(f"{i}. {member.FirstName} {member.LastName} (Schedule ID: {schedule.ScheduleId})") else: print(f"{i}. Unknown Member (ID: {schedule.MemberId}) (Schedule ID: {schedule.ScheduleId})") # Let user select member try: choice = input(f"\nSelect member to decline (1-{len(pending_schedules)}): ").strip() if not choice or not choice.isdigit(): print("āŒ Invalid selection") return member_index = int(choice) - 1 if member_index < 0 or member_index >= len(pending_schedules): print("āŒ Invalid selection") return selected_schedule = pending_schedules[member_index] except (KeyboardInterrupt, EOFError): print("\nšŸ›‘ Cancelled") return # Get decline reason if not provided decline_reason = args.reason if hasattr(args, 'reason') and args.reason else None if not decline_reason: try: decline_reason = input("\nEnter decline reason (optional, press Enter to skip): ").strip() if not decline_reason: decline_reason = None except (KeyboardInterrupt, EOFError): print("\nšŸ›‘ Cancelled") return # Decline the selected schedule schedule_to_decline = selected_schedule # Direct mode with schedule ID elif hasattr(args, 'schedule_id') and args.schedule_id: schedule_to_decline = cli.schedule_repo.get_by_id(args.schedule_id) if not schedule_to_decline: print(f"āŒ Schedule with ID {args.schedule_id} not found") return decline_reason = args.reason if hasattr(args, 'reason') else None else: print("āŒ Either --date or schedule_id must be provided") return # Common validation and decline logic if schedule_to_decline.Status == ScheduleStatus.DECLINED.value: print(f"āš ļø Schedule {schedule_to_decline.ScheduleId} is already declined") return if schedule_to_decline.Status == ScheduleStatus.ACCEPTED.value: print(f"āš ļø Schedule {schedule_to_decline.ScheduleId} was previously accepted") return # Get member and service info for display member = cli.member_repo.get_by_id(schedule_to_decline.MemberId) service = cli.service_repo.get_by_id(schedule_to_decline.ServiceId) service_type = None if service: service_type = cli.service_type_repo.get_by_id(service.ServiceTypeId) # Mark the schedule as declined cli.schedule_repo.mark_declined(schedule_to_decline.ScheduleId, decline_reason=decline_reason) # Update member's decline timestamp (using service date) if service: cli.member_repo.set_last_declined(schedule_to_decline.MemberId, str(service.ServiceDate)) print(f"āŒ Schedule {schedule_to_decline.ScheduleId} declined successfully!") if member and service and service_type: print(f" Member: {member.FirstName} {member.LastName}") print(f" Service: {service_type.TypeName} on {service.ServiceDate}") if decline_reason: print(f" Reason: {decline_reason}") def cmd_schedules_schedule(cli: "NimbusFlowCLI", args) -> None: """Schedule next member for a service with preview and confirmation.""" # Determine if we're using service ID or date-based selection service = None if hasattr(args, 'date') and args.date: # Date-based selection try: target_date = date.fromisoformat(args.date) except ValueError: print(f"āŒ Invalid date format '{args.date}'. Use YYYY-MM-DD") return # Find services for the specified date all_services = cli.service_repo.list_all() services_on_date = [s for s in all_services if s.ServiceDate == target_date] if not services_on_date: print(f"āŒ No services found for {args.date}") return # Get service types for display service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()} # Show available services for the date print(f"\nšŸ“… Services available on {args.date}:") print("-" * 50) for i, svc in enumerate(services_on_date, 1): type_name = service_type_map.get(svc.ServiceTypeId, "Unknown") print(f"{i}. {type_name} (Service ID: {svc.ServiceId})") # Let user select service try: choice = input(f"\nSelect service (1-{len(services_on_date)}): ").strip() if not choice or not choice.isdigit(): print("āŒ Invalid selection") return service_index = int(choice) - 1 if service_index < 0 or service_index >= len(services_on_date): print("āŒ Invalid selection") return service = services_on_date[service_index] except (KeyboardInterrupt, EOFError): print("\nšŸ›‘ Operation cancelled") return elif hasattr(args, 'service_id') and args.service_id: # Service ID based selection print(f"Scheduling for service ID {args.service_id}...") service = cli.service_repo.get_by_id(args.service_id) if not service: print(f"āŒ Service ID {args.service_id} not found.") return else: print("āŒ Either --date or service_id must be provided") return # Get service type name service_type = cli.service_type_repo.get_by_id(service.ServiceTypeId) service_type_name = service_type.TypeName if service_type else "Unknown" print(f"\nšŸ“… Selected Service: {service_type_name} on {service.ServiceDate}") # Get classification constraints if not provided classification_ids = [] if args.classifications: # Convert classification names to IDs all_classifications = cli.classification_repo.list_all() classification_map = {c.ClassificationName.lower(): c.ClassificationId for c in all_classifications} for class_name in args.classifications: class_id = classification_map.get(class_name.lower()) if class_id: classification_ids.append(class_id) else: print(f"āŒ Unknown classification: {class_name}") return else: # If no classifications specified, ask user to select all_classifications = cli.classification_repo.list_all() print("\nšŸŽµ Available classifications:") for i, classification in enumerate(all_classifications, 1): print(f" {i}. {classification.ClassificationName}") while True: try: choice = input(f"\nšŸŽÆ Select classification(s) (1-{len(all_classifications)}, comma-separated): ").strip() if not choice: continue selections = [int(x.strip()) for x in choice.split(',')] if all(1 <= sel <= len(all_classifications) for sel in selections): classification_ids = [all_classifications[sel-1].ClassificationId for sel in selections] break else: print(f"āŒ Please enter numbers between 1 and {len(all_classifications)}") except ValueError: print("āŒ Please enter valid numbers") except (KeyboardInterrupt, EOFError): print("\nšŸ›‘ Operation cancelled") return # Get selected classification names for display selected_classifications = [c.ClassificationName for c in cli.classification_repo.list_all() if c.ClassificationId in classification_ids] print(f"\nšŸ” Looking for eligible members in: {', '.join(selected_classifications)}") excluded_members = set() while True: # Preview who would be scheduled (excluding any we've already shown) preview_result = cli.scheduling_service.preview_next_member( classification_ids=classification_ids, service_id=service.ServiceId, only_active=True, exclude_member_ids=excluded_members, ) if not preview_result: if excluded_members: print("āŒ No more eligible members found for this service.") else: print("āŒ No eligible members found for this service.") return member_id, first_name, last_name = preview_result # Show preview print(f"\n✨ Next available member:") print(f" šŸ‘¤ {first_name} {last_name} (ID: {member_id})") # Confirm scheduling try: confirm = input(f"\nšŸ¤” Schedule {first_name} {last_name} for this service? (y/N/q to quit): ").strip().lower() if confirm in ['y', 'yes']: # Actually create the schedule result = cli.scheduling_service.schedule_next_member( classification_ids=classification_ids, service_id=service.ServiceId, only_active=True, exclude_member_ids=excluded_members, ) if result: scheduled_member_id, scheduled_first, scheduled_last, schedule_id = result print(f"\nāœ… Successfully scheduled {scheduled_first} {scheduled_last}!") print(f" šŸ“‹ Schedule ID: {schedule_id}") print(f" šŸ“§ Status: Pending (awaiting member response)") else: print("āŒ Failed to create schedule. The member may no longer be eligible.") return elif confirm in ['q', 'quit']: print("šŸ›‘ Scheduling cancelled") return else: # User declined this member - add to exclusion list and continue excluded_members.add(member_id) print(f"ā­ļø Skipping {first_name} {last_name}, looking for next member...") except (KeyboardInterrupt, EOFError): print("\nšŸ›‘ Operation cancelled") return def setup_schedules_parser(subparsers) -> None: """Set up schedule-related command parsers.""" # Schedules commands schedules_parser = subparsers.add_parser("schedules", help="Manage schedules") schedules_subparsers = schedules_parser.add_subparsers(dest="schedules_action", help="Schedule actions") # schedules list schedules_list_parser = schedules_subparsers.add_parser("list", help="List schedules") schedules_list_parser.add_argument("--service-id", type=int, help="Filter by service ID") schedules_list_parser.add_argument("--status", type=str, help="Filter by status (pending/accepted/declined)") # schedules show schedules_show_parser = schedules_subparsers.add_parser("show", help="Show schedule details") schedules_show_parser.add_argument("schedule_id", type=int, help="Schedule ID to show") # schedules accept schedules_accept_parser = schedules_subparsers.add_parser("accept", help="Accept a scheduled position") schedules_accept_parser.add_argument("schedule_id", type=int, nargs="?", help="Schedule ID to accept (optional if using --date)") schedules_accept_parser.add_argument("--date", type=str, help="Interactive mode: select service and member by date (YYYY-MM-DD)") # schedules decline schedules_decline_parser = schedules_subparsers.add_parser("decline", help="Decline a scheduled position") schedules_decline_parser.add_argument("schedule_id", type=int, nargs="?", help="Schedule ID to decline (optional if using --date)") schedules_decline_parser.add_argument("--date", type=str, help="Interactive mode: select service and member by date (YYYY-MM-DD)") schedules_decline_parser.add_argument("--reason", type=str, help="Reason for declining") # schedules schedule schedules_schedule_parser = schedules_subparsers.add_parser("schedule", help="Schedule next member for a service (cycles through eligible members)") schedules_schedule_parser.add_argument("service_id", type=int, nargs="?", help="Service ID to schedule for (optional if using --date)") schedules_schedule_parser.add_argument("--date", type=str, help="Interactive mode: select service by date (YYYY-MM-DD)") schedules_schedule_parser.add_argument("--classifications", nargs="*", help="Classification names to filter by (e.g., Soprano Alto)")