1039 lines
51 KiB
Python
1039 lines
51 KiB
Python
"""
|
|
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 OR accepted schedules for this service
|
|
all_schedules = cli.schedule_repo.list_all()
|
|
available_schedules = [
|
|
s for s in all_schedules
|
|
if s.ServiceId == selected_service.ServiceId and s.Status in [ScheduleStatus.PENDING.value, ScheduleStatus.ACCEPTED.value]
|
|
]
|
|
|
|
if not available_schedules:
|
|
service_type_name = service_type_map.get(selected_service.ServiceTypeId, "Unknown")
|
|
print(f"❌ No pending or accepted 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(available_schedules, 1):
|
|
member = member_map.get(schedule.MemberId)
|
|
status_color = TableColors.SUCCESS if schedule.Status == ScheduleStatus.ACCEPTED.value else TableColors.WARNING
|
|
status_text = f"{status_color}{schedule.Status.upper()}{TableColors.RESET}"
|
|
|
|
if member:
|
|
print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {member.FirstName} {member.LastName} {TableColors.DIM}({status_text}){TableColors.RESET}")
|
|
else:
|
|
print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {TableColors.DIM}Unknown Member (ID: {schedule.MemberId}) ({status_text}){TableColors.RESET}")
|
|
print()
|
|
|
|
# Let user select member
|
|
try:
|
|
print(f"\n{TableColors.INPUT_BOX}┌─ Select member to decline (1-{len(available_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(available_schedules):
|
|
print("❌ Invalid selection")
|
|
return
|
|
|
|
selected_schedule = available_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
|
|
|
|
# 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)
|
|
|
|
# Show what we're about to decline
|
|
was_accepted = schedule_to_decline.Status == ScheduleStatus.ACCEPTED.value
|
|
status_text = "accepted" if was_accepted else "pending"
|
|
|
|
if member and service and service_type:
|
|
print(f"\n{TableColors.WARNING}About to decline {status_text} schedule:{TableColors.RESET}")
|
|
print(f" Member: {TableColors.BOLD}{member.FirstName} {member.LastName}{TableColors.RESET}")
|
|
print(f" Service: {service_type.TypeName} on {service.ServiceDate}")
|
|
if decline_reason:
|
|
print(f" Reason: {decline_reason}")
|
|
|
|
# Use the scheduling service to handle decline logic properly
|
|
try:
|
|
action, updated_schedule_id = cli.scheduling_service.decline_service_for_user(
|
|
member_id=schedule_to_decline.MemberId,
|
|
service_id=schedule_to_decline.ServiceId,
|
|
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"\n{TableColors.SUCCESS}✅ Schedule {updated_schedule_id} declined successfully!{TableColors.RESET}")
|
|
|
|
if was_accepted:
|
|
print(f"{TableColors.WARNING} Note: This was previously accepted - member moved back to scheduling pool{TableColors.RESET}")
|
|
|
|
except Exception as e:
|
|
print(f"{TableColors.ERROR}❌ Failed to decline schedule: {e}{TableColors.RESET}")
|
|
return
|
|
|
|
|
|
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}")
|
|
|
|
# Check if we're doing name-based scheduling
|
|
if hasattr(args, 'member_name') and args.member_name:
|
|
return _schedule_specific_member(cli, service, service_type_name, args.member_name)
|
|
|
|
# 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 _schedule_specific_member(cli: "NimbusFlowCLI", service, service_type_name: str, member_name: str) -> None:
|
|
"""Helper function to schedule a specific member by name."""
|
|
|
|
# Search for matching members
|
|
all_members = cli.member_repo.list_all()
|
|
search_terms = member_name.lower().split()
|
|
|
|
matching_members = []
|
|
for member in all_members:
|
|
member_text = f"{member.FirstName} {member.LastName}".lower()
|
|
# Match if all search terms are found in the member's name
|
|
if all(term in member_text for term in search_terms):
|
|
matching_members.append(member)
|
|
|
|
if not matching_members:
|
|
print(f"{TableColors.ERROR}❌ No members found matching '{member_name}'{TableColors.RESET}")
|
|
return
|
|
|
|
# If multiple matches, let user select
|
|
selected_member = None
|
|
if len(matching_members) == 1:
|
|
selected_member = matching_members[0]
|
|
print(f"\n{TableColors.SUCCESS}Found member: {selected_member.FirstName} {selected_member.LastName}{TableColors.RESET}")
|
|
else:
|
|
print(f"\n{TableColors.HEADER}Multiple members found matching '{member_name}':{TableColors.RESET}")
|
|
print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}")
|
|
print()
|
|
for i, member in enumerate(matching_members, 1):
|
|
status = "Active" if member.IsActive else "Inactive"
|
|
status_color = TableColors.SUCCESS if member.IsActive else TableColors.DIM
|
|
print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {TableColors.BOLD}{member.FirstName} {member.LastName}{TableColors.RESET} {TableColors.DIM}({status_color}{status}{TableColors.RESET}{TableColors.DIM}){TableColors.RESET}")
|
|
print()
|
|
|
|
try:
|
|
print(f"\n{TableColors.INPUT_BOX}┌─ Select member (1-{len(matching_members)}) ─┐{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
|
|
|
|
member_index = int(choice) - 1
|
|
if member_index < 0 or member_index >= len(matching_members):
|
|
print(f"{TableColors.ERROR}❌ Invalid selection{TableColors.RESET}")
|
|
return
|
|
|
|
selected_member = matching_members[member_index]
|
|
except (KeyboardInterrupt, EOFError):
|
|
print(f"\n{TableColors.WARNING}🛑 Operation cancelled{TableColors.RESET}")
|
|
return
|
|
|
|
# Check if member is active
|
|
if not selected_member.IsActive:
|
|
print(f"\n{TableColors.WARNING}⚠️ Warning: {selected_member.FirstName} {selected_member.LastName} is marked as inactive{TableColors.RESET}")
|
|
try:
|
|
confirm = input(f"{TableColors.INPUT_BOX}Continue anyway? (y/N) {TableColors.RESET}").strip().lower()
|
|
if confirm not in ['y', 'yes']:
|
|
print(f"{TableColors.WARNING}Scheduling cancelled{TableColors.RESET}")
|
|
return
|
|
except (KeyboardInterrupt, EOFError):
|
|
print(f"\n{TableColors.WARNING}🛑 Operation cancelled{TableColors.RESET}")
|
|
return
|
|
|
|
# Get member's classification
|
|
if not selected_member.ClassificationId:
|
|
print(f"{TableColors.ERROR}❌ {selected_member.FirstName} {selected_member.LastName} has no classification assigned{TableColors.RESET}")
|
|
return
|
|
|
|
member_classification = cli.classification_repo.get_by_id(selected_member.ClassificationId)
|
|
if not member_classification:
|
|
print(f"{TableColors.ERROR}❌ Could not find classification for {selected_member.FirstName} {selected_member.LastName}{TableColors.RESET}")
|
|
return
|
|
|
|
classification_names = [member_classification.ClassificationName]
|
|
|
|
# Check service availability
|
|
if not cli.availability_repo.get(selected_member.MemberId, service.ServiceTypeId):
|
|
print(f"{TableColors.ERROR}❌ {selected_member.FirstName} {selected_member.LastName} is not available for {service_type_name} services{TableColors.RESET}")
|
|
return
|
|
|
|
# Check for existing schedules on the same date
|
|
if cli.schedule_repo.has_schedule_on_date(selected_member.MemberId, str(service.ServiceDate)):
|
|
print(f"{TableColors.ERROR}❌ {selected_member.FirstName} {selected_member.LastName} already has a schedule on {service.ServiceDate}{TableColors.RESET}")
|
|
return
|
|
|
|
# Check for existing schedule for this specific service
|
|
existing_schedule = cli.schedule_repo.get_one(member_id=selected_member.MemberId, service_id=service.ServiceId)
|
|
if existing_schedule:
|
|
status_text = existing_schedule.Status.upper()
|
|
print(f"{TableColors.ERROR}❌ {selected_member.FirstName} {selected_member.LastName} already has a {status_text} schedule for this service{TableColors.RESET}")
|
|
return
|
|
|
|
# Show confirmation
|
|
print(f"\n{TableColors.HEADER}Scheduling Confirmation{TableColors.RESET}")
|
|
print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}")
|
|
print(f" {TableColors.BOLD}Member:{TableColors.RESET} {selected_member.FirstName} {selected_member.LastName}")
|
|
print(f" {TableColors.BOLD}Service:{TableColors.RESET} {service_type_name} on {service.ServiceDate}")
|
|
print(f" {TableColors.BOLD}Classifications:{TableColors.RESET} {', '.join(classification_names)}")
|
|
print()
|
|
|
|
try:
|
|
print(f"\n{TableColors.INPUT_BOX}┌─ Create this schedule? (Y/n) ─┐{TableColors.RESET}")
|
|
confirm = input(f"{TableColors.INPUT_BOX}└─> {TableColors.RESET}").strip().lower()
|
|
if confirm in ['n', 'no']:
|
|
print(f"{TableColors.WARNING}Scheduling cancelled{TableColors.RESET}")
|
|
return
|
|
except (KeyboardInterrupt, EOFError):
|
|
print(f"\n{TableColors.WARNING}🛑 Operation cancelled{TableColors.RESET}")
|
|
return
|
|
|
|
# Create the schedule
|
|
try:
|
|
from backend.models.enums import ScheduleStatus
|
|
|
|
schedule = cli.schedule_repo.create(
|
|
service_id=service.ServiceId,
|
|
member_id=selected_member.MemberId,
|
|
status=ScheduleStatus.PENDING,
|
|
)
|
|
|
|
# Update the member's LastScheduledAt timestamp
|
|
cli.member_repo.touch_last_scheduled(selected_member.MemberId)
|
|
|
|
print(f"\n{TableColors.SUCCESS}✅ Successfully scheduled {selected_member.FirstName} {selected_member.LastName}!{TableColors.RESET}")
|
|
print(f"{TableColors.DIM}Schedule ID: {schedule.ScheduleId}{TableColors.RESET}")
|
|
print(f"{TableColors.DIM}Status: Pending (awaiting member response){TableColors.RESET}")
|
|
|
|
except Exception as e:
|
|
print(f"{TableColors.ERROR}❌ Failed to create schedule: {e}{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 or by name)")
|
|
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)")
|
|
schedules_schedule_parser.add_argument("--member-name", type=str, help="Schedule a specific member by name (first, last, or both)") |