""" 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 def cmd_schedules_list(cli: "NimbusFlowCLI", args) -> None: """List schedules with optional filters.""" print("Listing schedules...") 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"āŒ Invalid status '{args.status}'. Valid options: pending, accepted, declined") return if not schedules: print("No schedules found.") 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 header print(f"\n{'ID':<3} | {'Status':<10} | {'Member':<20} | {'Service':<15} | {'Scheduled'}") print("-" * 80) # 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(f"\nTotal: {len(schedules)} schedules") 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") 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šŸ“… 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'}") if schedule.DeclineReason: print(f"Decline Reason: {schedule.DeclineReason}") 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)")