384 lines
16 KiB
Python
384 lines
16 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
|
|
|
|
|
|
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 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") |