""" 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 hasattr(args, 'date') and args.date: # Filter schedules by date - find services for that date first try: from datetime import date as date_type target_date = date_type.fromisoformat(args.date) services_on_date = [s.ServiceId for s in cli.service_repo.list_all() if s.ServiceDate == target_date] schedules = [s for s in schedules if s.ServiceId in services_on_date] except ValueError: print(f"{TableColors.ERROR}Invalid date format '{args.date}'. Use YYYY-MM-DD{TableColors.RESET}") return 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{TableColors.HEADER}Services available on {args.date}{TableColors.RESET}") print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}") print() for i, service in enumerate(services_on_date, 1): type_name = service_type_map.get(service.ServiceTypeId, "Unknown") print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {type_name}") print() # Let user select service try: print(f"\n{TableColors.INPUT_BOX}┌─ Select service (1-{len(services_on_date)}) ─┐{TableColors.RESET}") choice = input(f"{TableColors.INPUT_BOX}└─> {TableColors.RESET}").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 # Clear screen after service selection print("\033[2J\033[H") # 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{TableColors.HEADER}Members scheduled for {service_type_map.get(selected_service.ServiceTypeId, 'Unknown')} on {args.date}:{TableColors.RESET}") print(f"{TableColors.BORDER}{'─' * 70}{TableColors.RESET}") for i, schedule in enumerate(pending_schedules, 1): member = member_map.get(schedule.MemberId) if member: print(f"{TableColors.CYAN}{i:2d}.{TableColors.RESET} {TableColors.BOLD}{member.FirstName} {member.LastName}{TableColors.RESET} {TableColors.DIM}(Schedule ID: {schedule.ScheduleId}){TableColors.RESET}") else: print(f"{TableColors.CYAN}{i:2d}.{TableColors.RESET} {TableColors.DIM}Unknown Member (ID: {schedule.MemberId}) (Schedule ID: {schedule.ScheduleId}){TableColors.RESET}") # Let user select multiple members print(f"\n{TableColors.SUCCESS}Multiple Selection Options:{TableColors.RESET}") print(f" • {TableColors.CYAN}Single:{TableColors.RESET} Enter a number (e.g., {TableColors.YELLOW}3{TableColors.RESET})") print(f" • {TableColors.CYAN}Multiple:{TableColors.RESET} Enter numbers separated by commas (e.g., {TableColors.YELLOW}1,3,5{TableColors.RESET})") print(f" • {TableColors.CYAN}Range:{TableColors.RESET} Enter a range (e.g., {TableColors.YELLOW}1-4{TableColors.RESET})") print(f" • {TableColors.CYAN}All:{TableColors.RESET} Enter {TableColors.YELLOW}all{TableColors.RESET} to select everyone") try: choice = input(f"\n{TableColors.INPUT_BOX}┌─ Select members to accept ─┐{TableColors.RESET}\n{TableColors.INPUT_BOX}└─> {TableColors.RESET}").strip() if not choice: print(f"{TableColors.ERROR}❌ No selection made{TableColors.RESET}") return selected_indices = [] if choice.lower() == 'all': selected_indices = list(range(len(pending_schedules))) elif '-' in choice and ',' not in choice: # Handle range (e.g., "1-4") try: start, end = choice.split('-') start_idx = int(start.strip()) - 1 end_idx = int(end.strip()) - 1 if start_idx < 0 or end_idx >= len(pending_schedules) or start_idx > end_idx: print(f"{TableColors.ERROR}❌ Invalid range{TableColors.RESET}") return selected_indices = list(range(start_idx, end_idx + 1)) except (ValueError, IndexError): print(f"{TableColors.ERROR}❌ Invalid range format{TableColors.RESET}") return else: # Handle single number or comma-separated list try: numbers = [int(x.strip()) for x in choice.split(',')] for num in numbers: idx = num - 1 if idx < 0 or idx >= len(pending_schedules): print(f"{TableColors.ERROR}❌ Invalid selection: {num}{TableColors.RESET}") return if idx not in selected_indices: selected_indices.append(idx) except ValueError: print(f"{TableColors.ERROR}❌ Invalid input format{TableColors.RESET}") return if not selected_indices: print(f"{TableColors.ERROR}❌ No valid selections{TableColors.RESET}") return schedules_to_accept = [pending_schedules[i] for i in selected_indices] except (KeyboardInterrupt, EOFError): print(f"\n{TableColors.WARNING}🛑 Cancelled{TableColors.RESET}") return # Direct mode with schedule ID (single schedule) 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"{TableColors.ERROR}❌ Schedule with ID {args.schedule_id} not found{TableColors.RESET}") return schedules_to_accept = [schedule_to_accept] else: print(f"{TableColors.ERROR}❌ Either --date or schedule_id must be provided{TableColors.RESET}") return # Process multiple schedules successful_accepts = [] failed_accepts = [] print(f"\n{TableColors.SUCCESS}Processing {len(schedules_to_accept)} schedule(s)...{TableColors.RESET}") for schedule in schedules_to_accept: # Validation for each schedule if schedule.Status == ScheduleStatus.ACCEPTED.value: failed_accepts.append((schedule, f"already accepted")) continue if schedule.Status == ScheduleStatus.DECLINED.value: failed_accepts.append((schedule, f"previously declined")) continue # Get member and service info for display 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) try: # Mark the schedule as accepted cli.schedule_repo.mark_accepted(schedule.ScheduleId) # Update member's acceptance timestamp cli.member_repo.set_last_accepted(schedule.MemberId) successful_accepts.append((schedule, member, service, service_type)) except Exception as e: failed_accepts.append((schedule, f"database error: {e}")) # Display results if successful_accepts: print(f"\n{TableColors.SUCCESS}✅ Successfully accepted {len(successful_accepts)} schedule(s):{TableColors.RESET}") for schedule, member, service, service_type in successful_accepts: member_name = f"{member.FirstName} {member.LastName}" if member else f"Member ID {schedule.MemberId}" service_info = f"{service_type.TypeName} on {service.ServiceDate}" if service and service_type else f"Service ID {schedule.ServiceId}" print(f" {TableColors.CYAN}•{TableColors.RESET} {TableColors.BOLD}{member_name}{TableColors.RESET} - {service_info}") if failed_accepts: print(f"\n{TableColors.WARNING}⚠️ Could not accept {len(failed_accepts)} schedule(s):{TableColors.RESET}") for schedule, reason in failed_accepts: member = cli.member_repo.get_by_id(schedule.MemberId) member_name = f"{member.FirstName} {member.LastName}" if member else f"Member ID {schedule.MemberId}" print(f" {TableColors.YELLOW}•{TableColors.RESET} {TableColors.BOLD}{member_name}{TableColors.RESET} - {reason}") if not successful_accepts and not failed_accepts: print(f"{TableColors.DIM}No schedules processed{TableColors.RESET}") def cmd_schedules_remove(cli: "NimbusFlowCLI", args) -> None: """Remove scheduled members and move them back to the front of the queue.""" # Interactive mode with date parameter if hasattr(args, 'date') and args.date: try: target_date = date.fromisoformat(args.date) except ValueError: print(f"{TableColors.ERROR}❌ Invalid date format '{args.date}'. Use YYYY-MM-DD{TableColors.RESET}") 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"{TableColors.ERROR}❌ No services found for {args.date}{TableColors.RESET}") 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{TableColors.HEADER}Services available on {args.date}{TableColors.RESET}") print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}") print() for i, service in enumerate(services_on_date, 1): type_name = service_type_map.get(service.ServiceTypeId, "Unknown") print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {type_name}") print() # Let user select service try: print(f"\n{TableColors.INPUT_BOX}┌─ Select service (1-{len(services_on_date)}) ─┐{TableColors.RESET}") choice = input(f"{TableColors.INPUT_BOX}└─> {TableColors.RESET}").strip() if not choice or not choice.isdigit(): print(f"{TableColors.ERROR}❌ Invalid selection{TableColors.RESET}") return service_index = int(choice) - 1 if service_index < 0 or service_index >= len(services_on_date): print(f"{TableColors.ERROR}❌ Invalid selection{TableColors.RESET}") return selected_service = services_on_date[service_index] except (KeyboardInterrupt, EOFError): print(f"\n{TableColors.WARNING}🛑 Cancelled{TableColors.RESET}") return # Clear screen after service selection print("\033[2J\033[H") # Find all schedules for this service (not just pending) all_schedules = cli.schedule_repo.list_all() service_schedules = [ s for s in all_schedules if s.ServiceId == selected_service.ServiceId ] if not service_schedules: service_type_name = service_type_map.get(selected_service.ServiceTypeId, "Unknown") print(f"{TableColors.WARNING}⚠️ No schedules found for {service_type_name} on {args.date}{TableColors.RESET}") return # Get member info for display member_map = {m.MemberId: m for m in cli.member_repo.list_all()} # Show available members to remove print(f"\n{TableColors.HEADER}Members scheduled for {service_type_map.get(selected_service.ServiceTypeId, 'Unknown')} on {args.date}:{TableColors.RESET}") print(f"{TableColors.BORDER}{'─' * 70}{TableColors.RESET}") for i, schedule in enumerate(service_schedules, 1): member = member_map.get(schedule.MemberId) status_color = TableColors.SUCCESS if schedule.Status == "accepted" else TableColors.WARNING if schedule.Status == "pending" else TableColors.ERROR status_display = f"{status_color}{schedule.Status.upper()}{TableColors.RESET}" if member: print(f"{TableColors.CYAN}{i:2d}.{TableColors.RESET} {TableColors.BOLD}{member.FirstName} {member.LastName}{TableColors.RESET} {TableColors.DIM}({status_display}, ID: {schedule.ScheduleId}){TableColors.RESET}") else: print(f"{TableColors.CYAN}{i:2d}.{TableColors.RESET} {TableColors.DIM}Unknown Member (ID: {schedule.MemberId}) ({status_display}, ID: {schedule.ScheduleId}){TableColors.RESET}") # Let user select multiple members to remove print(f"\n{TableColors.SUCCESS}Multiple Selection Options:{TableColors.RESET}") print(f" • {TableColors.CYAN}Single:{TableColors.RESET} Enter a number (e.g., {TableColors.YELLOW}3{TableColors.RESET})") print(f" • {TableColors.CYAN}Multiple:{TableColors.RESET} Enter numbers separated by commas (e.g., {TableColors.YELLOW}1,3,5{TableColors.RESET})") print(f" • {TableColors.CYAN}Range:{TableColors.RESET} Enter a range (e.g., {TableColors.YELLOW}1-4{TableColors.RESET})") print(f" • {TableColors.CYAN}All:{TableColors.RESET} Enter {TableColors.YELLOW}all{TableColors.RESET} to remove everyone") try: choice = input(f"\n{TableColors.INPUT_BOX}┌─ Select members to remove ─┐{TableColors.RESET}\n{TableColors.INPUT_BOX}└─> {TableColors.RESET}").strip() if not choice: print(f"{TableColors.ERROR}❌ No selection made{TableColors.RESET}") return selected_indices = [] if choice.lower() == 'all': selected_indices = list(range(len(service_schedules))) elif '-' in choice and ',' not in choice: # Handle range (e.g., "1-4") try: start, end = choice.split('-') start_idx = int(start.strip()) - 1 end_idx = int(end.strip()) - 1 if start_idx < 0 or end_idx >= len(service_schedules) or start_idx > end_idx: print(f"{TableColors.ERROR}❌ Invalid range{TableColors.RESET}") return selected_indices = list(range(start_idx, end_idx + 1)) except (ValueError, IndexError): print(f"{TableColors.ERROR}❌ Invalid range format{TableColors.RESET}") return else: # Handle single number or comma-separated list try: numbers = [int(x.strip()) for x in choice.split(',')] for num in numbers: idx = num - 1 if idx < 0 or idx >= len(service_schedules): print(f"{TableColors.ERROR}❌ Invalid selection: {num}{TableColors.RESET}") return if idx not in selected_indices: selected_indices.append(idx) except ValueError: print(f"{TableColors.ERROR}❌ Invalid input format{TableColors.RESET}") return if not selected_indices: print(f"{TableColors.ERROR}❌ No valid selections{TableColors.RESET}") return schedules_to_remove = [service_schedules[i] for i in selected_indices] except (KeyboardInterrupt, EOFError): print(f"\n{TableColors.WARNING}🛑 Cancelled{TableColors.RESET}") return # Direct mode with schedule ID (single schedule) elif hasattr(args, 'schedule_id') and args.schedule_id: schedule_to_remove = cli.schedule_repo.get_by_id(args.schedule_id) if not schedule_to_remove: print(f"{TableColors.ERROR}❌ Schedule with ID {args.schedule_id} not found{TableColors.RESET}") return schedules_to_remove = [schedule_to_remove] else: print(f"{TableColors.ERROR}❌ Either --date or schedule_id must be provided{TableColors.RESET}") return # Process schedule removals successful_removals = [] failed_removals = [] print(f"\n{TableColors.WARNING}Processing {len(schedules_to_remove)} schedule removal(s)...{TableColors.RESET}") for schedule in schedules_to_remove: # Get member and service info for display 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) try: # Delete the schedule was_deleted = cli.schedule_repo.delete_schedule(schedule.ScheduleId) if was_deleted: # Move member back to front of round robin queue cli.member_repo.reset_to_queue_front(schedule.MemberId) successful_removals.append((schedule, member, service, service_type)) else: failed_removals.append((schedule, "schedule not found")) except Exception as e: failed_removals.append((schedule, f"database error: {e}")) # Display results if successful_removals: print(f"\n{TableColors.SUCCESS}✅ Successfully removed {len(successful_removals)} schedule(s):{TableColors.RESET}") print(f"{TableColors.SUCCESS} Members moved to front of round robin queue:{TableColors.RESET}") for schedule, member, service, service_type in successful_removals: member_name = f"{member.FirstName} {member.LastName}" if member else f"Member ID {schedule.MemberId}" service_info = f"{service_type.TypeName} on {service.ServiceDate}" if service and service_type else f"Service ID {schedule.ServiceId}" print(f" {TableColors.CYAN}•{TableColors.RESET} {TableColors.BOLD}{member_name}{TableColors.RESET} - {service_info}") if failed_removals: print(f"\n{TableColors.ERROR}❌ Could not remove {len(failed_removals)} schedule(s):{TableColors.RESET}") for schedule, reason in failed_removals: member = cli.member_repo.get_by_id(schedule.MemberId) member_name = f"{member.FirstName} {member.LastName}" if member else f"Member ID {schedule.MemberId}" print(f" {TableColors.YELLOW}•{TableColors.RESET} {TableColors.BOLD}{member_name}{TableColors.RESET} - {reason}") if not successful_removals and not failed_removals: print(f"{TableColors.DIM}No schedules processed{TableColors.RESET}") 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{TableColors.HEADER}Services available on {args.date}{TableColors.RESET}") print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}") print() for i, service in enumerate(services_on_date, 1): type_name = service_type_map.get(service.ServiceTypeId, "Unknown") print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {type_name}") print() # Let user select service try: print(f"\n{TableColors.INPUT_BOX}┌─ Select service (1-{len(services_on_date)}) ─┐{TableColors.RESET}") choice = input(f"{TableColors.INPUT_BOX}└─> {TableColors.RESET}").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 # Clear screen after service selection print("\033[2J\033[H") # 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 service_name = service_type_map.get(selected_service.ServiceTypeId, 'Unknown') print(f"{TableColors.HEADER}Members scheduled for {service_name} on {args.date}{TableColors.RESET}") print(f"{TableColors.BORDER}─" * 70 + f"{TableColors.RESET}") print() for i, schedule in enumerate(pending_schedules, 1): member = member_map.get(schedule.MemberId) if member: print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {member.FirstName} {member.LastName}") else: print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {TableColors.DIM}Unknown Member (ID: {schedule.MemberId}){TableColors.RESET}") print() # Let user select member try: print(f"\n{TableColors.INPUT_BOX}┌─ Select member to decline (1-{len(pending_schedules)}) ─┐{TableColors.RESET}") choice = input(f"{TableColors.INPUT_BOX}└─> {TableColors.RESET}").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{TableColors.HEADER}Services available on {args.date}{TableColors.RESET}") print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}") print() for i, svc in enumerate(services_on_date, 1): type_name = service_type_map.get(svc.ServiceTypeId, "Unknown") print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {type_name}") print() # Let user select service try: print(f"\n{TableColors.INPUT_BOX}┌─ Select service (1-{len(services_on_date)}) ─┐{TableColors.RESET}") choice = input(f"{TableColors.INPUT_BOX}└─> {TableColors.RESET}").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 # Clear screen after service selection print("\033[2J\033[H") 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"{TableColors.HEADER}Selected Service: {service_type_name} on {service.ServiceDate}{TableColors.RESET}") # 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(f"\n{TableColors.HEADER}Available classifications{TableColors.RESET}") print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}") print() for i, classification in enumerate(all_classifications, 1): print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {classification.ClassificationName}") print() while True: try: print(f"\n{TableColors.INPUT_BOX}┌─ Select classification(s) (1-{len(all_classifications)}, comma-separated) ─┐{TableColors.RESET}") choice = input(f"{TableColors.INPUT_BOX}└─> {TableColors.RESET}").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{TableColors.SUCCESS}Looking for eligible members in: {', '.join(selected_classifications)}{TableColors.RESET}") print() 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{TableColors.HEADER}Next available member{TableColors.RESET}") print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}") print(f" {TableColors.CYAN}{first_name} {last_name}{TableColors.RESET}") print() # Confirm scheduling try: print(f"\n{TableColors.INPUT_BOX}┌─ Schedule {first_name} {last_name} for this service? (y/N/q to quit) ─┐{TableColors.RESET}") confirm = input(f"{TableColors.INPUT_BOX}└─> {TableColors.RESET}").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{TableColors.SUCCESS}Successfully scheduled {scheduled_first} {scheduled_last}!{TableColors.RESET}") print(f"{TableColors.DIM}Schedule ID: {schedule_id}{TableColors.RESET}") print(f"{TableColors.DIM}Status: Pending (awaiting member response){TableColors.RESET}") else: print("❌ Failed to create schedule. The member may no longer be eligible.") return elif confirm in ['q', 'quit']: print(f"\n{TableColors.WARNING}Scheduling cancelled{TableColors.RESET}") return else: # User declined this member - add to exclusion list and continue excluded_members.add(member_id) print(f"\n{TableColors.DIM}Skipping {first_name} {last_name}, looking for next member...{TableColors.RESET}") print() except (KeyboardInterrupt, EOFError): print(f"\n{TableColors.WARNING}Operation cancelled{TableColors.RESET}") 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 remove schedules_remove_parser = schedules_subparsers.add_parser("remove", help="Remove scheduled members and move them to front of queue") schedules_remove_parser.add_argument("schedule_id", type=int, nargs="?", help="Schedule ID to remove (optional if using --date)") schedules_remove_parser.add_argument("--date", type=str, help="Interactive mode: select service and members by date (YYYY-MM-DD)") # 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)")