feat(cli): improve member design and usability
This commit is contained in:
@@ -5,7 +5,7 @@ CLI command modules.
|
|||||||
from .members import cmd_members_list, cmd_members_show, setup_members_parser
|
from .members import cmd_members_list, cmd_members_show, setup_members_parser
|
||||||
from .schedules import (
|
from .schedules import (
|
||||||
cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept,
|
cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept,
|
||||||
cmd_schedules_decline, cmd_schedules_schedule, setup_schedules_parser
|
cmd_schedules_decline, cmd_schedules_remove, cmd_schedules_schedule, setup_schedules_parser
|
||||||
)
|
)
|
||||||
from .services import cmd_services_list, setup_services_parser
|
from .services import cmd_services_list, setup_services_parser
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ __all__ = [
|
|||||||
"cmd_members_list", "cmd_members_show", "setup_members_parser",
|
"cmd_members_list", "cmd_members_show", "setup_members_parser",
|
||||||
# Schedule commands
|
# Schedule commands
|
||||||
"cmd_schedules_list", "cmd_schedules_show", "cmd_schedules_accept",
|
"cmd_schedules_list", "cmd_schedules_show", "cmd_schedules_accept",
|
||||||
"cmd_schedules_decline", "cmd_schedules_schedule", "setup_schedules_parser",
|
"cmd_schedules_decline", "cmd_schedules_remove", "cmd_schedules_schedule", "setup_schedules_parser",
|
||||||
# Service commands
|
# Service commands
|
||||||
"cmd_services_list", "setup_services_parser",
|
"cmd_services_list", "setup_services_parser",
|
||||||
]
|
]
|
||||||
@@ -21,6 +21,17 @@ def cmd_schedules_list(cli: "NimbusFlowCLI", args) -> None:
|
|||||||
if args.service_id:
|
if args.service_id:
|
||||||
schedules = [s for s in schedules if s.ServiceId == 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:
|
if args.status:
|
||||||
try:
|
try:
|
||||||
status_enum = ScheduleStatus.from_raw(args.status.lower())
|
status_enum = ScheduleStatus.from_raw(args.status.lower())
|
||||||
@@ -128,15 +139,18 @@ def cmd_schedules_accept(cli: "NimbusFlowCLI", args) -> None:
|
|||||||
service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()}
|
service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()}
|
||||||
|
|
||||||
# Show available services for the date
|
# Show available services for the date
|
||||||
print(f"\n📅 Services available on {args.date}:")
|
print(f"\n{TableColors.HEADER}Services available on {args.date}{TableColors.RESET}")
|
||||||
print("-" * 40)
|
print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}")
|
||||||
|
print()
|
||||||
for i, service in enumerate(services_on_date, 1):
|
for i, service in enumerate(services_on_date, 1):
|
||||||
type_name = service_type_map.get(service.ServiceTypeId, "Unknown")
|
type_name = service_type_map.get(service.ServiceTypeId, "Unknown")
|
||||||
print(f"{i}. {type_name} (Service ID: {service.ServiceId})")
|
print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {type_name}")
|
||||||
|
print()
|
||||||
|
|
||||||
# Let user select service
|
# Let user select service
|
||||||
try:
|
try:
|
||||||
choice = input(f"\nSelect service (1-{len(services_on_date)}): ").strip()
|
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():
|
if not choice or not choice.isdigit():
|
||||||
print("❌ Invalid selection")
|
print("❌ Invalid selection")
|
||||||
return
|
return
|
||||||
@@ -167,72 +181,321 @@ def cmd_schedules_accept(cli: "NimbusFlowCLI", args) -> None:
|
|||||||
member_map = {m.MemberId: m for m in cli.member_repo.list_all()}
|
member_map = {m.MemberId: m for m in cli.member_repo.list_all()}
|
||||||
|
|
||||||
# Show available members to accept
|
# Show available members to accept
|
||||||
print(f"\n👥 Members scheduled for {service_type_map.get(selected_service.ServiceTypeId, 'Unknown')} on {args.date}:")
|
print(f"\n{TableColors.HEADER}Members scheduled for {service_type_map.get(selected_service.ServiceTypeId, 'Unknown')} on {args.date}:{TableColors.RESET}")
|
||||||
print("-" * 60)
|
print(f"{TableColors.BORDER}{'─' * 70}{TableColors.RESET}")
|
||||||
for i, schedule in enumerate(pending_schedules, 1):
|
for i, schedule in enumerate(pending_schedules, 1):
|
||||||
member = member_map.get(schedule.MemberId)
|
member = member_map.get(schedule.MemberId)
|
||||||
if member:
|
if member:
|
||||||
print(f"{i}. {member.FirstName} {member.LastName} (Schedule ID: {schedule.ScheduleId})")
|
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:
|
else:
|
||||||
print(f"{i}. Unknown Member (ID: {schedule.MemberId}) (Schedule ID: {schedule.ScheduleId})")
|
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")
|
||||||
|
|
||||||
# Let user select member
|
|
||||||
try:
|
try:
|
||||||
choice = input(f"\nSelect member to accept (1-{len(pending_schedules)}): ").strip()
|
choice = input(f"\n{TableColors.INPUT_BOX}┌─ Select members to accept ─┐{TableColors.RESET}\n{TableColors.INPUT_BOX}└─> {TableColors.RESET}").strip()
|
||||||
if not choice or not choice.isdigit():
|
if not choice:
|
||||||
print("❌ Invalid selection")
|
print(f"{TableColors.ERROR}❌ No selection made{TableColors.RESET}")
|
||||||
return
|
return
|
||||||
|
|
||||||
member_index = int(choice) - 1
|
selected_indices = []
|
||||||
if member_index < 0 or member_index >= len(pending_schedules):
|
|
||||||
print("❌ Invalid selection")
|
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
|
return
|
||||||
|
|
||||||
selected_schedule = pending_schedules[member_index]
|
schedules_to_accept = [pending_schedules[i] for i in selected_indices]
|
||||||
except (KeyboardInterrupt, EOFError):
|
except (KeyboardInterrupt, EOFError):
|
||||||
print("\n🛑 Cancelled")
|
print(f"\n{TableColors.WARNING}🛑 Cancelled{TableColors.RESET}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Accept the selected schedule
|
|
||||||
schedule_to_accept = selected_schedule
|
|
||||||
|
|
||||||
# Direct mode with schedule ID
|
# Direct mode with schedule ID (single schedule)
|
||||||
elif hasattr(args, 'schedule_id') and args.schedule_id:
|
elif hasattr(args, 'schedule_id') and args.schedule_id:
|
||||||
schedule_to_accept = cli.schedule_repo.get_by_id(args.schedule_id)
|
schedule_to_accept = cli.schedule_repo.get_by_id(args.schedule_id)
|
||||||
if not schedule_to_accept:
|
if not schedule_to_accept:
|
||||||
print(f"❌ Schedule with ID {args.schedule_id} not found")
|
print(f"{TableColors.ERROR}❌ Schedule with ID {args.schedule_id} not found{TableColors.RESET}")
|
||||||
return
|
return
|
||||||
|
schedules_to_accept = [schedule_to_accept]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("❌ Either --date or schedule_id must be provided")
|
print(f"{TableColors.ERROR}❌ Either --date or schedule_id must be provided{TableColors.RESET}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Common validation and acceptance logic
|
# Process multiple schedules
|
||||||
if schedule_to_accept.Status == ScheduleStatus.ACCEPTED.value:
|
successful_accepts = []
|
||||||
print(f"⚠️ Schedule {schedule_to_accept.ScheduleId} is already accepted")
|
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}{'─' * 40}{TableColors.RESET}")
|
||||||
|
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} {TableColors.DIM}(Service ID: {service.ServiceId}){TableColors.RESET}")
|
||||||
|
|
||||||
|
# Let user select service
|
||||||
|
try:
|
||||||
|
choice = input(f"\n{TableColors.INPUT_BOX}┌─ Select service (1-{len(services_on_date)}) ─┐{TableColors.RESET}\n{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
|
||||||
|
|
||||||
|
# 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
|
return
|
||||||
|
|
||||||
if schedule_to_accept.Status == ScheduleStatus.DECLINED.value:
|
# Process schedule removals
|
||||||
print(f"⚠️ Schedule {schedule_to_accept.ScheduleId} was previously declined")
|
successful_removals = []
|
||||||
return
|
failed_removals = []
|
||||||
|
|
||||||
# Get member and service info for display
|
print(f"\n{TableColors.WARNING}Processing {len(schedules_to_remove)} schedule removal(s)...{TableColors.RESET}")
|
||||||
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
|
for schedule in schedules_to_remove:
|
||||||
cli.schedule_repo.mark_accepted(schedule_to_accept.ScheduleId)
|
# 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}"))
|
||||||
|
|
||||||
# Update member's acceptance timestamp
|
# Display results
|
||||||
cli.member_repo.set_last_accepted(schedule_to_accept.MemberId)
|
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}")
|
||||||
|
|
||||||
print(f"✅ Schedule {schedule_to_accept.ScheduleId} accepted successfully!")
|
if failed_removals:
|
||||||
if member and service and service_type:
|
print(f"\n{TableColors.ERROR}❌ Could not remove {len(failed_removals)} schedule(s):{TableColors.RESET}")
|
||||||
print(f" Member: {member.FirstName} {member.LastName}")
|
for schedule, reason in failed_removals:
|
||||||
print(f" Service: {service_type.TypeName} on {service.ServiceDate}")
|
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:
|
def cmd_schedules_decline(cli: "NimbusFlowCLI", args) -> None:
|
||||||
@@ -257,15 +520,18 @@ def cmd_schedules_decline(cli: "NimbusFlowCLI", args) -> None:
|
|||||||
service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()}
|
service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()}
|
||||||
|
|
||||||
# Show available services for the date
|
# Show available services for the date
|
||||||
print(f"\n📅 Services available on {args.date}:")
|
print(f"\n{TableColors.HEADER}Services available on {args.date}{TableColors.RESET}")
|
||||||
print("-" * 40)
|
print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}")
|
||||||
|
print()
|
||||||
for i, service in enumerate(services_on_date, 1):
|
for i, service in enumerate(services_on_date, 1):
|
||||||
type_name = service_type_map.get(service.ServiceTypeId, "Unknown")
|
type_name = service_type_map.get(service.ServiceTypeId, "Unknown")
|
||||||
print(f"{i}. {type_name} (Service ID: {service.ServiceId})")
|
print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {type_name}")
|
||||||
|
print()
|
||||||
|
|
||||||
# Let user select service
|
# Let user select service
|
||||||
try:
|
try:
|
||||||
choice = input(f"\nSelect service (1-{len(services_on_date)}): ").strip()
|
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():
|
if not choice or not choice.isdigit():
|
||||||
print("❌ Invalid selection")
|
print("❌ Invalid selection")
|
||||||
return
|
return
|
||||||
@@ -405,15 +671,18 @@ def cmd_schedules_schedule(cli: "NimbusFlowCLI", args) -> None:
|
|||||||
service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()}
|
service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()}
|
||||||
|
|
||||||
# Show available services for the date
|
# Show available services for the date
|
||||||
print(f"\n📅 Services available on {args.date}:")
|
print(f"\n{TableColors.HEADER}Services available on {args.date}{TableColors.RESET}")
|
||||||
print("-" * 50)
|
print(f"{TableColors.BORDER}─" * 50 + f"{TableColors.RESET}")
|
||||||
|
print()
|
||||||
for i, svc in enumerate(services_on_date, 1):
|
for i, svc in enumerate(services_on_date, 1):
|
||||||
type_name = service_type_map.get(svc.ServiceTypeId, "Unknown")
|
type_name = service_type_map.get(svc.ServiceTypeId, "Unknown")
|
||||||
print(f"{i}. {type_name} (Service ID: {svc.ServiceId})")
|
print(f" {TableColors.CYAN}{i}.{TableColors.RESET} {type_name}")
|
||||||
|
print()
|
||||||
|
|
||||||
# Let user select service
|
# Let user select service
|
||||||
try:
|
try:
|
||||||
choice = input(f"\nSelect service (1-{len(services_on_date)}): ").strip()
|
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():
|
if not choice or not choice.isdigit():
|
||||||
print("❌ Invalid selection")
|
print("❌ Invalid selection")
|
||||||
return
|
return
|
||||||
@@ -577,6 +846,11 @@ def setup_schedules_parser(subparsers) -> None:
|
|||||||
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("--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_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
|
||||||
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)")
|
||||||
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("service_id", type=int, nargs="?", help="Service ID to schedule for (optional if using --date)")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
from .commands import (
|
from .commands import (
|
||||||
cmd_members_list, cmd_members_show,
|
cmd_members_list, cmd_members_show,
|
||||||
cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept, cmd_schedules_decline, cmd_schedules_schedule,
|
cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept, cmd_schedules_decline, cmd_schedules_remove, cmd_schedules_schedule,
|
||||||
cmd_services_list,
|
cmd_services_list,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -173,15 +173,12 @@ def display_schedules_menu():
|
|||||||
print(f"\n{Colors.HEADER}Schedules{Colors.RESET}")
|
print(f"\n{Colors.HEADER}Schedules{Colors.RESET}")
|
||||||
print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}")
|
print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}")
|
||||||
print()
|
print()
|
||||||
print(f" {Colors.CYAN}1.{Colors.RESET} List all schedules")
|
print(f" {Colors.CYAN}1.{Colors.RESET} Browse schedules")
|
||||||
print(f" {Colors.CYAN}2.{Colors.RESET} List pending schedules")
|
print(f" {Colors.CYAN}2.{Colors.RESET} {Colors.GREEN}Accept a schedule{Colors.RESET}")
|
||||||
print(f" {Colors.CYAN}3.{Colors.RESET} List accepted schedules")
|
print(f" {Colors.CYAN}3.{Colors.RESET} {Colors.RED}Decline a schedule{Colors.RESET}")
|
||||||
print(f" {Colors.CYAN}4.{Colors.RESET} List declined schedules")
|
print(f" {Colors.CYAN}4.{Colors.RESET} {Colors.ERROR}Remove scheduled members{Colors.RESET}")
|
||||||
print(f" {Colors.CYAN}5.{Colors.RESET} Show schedule details")
|
print(f" {Colors.CYAN}5.{Colors.RESET} {Colors.YELLOW}Schedule next member for service{Colors.RESET}")
|
||||||
print(f" {Colors.CYAN}6.{Colors.RESET} {Colors.GREEN}Accept a schedule{Colors.RESET}")
|
print(f" {Colors.CYAN}6.{Colors.RESET} {Colors.DIM}Back to main menu{Colors.RESET}")
|
||||||
print(f" {Colors.CYAN}7.{Colors.RESET} {Colors.RED}Decline a schedule{Colors.RESET}")
|
|
||||||
print(f" {Colors.CYAN}8.{Colors.RESET} {Colors.YELLOW}Schedule next member for service{Colors.RESET}")
|
|
||||||
print(f" {Colors.CYAN}9.{Colors.RESET} {Colors.DIM}Back to main menu{Colors.RESET}")
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
@@ -251,6 +248,25 @@ def get_date_input(prompt: str = "Enter date (YYYY-MM-DD)") -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_input_optional(prompt: str = "Enter date (YYYY-MM-DD)") -> str:
|
||||||
|
"""Get optional date input from user (allows empty input)."""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
print(create_simple_input_box(prompt))
|
||||||
|
date_str = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip()
|
||||||
|
if not date_str:
|
||||||
|
return "" # Allow empty input
|
||||||
|
# Basic date format validation
|
||||||
|
if len(date_str) == 10 and date_str.count('-') == 2:
|
||||||
|
parts = date_str.split('-')
|
||||||
|
if len(parts[0]) == 4 and len(parts[1]) == 2 and len(parts[2]) == 2:
|
||||||
|
return date_str
|
||||||
|
print(f"{Colors.ERROR}Please use format YYYY-MM-DD (e.g., 2025-09-07){Colors.RESET}")
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
class MockArgs:
|
class MockArgs:
|
||||||
"""Mock args object for interactive commands."""
|
"""Mock args object for interactive commands."""
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -306,73 +322,105 @@ def handle_schedules_menu(cli: "NimbusFlowCLI"):
|
|||||||
while True:
|
while True:
|
||||||
clear_screen()
|
clear_screen()
|
||||||
display_schedules_menu()
|
display_schedules_menu()
|
||||||
choice = get_user_choice(9)
|
choice = get_user_choice(6)
|
||||||
|
|
||||||
if choice == 1: # List all schedules
|
if choice == 1: # List all schedules
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print(f"{Colors.SUCCESS}Listing all schedules...{Colors.RESET}\n")
|
|
||||||
cmd_schedules_list(cli, MockArgs(service_id=None, status=None))
|
|
||||||
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
|
||||||
|
|
||||||
elif choice == 2: # List pending schedules
|
# Get date filter
|
||||||
clear_screen()
|
date = get_date_input_optional("Enter date to filter schedules (or press Enter to skip)")
|
||||||
print(f"{Colors.WARNING}Listing pending schedules...{Colors.RESET}\n")
|
if not date:
|
||||||
cmd_schedules_list(cli, MockArgs(service_id=None, status="pending"))
|
|
||||||
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
|
||||||
|
|
||||||
elif choice == 3: # List accepted schedules
|
|
||||||
clear_screen()
|
|
||||||
print(f"{Colors.SUCCESS}Listing accepted schedules...{Colors.RESET}\n")
|
|
||||||
cmd_schedules_list(cli, MockArgs(service_id=None, status="accepted"))
|
|
||||||
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
|
||||||
|
|
||||||
elif choice == 4: # List declined schedules
|
|
||||||
clear_screen()
|
|
||||||
print(f"{Colors.ERROR}Listing declined schedules...{Colors.RESET}\n")
|
|
||||||
cmd_schedules_list(cli, MockArgs(service_id=None, status="declined"))
|
|
||||||
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
|
||||||
|
|
||||||
elif choice == 5: # Show schedule details
|
|
||||||
clear_screen()
|
|
||||||
schedule_id = get_text_input("Enter schedule ID", True)
|
|
||||||
if schedule_id.isdigit():
|
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print(f"{Colors.SUCCESS}Showing details for schedule {schedule_id}...{Colors.RESET}\n")
|
cmd_schedules_list(cli, MockArgs(service_id=None, status=None))
|
||||||
cmd_schedules_show(cli, MockArgs(schedule_id=int(schedule_id)))
|
|
||||||
else:
|
else:
|
||||||
print(f"{Colors.ERROR}Invalid schedule ID{Colors.RESET}")
|
# Find services for the date
|
||||||
|
try:
|
||||||
|
from datetime import date as date_type
|
||||||
|
target_date = date_type.fromisoformat(date)
|
||||||
|
all_services = cli.service_repo.list_all()
|
||||||
|
services_on_date = [s for s in all_services if s.ServiceDate == target_date]
|
||||||
|
except ValueError:
|
||||||
|
clear_screen()
|
||||||
|
print(f"{Colors.ERROR}Invalid date format. Please use YYYY-MM-DD format.{Colors.RESET}")
|
||||||
|
services_on_date = []
|
||||||
|
|
||||||
|
if not services_on_date:
|
||||||
|
clear_screen()
|
||||||
|
print(f"{Colors.ERROR}No services found for {date}{Colors.RESET}")
|
||||||
|
else:
|
||||||
|
clear_screen()
|
||||||
|
|
||||||
|
# Show available services for selection
|
||||||
|
service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()}
|
||||||
|
|
||||||
|
print(f"\n{Colors.HEADER}Services available on {date}{Colors.RESET}")
|
||||||
|
print(f"{Colors.GREY}─" * 50 + f"{Colors.RESET}")
|
||||||
|
print()
|
||||||
|
for i, service in enumerate(services_on_date, 1):
|
||||||
|
type_name = service_type_map.get(service.ServiceTypeId, "Unknown")
|
||||||
|
print(f" {Colors.CYAN}{i}.{Colors.RESET} {type_name}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Get service selection
|
||||||
|
try:
|
||||||
|
print(create_simple_input_box(f"Select service (1-{len(services_on_date)}) or press Enter to show all"))
|
||||||
|
choice_input = input(f"{Colors.INPUT_BOX}└─> {Colors.RESET}").strip()
|
||||||
|
|
||||||
|
if not choice_input:
|
||||||
|
# Empty input - show all services for this date
|
||||||
|
clear_screen()
|
||||||
|
cmd_schedules_list(cli, MockArgs(service_id=None, status=None, date=date))
|
||||||
|
elif not choice_input.isdigit():
|
||||||
|
print(f"{Colors.ERROR}Invalid selection{Colors.RESET}")
|
||||||
|
else:
|
||||||
|
service_choice = int(choice_input)
|
||||||
|
|
||||||
|
if service_choice < 1 or service_choice > len(services_on_date):
|
||||||
|
print(f"{Colors.ERROR}Please enter a number between 1 and {len(services_on_date)}{Colors.RESET}")
|
||||||
|
else:
|
||||||
|
clear_screen()
|
||||||
|
selected_service = services_on_date[service_choice - 1]
|
||||||
|
cmd_schedules_list(cli, MockArgs(service_id=selected_service.ServiceId, status=None))
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
print(f"\n{Colors.WARNING}Operation cancelled{Colors.RESET}")
|
||||||
|
|
||||||
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||||||
|
|
||||||
elif choice == 6: # Accept schedule
|
elif choice == 2: # Accept schedule
|
||||||
clear_screen()
|
clear_screen()
|
||||||
date = get_date_input("Enter date for interactive accept")
|
date = get_date_input("Enter date for interactive accept")
|
||||||
if date:
|
if date:
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print(f"{Colors.SUCCESS}Starting interactive accept for {date}...{Colors.RESET}\n")
|
|
||||||
cmd_schedules_accept(cli, MockArgs(date=date, schedule_id=None))
|
cmd_schedules_accept(cli, MockArgs(date=date, schedule_id=None))
|
||||||
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||||||
|
|
||||||
elif choice == 7: # Decline schedule
|
elif choice == 3: # Decline schedule
|
||||||
clear_screen()
|
clear_screen()
|
||||||
date = get_date_input("Enter date for interactive decline")
|
date = get_date_input("Enter date for interactive decline")
|
||||||
if date:
|
if date:
|
||||||
clear_screen()
|
clear_screen()
|
||||||
reason = get_text_input("Enter decline reason (optional)", False)
|
reason = get_text_input("Enter decline reason (optional)", False)
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print(f"{Colors.ERROR}Starting interactive decline for {date}...{Colors.RESET}\n")
|
|
||||||
cmd_schedules_decline(cli, MockArgs(date=date, schedule_id=None, reason=reason))
|
cmd_schedules_decline(cli, MockArgs(date=date, schedule_id=None, reason=reason))
|
||||||
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||||||
|
|
||||||
elif choice == 8: # Schedule next member
|
elif choice == 4: # Remove scheduled members
|
||||||
|
clear_screen()
|
||||||
|
date = get_date_input("Enter date to remove schedules for")
|
||||||
|
if date:
|
||||||
|
clear_screen()
|
||||||
|
cmd_schedules_remove(cli, MockArgs(date=date, schedule_id=None))
|
||||||
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||||||
|
|
||||||
|
elif choice == 5: # Schedule next member
|
||||||
clear_screen()
|
clear_screen()
|
||||||
date = get_date_input("Enter date to schedule for")
|
date = get_date_input("Enter date to schedule for")
|
||||||
if date:
|
if date:
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print(f"{Colors.WARNING}Starting scheduling for {date}...{Colors.RESET}\n")
|
|
||||||
cmd_schedules_schedule(cli, MockArgs(service_id=None, date=date, classifications=None))
|
cmd_schedules_schedule(cli, MockArgs(service_id=None, date=date, classifications=None))
|
||||||
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
|
||||||
|
|
||||||
elif choice == 9: # Back to main menu
|
elif choice == 6: # Back to main menu
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from .commands import (
|
|||||||
cmd_members_list, cmd_members_show, setup_members_parser,
|
cmd_members_list, cmd_members_show, setup_members_parser,
|
||||||
# Schedule commands
|
# Schedule commands
|
||||||
cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept,
|
cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept,
|
||||||
cmd_schedules_decline, cmd_schedules_schedule, setup_schedules_parser,
|
cmd_schedules_decline, cmd_schedules_remove, cmd_schedules_schedule, setup_schedules_parser,
|
||||||
# Service commands
|
# Service commands
|
||||||
cmd_services_list, setup_services_parser,
|
cmd_services_list, setup_services_parser,
|
||||||
)
|
)
|
||||||
@@ -99,10 +99,12 @@ def main():
|
|||||||
cmd_schedules_accept(cli, args)
|
cmd_schedules_accept(cli, args)
|
||||||
elif args.schedules_action == "decline":
|
elif args.schedules_action == "decline":
|
||||||
cmd_schedules_decline(cli, args)
|
cmd_schedules_decline(cli, args)
|
||||||
|
elif args.schedules_action == "remove":
|
||||||
|
cmd_schedules_remove(cli, args)
|
||||||
elif args.schedules_action == "schedule":
|
elif args.schedules_action == "schedule":
|
||||||
cmd_schedules_schedule(cli, args)
|
cmd_schedules_schedule(cli, args)
|
||||||
else:
|
else:
|
||||||
print(f"{Colors.ERROR}❌ Unknown schedules action. Use 'list', 'show', 'accept', 'decline', or 'schedule'{Colors.RESET}")
|
print(f"{Colors.ERROR}❌ Unknown schedules action. Use 'list', 'show', 'accept', 'decline', 'remove', or 'schedule'{Colors.RESET}")
|
||||||
|
|
||||||
elif args.command == "services":
|
elif args.command == "services":
|
||||||
if args.services_action == "list":
|
if args.services_action == "list":
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class TableColors:
|
|||||||
ERROR = '\033[1m\033[91m' # Bold Red
|
ERROR = '\033[1m\033[91m' # Bold Red
|
||||||
WARNING = '\033[1m\033[93m' # Bold Yellow
|
WARNING = '\033[1m\033[93m' # Bold Yellow
|
||||||
BORDER = '\033[90m' # Grey
|
BORDER = '\033[90m' # Grey
|
||||||
|
INPUT_BOX = '\033[90m' # Grey (for input styling)
|
||||||
|
|
||||||
|
|
||||||
def format_member_row(member, classification_name: Optional[str] = None) -> str:
|
def format_member_row(member, classification_name: Optional[str] = None) -> str:
|
||||||
|
|||||||
@@ -234,4 +234,20 @@ class MemberRepository(BaseRepository[MemberModel]):
|
|||||||
DeclineStreak = COALESCE(DeclineStreak, 0) + 1
|
DeclineStreak = COALESCE(DeclineStreak, 0) + 1
|
||||||
WHERE {self._PK} = ?
|
WHERE {self._PK} = ?
|
||||||
"""
|
"""
|
||||||
self.db.execute(sql, (decline_date, member_id))
|
self.db.execute(sql, (decline_date, member_id))
|
||||||
|
|
||||||
|
def reset_to_queue_front(self, member_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Reset member timestamps to move them to the front of the round robin queue.
|
||||||
|
This sets LastScheduledAt and LastAcceptedAt to far past values, effectively
|
||||||
|
making them the highest priority for scheduling.
|
||||||
|
"""
|
||||||
|
sql = f"""
|
||||||
|
UPDATE {self._TABLE}
|
||||||
|
SET LastScheduledAt = '1970-01-01 00:00:00',
|
||||||
|
LastAcceptedAt = '1970-01-01 00:00:00',
|
||||||
|
LastDeclinedAt = NULL,
|
||||||
|
DeclineStreak = 0
|
||||||
|
WHERE {self._PK} = ?
|
||||||
|
"""
|
||||||
|
self.db.execute(sql, (member_id,))
|
||||||
@@ -257,8 +257,14 @@ class ScheduleRepository(BaseRepository[ScheduleModel]):
|
|||||||
"""
|
"""
|
||||||
rows = self.db.fetchall(sql, (service_id, ScheduleStatus.PENDING.value))
|
rows = self.db.fetchall(sql, (service_id, ScheduleStatus.PENDING.value))
|
||||||
return [ScheduleModel.from_row(r) for r in rows]
|
return [ScheduleModel.from_row(r) for r in rows]
|
||||||
|
|
||||||
def delete(self, schedule_id: int) -> None:
|
def delete_schedule(self, schedule_id: int) -> bool:
|
||||||
"""Hard‑delete a schedule row (use with caution)."""
|
"""
|
||||||
|
Delete a schedule by ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if a schedule was deleted, False if not found
|
||||||
|
"""
|
||||||
sql = f"DELETE FROM {self._TABLE} WHERE {self._PK} = ?"
|
sql = f"DELETE FROM {self._TABLE} WHERE {self._PK} = ?"
|
||||||
self.db.execute(sql, (schedule_id,))
|
cursor = self.db.execute(sql, (schedule_id,))
|
||||||
|
return cursor.rowcount > 0
|
||||||
Reference in New Issue
Block a user