|
|
|
|
@@ -558,16 +558,16 @@ def cmd_schedules_decline(cli: "NimbusFlowCLI", args) -> None:
|
|
|
|
|
# Clear screen after service selection
|
|
|
|
|
print("\033[2J\033[H")
|
|
|
|
|
|
|
|
|
|
# Find pending schedules for this service
|
|
|
|
|
# Find pending OR accepted schedules for this service
|
|
|
|
|
all_schedules = cli.schedule_repo.list_all()
|
|
|
|
|
pending_schedules = [
|
|
|
|
|
available_schedules = [
|
|
|
|
|
s for s in all_schedules
|
|
|
|
|
if s.ServiceId == selected_service.ServiceId and s.Status == ScheduleStatus.PENDING.value
|
|
|
|
|
if s.ServiceId == selected_service.ServiceId and s.Status in [ScheduleStatus.PENDING.value, ScheduleStatus.ACCEPTED.value]
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if not pending_schedules:
|
|
|
|
|
if not available_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}")
|
|
|
|
|
print(f"❌ No pending or accepted schedules found for {service_type_name} on {args.date}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Get member info for display
|
|
|
|
|
@@ -578,28 +578,31 @@ def cmd_schedules_decline(cli: "NimbusFlowCLI", args) -> None:
|
|
|
|
|
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):
|
|
|
|
|
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}")
|
|
|
|
|
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}){TableColors.RESET}")
|
|
|
|
|
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(pending_schedules)}) ─┐{TableColors.RESET}")
|
|
|
|
|
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(pending_schedules):
|
|
|
|
|
if member_index < 0 or member_index >= len(available_schedules):
|
|
|
|
|
print("❌ Invalid selection")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
selected_schedule = pending_schedules[member_index]
|
|
|
|
|
selected_schedule = available_schedules[member_index]
|
|
|
|
|
except (KeyboardInterrupt, EOFError):
|
|
|
|
|
print("\n🛑 Cancelled")
|
|
|
|
|
return
|
|
|
|
|
@@ -635,10 +638,6 @@ def cmd_schedules_decline(cli: "NimbusFlowCLI", args) -> None:
|
|
|
|
|
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)
|
|
|
|
|
@@ -646,19 +645,37 @@ def cmd_schedules_decline(cli: "NimbusFlowCLI", args) -> 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)
|
|
|
|
|
# Show what we're about to decline
|
|
|
|
|
was_accepted = schedule_to_decline.Status == ScheduleStatus.ACCEPTED.value
|
|
|
|
|
status_text = "accepted" if was_accepted else "pending"
|
|
|
|
|
|
|
|
|
|
# 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"\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}")
|
|
|
|
|
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:
|
|
|
|
|
@@ -734,6 +751,10 @@ def cmd_schedules_schedule(cli: "NimbusFlowCLI", args) -> None:
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
@@ -848,6 +869,137 @@ def cmd_schedules_schedule(cli: "NimbusFlowCLI", args) -> None:
|
|
|
|
|
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
|
|
|
|
|
@@ -880,7 +1032,8 @@ def setup_schedules_parser(subparsers) -> None:
|
|
|
|
|
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 = 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("--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)")
|