chore(backend): refactor mono cli file into package
This commit is contained in:
8
backend/cli/__init__.py
Normal file
8
backend/cli/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
NimbusFlow CLI module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .base import NimbusFlowCLI, CLIError
|
||||||
|
from .main import main
|
||||||
|
|
||||||
|
__all__ = ["NimbusFlowCLI", "CLIError", "main"]
|
||||||
17
backend/cli/__main__.py
Normal file
17
backend/cli/__main__.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"""
|
||||||
|
Main entry point for the CLI package.
|
||||||
|
Allows running with: python -m backend.cli
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add the project root to sys.path for backend imports
|
||||||
|
project_root = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
# Import the main function from within the package
|
||||||
|
from .main import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
82
backend/cli/ascii.txt
Normal file
82
backend/cli/ascii.txt
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
█████████████████████████████████████████████████████████████████████▓▓▒▒▒▒▒░░▒░▒▒░░░░░░░░░░░░░░░░▒▒▒▓▓███████████████████████████████████████████████
|
||||||
|
████████████████████████████████████████████████████████████▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒▒▒░░░▒░░░░░░░░░░░▒▒▓████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████▓▒▒▒▒▒▒▒▒▒▓▓▓████████████████████████████████▓▓▒░░░░░░░░░░▒▓████████████████████████████████████
|
||||||
|
█████████████████████████████████████████████████▓▒▒▒▒▓▓██████████████████████████████████████████████████▒░░░░░░░░░▓█████████████████████████████████
|
||||||
|
████████████████████████████████████████████▓▒▒▒▓████████████████████████████████████████████████████████████▓░░░░░░░░▓███████████████████████████████
|
||||||
|
████████████████████████████████████████▓▒▒█████████████████████████████████████████████████████████████████████▒▒░░░░░▒██████████████████████████████
|
||||||
|
█████████████████████████████████████▓▓████████████████████████████████▓▓▓▓▓█████████████████████████████████████▓▒░░░░░░▓████████████████████████████
|
||||||
|
███████████████████████████████████████████████████████████████████▒▒▒▒▒▒░░▒▒░▒▓███▓▓▓▓▓██████████████████████████▓▒░░░░░░▓███████████████████████████
|
||||||
|
██████████████████████████████████████████████████▓▓▒▒▒▓▓█████▓▓▓▒▒▒▒░░░░░░░░░░▒▒▒▒░░░▒▒▒▒▓████████████████████████▒░░░░░░▒███████████████████████████
|
||||||
|
███████████████████████████████████████████████▒▒▒░░░░░░░░▒▓▒▒▒▒░░░▒░░░░░░░░░░░░░░░░░░░░░░▒▒███████████████████████▒░░░░░▒▒███████████████████████████
|
||||||
|
█████████████████████████████████████████████▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒█████████████████████▒░░░░░░▒▒███████████████████████████
|
||||||
|
███████████████████████████████████████████▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒██████████████████▓░░░░░░░▒▒▓███████████████████████████
|
||||||
|
███████████████████████████████████████████▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒████████████████▓░░░░░░░░▒▒▒████████████████████████████
|
||||||
|
██████████████████████████████████████████▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒█████████████▓▒░░░░░░░░▒▒▒▒█████████████████████████████
|
||||||
|
████████████████████████████████████████▓▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▓███████▓▒░░░░░░░░░░▒▒▒▒▒██████████████████████████████
|
||||||
|
██████████████████████████████████████▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░░░▒▒▒▓▓▒░░░░░░░░░░░░░▒▒▒▒▒▓███████████████████████████████
|
||||||
|
█████████████████████████████████████▓▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░░░░░░░▒▒░░░░░░░░░░░░░▒▒▒▒▒▒▓█████████████████████████████████
|
||||||
|
████████████████████████████████████▒▒▒▒░░░░░░░░░░░░░▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░▒▒░░░░░░░░░░░░░▒▒▒░░░░░░░░▒▒▒▒▒▒▒▒▓███████████████████████████████████
|
||||||
|
██████████████████████████████████▓▒▒▒░░░░░░░░░░░░▒▒░░░░░░░░░░░▒░░░░░░░░░░░░░░░░▒▒░░░░░░░░░░░░▒░▒▒░░░░▒▒▒▒▒▒▒▒▒▓██████████████████████████████████████
|
||||||
|
█████████████████████████████████▒▒▒▒░░░░░░░░░░░▒▒░░░░░░░░░░░░░░░▒░░░░░░░░░░░░░▒▒░░░░░░░░▒░░▒▒░░░▒▒▒▒▒▒▒▒▒▒▒▓█████████████████████████████████████████
|
||||||
|
█████████████████████████████████▒▒▒░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░▒░░░░░░░░░░▒▒▒▒░░░░░░░░▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒█████████████████████████████████████████████
|
||||||
|
████████████████████████████████▒▒▒▒░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░░▒▒▒░░░░░░▒▒▒░░░░▒▒▒▒▒▓█████████████████████████████████████████████████
|
||||||
|
████████████████████████████████▒▒▒▒░░░░░░▒░░▒▒▒░░░░░░░▒░░░░░░░░░░░▒░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒░▒░░░▒▒▒████████████████████████████████████████████████████
|
||||||
|
████████████████████████████████▒▒▒▒░░░░░▒▒░▒▒▒▒░░░░░▒▒░░░░░░░░░░░░▒░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒░░▒░░▒▒▒▒████████████████████████████████████████████████████
|
||||||
|
█████████████████████████████████▒▒▒▒░░░░▒▒▒▒▒▒▒░░░░░▒░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒▒▒█████████████████████████████████████████████████████
|
||||||
|
█████████████████████████████████▓▒▒▒▒▒░░░▒▒▒▒▒▒▒░░░░▒▒░░░░░░░░░░░▒░░░░░░░░▒▒▒▒▒▒░░░░▒▒▒▒▒▒▒▒▒▒▒▓█████████████████████████████████████████████████████
|
||||||
|
███████████████████████████████████▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░▒▒▒░░░░░░░░▒▒▒▒▒▒▒▒▒░▒▒▒▒▒▒░░░░░░▒▒▒▒▒▒▒▒▓███████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████▒▒▒▒▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░▒▒▒▒▒▓██████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████▒▒▒░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░▒░░░░░░▒▒░░░░░░░░░░░▒▒▒█████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████▓▒▒▒░░░░▒▒▒▒░░░░░░░░░░░░░░░░░▒░░░░░░▒▒░░░░░░░░▒▒▒▒▓█████████████████████████████████████████████████████████████
|
||||||
|
████████████████████████████████████████▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░▒░░░░░░▒▒░░░░░░▒▒▒▒▒▓██████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████▓▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓███████████████████████████████████████████████████████████████
|
||||||
|
███████████████████████████████████████████████▓▒▒▒░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓█████████████████████████████████████████████████████████████████
|
||||||
|
████████████████████████████████████████████████▓▒▒▒▒░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▓███▓▓▓▓▓█████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒███████████████████████████████████████████████████████████████████████████████████
|
||||||
|
████████████████████████████████████████████████████▓▒▒▒▒▒▒▒▒▒▒▒▓█████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
|
██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████
|
||||||
46
backend/cli/base.py
Normal file
46
backend/cli/base.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
Base CLI class and utilities for NimbusFlow CLI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from backend.db.connection import DatabaseConnection
|
||||||
|
from backend.repositories import (
|
||||||
|
MemberRepository,
|
||||||
|
ClassificationRepository,
|
||||||
|
ServiceRepository,
|
||||||
|
ServiceAvailabilityRepository,
|
||||||
|
ScheduleRepository,
|
||||||
|
ServiceTypeRepository
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CLIError(Exception):
|
||||||
|
"""Custom exception for CLI-specific errors."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NimbusFlowCLI:
|
||||||
|
"""Main CLI application class."""
|
||||||
|
|
||||||
|
def __init__(self, db_path: str = "database6_accepts_and_declines.db"):
|
||||||
|
"""Initialize CLI with database connection."""
|
||||||
|
self.db_path = Path(__file__).parent.parent / db_path
|
||||||
|
if not self.db_path.exists():
|
||||||
|
raise CLIError(f"Database not found: {self.db_path}")
|
||||||
|
|
||||||
|
self.db = DatabaseConnection(self.db_path)
|
||||||
|
self._init_repositories()
|
||||||
|
|
||||||
|
def _init_repositories(self):
|
||||||
|
"""Initialize all repository instances."""
|
||||||
|
self.member_repo = MemberRepository(self.db)
|
||||||
|
self.classification_repo = ClassificationRepository(self.db)
|
||||||
|
self.service_repo = ServiceRepository(self.db)
|
||||||
|
self.availability_repo = ServiceAvailabilityRepository(self.db)
|
||||||
|
self.schedule_repo = ScheduleRepository(self.db)
|
||||||
|
self.service_type_repo = ServiceTypeRepository(self.db)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Clean up database connection."""
|
||||||
|
if hasattr(self, 'db'):
|
||||||
|
self.db.close()
|
||||||
20
backend/cli/commands/__init__.py
Normal file
20
backend/cli/commands/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
CLI command modules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .members import cmd_members_list, cmd_members_show, setup_members_parser
|
||||||
|
from .schedules import (
|
||||||
|
cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept,
|
||||||
|
cmd_schedules_decline, setup_schedules_parser
|
||||||
|
)
|
||||||
|
from .services import cmd_services_list, setup_services_parser
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Member commands
|
||||||
|
"cmd_members_list", "cmd_members_show", "setup_members_parser",
|
||||||
|
# Schedule commands
|
||||||
|
"cmd_schedules_list", "cmd_schedules_show", "cmd_schedules_accept",
|
||||||
|
"cmd_schedules_decline", "setup_schedules_parser",
|
||||||
|
# Service commands
|
||||||
|
"cmd_services_list", "setup_services_parser",
|
||||||
|
]
|
||||||
98
backend/cli/commands/members.py
Normal file
98
backend/cli/commands/members.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
Member-related CLI commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.cli.base import NimbusFlowCLI
|
||||||
|
|
||||||
|
from backend.cli.utils import format_member_row
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_members_list(cli: "NimbusFlowCLI", args) -> None:
|
||||||
|
"""List all members with optional filters."""
|
||||||
|
print("Listing members...")
|
||||||
|
|
||||||
|
# Get all classifications for lookup
|
||||||
|
classifications = cli.classification_repo.list_all()
|
||||||
|
classification_map = {c.ClassificationId: c.ClassificationName for c in classifications}
|
||||||
|
|
||||||
|
# Apply filters
|
||||||
|
if args.classification:
|
||||||
|
# Find classification ID by name
|
||||||
|
classification_id = None
|
||||||
|
for c in classifications:
|
||||||
|
if c.ClassificationName.lower() == args.classification.lower():
|
||||||
|
classification_id = c.ClassificationId
|
||||||
|
break
|
||||||
|
|
||||||
|
if classification_id is None:
|
||||||
|
print(f"❌ Classification '{args.classification}' not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
members = cli.member_repo.get_by_classification_ids([classification_id])
|
||||||
|
elif args.active:
|
||||||
|
members = cli.member_repo.get_active()
|
||||||
|
else:
|
||||||
|
members = cli.member_repo.list_all()
|
||||||
|
|
||||||
|
if not members:
|
||||||
|
print("No members found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Print header
|
||||||
|
print(f"\n{'ID':<3} | {'First Name':<12} | {'Last Name':<15} | {'Classification':<12} | {'Active':<6} | {'Email'}")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
# Print members
|
||||||
|
for member in members:
|
||||||
|
classification_name = classification_map.get(member.ClassificationId)
|
||||||
|
print(format_member_row(member, classification_name))
|
||||||
|
|
||||||
|
print(f"\nTotal: {len(members)} members")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_members_show(cli: "NimbusFlowCLI", args) -> None:
|
||||||
|
"""Show detailed information about a specific member."""
|
||||||
|
member = cli.member_repo.get_by_id(args.member_id)
|
||||||
|
if not member:
|
||||||
|
print(f"❌ Member with ID {args.member_id} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get classification name
|
||||||
|
classification = None
|
||||||
|
if member.ClassificationId:
|
||||||
|
classification = cli.classification_repo.get_by_id(member.ClassificationId)
|
||||||
|
|
||||||
|
print(f"\n📋 Member Details (ID: {member.MemberId})")
|
||||||
|
print("-" * 50)
|
||||||
|
print(f"Name: {member.FirstName} {member.LastName}")
|
||||||
|
print(f"Email: {member.Email or 'N/A'}")
|
||||||
|
print(f"Phone: {member.PhoneNumber or 'N/A'}")
|
||||||
|
print(f"Classification: {classification.ClassificationName if classification else 'N/A'}")
|
||||||
|
print(f"Active: {'Yes' if member.IsActive else 'No'}")
|
||||||
|
print(f"Notes: {member.Notes or 'N/A'}")
|
||||||
|
|
||||||
|
print(f"\n⏰ Schedule History:")
|
||||||
|
print(f"Last Scheduled: {member.LastScheduledAt or 'Never'}")
|
||||||
|
print(f"Last Accepted: {member.LastAcceptedAt or 'Never'}")
|
||||||
|
print(f"Last Declined: {member.LastDeclinedAt or 'Never'}")
|
||||||
|
print(f"Decline Streak: {member.DeclineStreak}")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_members_parser(subparsers) -> None:
|
||||||
|
"""Set up member-related command parsers."""
|
||||||
|
# Members commands
|
||||||
|
members_parser = subparsers.add_parser("members", help="Manage members")
|
||||||
|
members_subparsers = members_parser.add_subparsers(dest="members_action", help="Members actions")
|
||||||
|
|
||||||
|
# members list
|
||||||
|
members_list_parser = members_subparsers.add_parser("list", help="List members")
|
||||||
|
members_list_parser.add_argument("--active", action="store_true", help="Show only active members")
|
||||||
|
members_list_parser.add_argument("--classification", type=str, help="Filter by classification name")
|
||||||
|
|
||||||
|
# members show
|
||||||
|
members_show_parser = members_subparsers.add_parser("show", help="Show member details")
|
||||||
|
members_show_parser.add_argument("member_id", type=int, help="Member ID to show")
|
||||||
341
backend/cli.py → backend/cli/commands/schedules.py
Executable file → Normal file
341
backend/cli.py → backend/cli/commands/schedules.py
Executable file → Normal file
@@ -1,179 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
"""
|
||||||
NimbusFlow CLI Tool
|
Schedule-related CLI commands.
|
||||||
-------------------
|
|
||||||
Command-line interface for managing the scheduling system backend.
|
|
||||||
|
|
||||||
Usage (run from nimbusflow directory):
|
|
||||||
python -m backend.cli members list [--active] [--classification NAME]
|
|
||||||
python -m backend.cli members show <member_id>
|
|
||||||
python -m backend.cli schedules list [--service_id N] [--status STATUS]
|
|
||||||
python -m backend.cli schedules show <schedule_id>
|
|
||||||
python -m backend.cli schedules accept <schedule_id>
|
|
||||||
python -m backend.cli schedules accept --date YYYY-MM-DD
|
|
||||||
python -m backend.cli schedules decline <schedule_id> [--reason "reason"]
|
|
||||||
python -m backend.cli schedules decline --date YYYY-MM-DD [--reason "reason"]
|
|
||||||
python -m backend.cli services list [--date YYYY-MM-DD] [--upcoming] [--limit N]
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
from datetime import date
|
||||||
from pathlib import Path
|
from typing import TYPE_CHECKING
|
||||||
from typing import Optional, List, Any
|
|
||||||
from datetime import date, datetime
|
|
||||||
|
|
||||||
# Note: This CLI should be run as: python -m backend.cli
|
if TYPE_CHECKING:
|
||||||
# This ensures proper module resolution without sys.path manipulation
|
from backend.cli.base import NimbusFlowCLI
|
||||||
|
|
||||||
from backend.db import DatabaseConnection
|
|
||||||
from backend.repositories import (
|
|
||||||
MemberRepository,
|
|
||||||
ClassificationRepository,
|
|
||||||
ServiceRepository,
|
|
||||||
ServiceAvailabilityRepository,
|
|
||||||
ScheduleRepository,
|
|
||||||
ServiceTypeRepository
|
|
||||||
)
|
|
||||||
from backend.models.enums import ScheduleStatus
|
from backend.models.enums import ScheduleStatus
|
||||||
|
from backend.cli.utils import format_schedule_row
|
||||||
|
|
||||||
|
|
||||||
class CLIError(Exception):
|
def cmd_schedules_list(cli: "NimbusFlowCLI", args) -> None:
|
||||||
"""Custom exception for CLI-specific errors."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NimbusFlowCLI:
|
|
||||||
"""Main CLI application class."""
|
|
||||||
|
|
||||||
def __init__(self, db_path: str = "database6_accepts_and_declines.db"):
|
|
||||||
"""Initialize CLI with database connection."""
|
|
||||||
self.db_path = Path(__file__).parent / db_path
|
|
||||||
if not self.db_path.exists():
|
|
||||||
raise CLIError(f"Database not found: {self.db_path}")
|
|
||||||
|
|
||||||
self.db = DatabaseConnection(self.db_path)
|
|
||||||
self._init_repositories()
|
|
||||||
|
|
||||||
def _init_repositories(self):
|
|
||||||
"""Initialize all repository instances."""
|
|
||||||
self.member_repo = MemberRepository(self.db)
|
|
||||||
self.classification_repo = ClassificationRepository(self.db)
|
|
||||||
self.service_repo = ServiceRepository(self.db)
|
|
||||||
self.availability_repo = ServiceAvailabilityRepository(self.db)
|
|
||||||
self.schedule_repo = ScheduleRepository(self.db)
|
|
||||||
self.service_type_repo = ServiceTypeRepository(self.db)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Clean up database connection."""
|
|
||||||
if hasattr(self, 'db'):
|
|
||||||
self.db.close()
|
|
||||||
|
|
||||||
|
|
||||||
def format_member_row(member, classification_name: Optional[str] = None) -> str:
|
|
||||||
"""Format a member for table display."""
|
|
||||||
active = "✓" if member.IsActive else "✗"
|
|
||||||
classification = classification_name or "N/A"
|
|
||||||
return f"{member.MemberId:3d} | {member.FirstName:<12} | {member.LastName:<15} | {classification:<12} | {active:^6} | {member.Email or 'N/A'}"
|
|
||||||
|
|
||||||
|
|
||||||
def format_schedule_row(schedule, member_name: str = "", service_info: str = "") -> str:
|
|
||||||
"""Format a schedule for table display."""
|
|
||||||
status_symbols = {
|
|
||||||
ScheduleStatus.PENDING: "⏳",
|
|
||||||
ScheduleStatus.ACCEPTED: "✅",
|
|
||||||
ScheduleStatus.DECLINED: "❌"
|
|
||||||
}
|
|
||||||
status_symbol = status_symbols.get(ScheduleStatus.from_raw(schedule.Status), "❓")
|
|
||||||
# Handle ScheduledAt - could be datetime object or string from DB
|
|
||||||
if schedule.ScheduledAt:
|
|
||||||
if isinstance(schedule.ScheduledAt, str):
|
|
||||||
# If it's a string, try to parse and format it, or use as-is
|
|
||||||
try:
|
|
||||||
dt_obj = datetime.fromisoformat(schedule.ScheduledAt.replace('Z', '+00:00'))
|
|
||||||
scheduled_date = dt_obj.strftime("%Y-%m-%d %H:%M")
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
scheduled_date = str(schedule.ScheduledAt)
|
|
||||||
else:
|
|
||||||
# If it's already a datetime object
|
|
||||||
scheduled_date = schedule.ScheduledAt.strftime("%Y-%m-%d %H:%M")
|
|
||||||
else:
|
|
||||||
scheduled_date = "N/A"
|
|
||||||
|
|
||||||
return f"{schedule.ScheduleId:3d} | {status_symbol} {schedule.Status:<8} | {member_name:<20} | {service_info:<15} | {scheduled_date}"
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_members_list(cli: NimbusFlowCLI, args) -> None:
|
|
||||||
"""List all members with optional filters."""
|
|
||||||
print("Listing members...")
|
|
||||||
|
|
||||||
# Get all classifications for lookup
|
|
||||||
classifications = cli.classification_repo.list_all()
|
|
||||||
classification_map = {c.ClassificationId: c.ClassificationName for c in classifications}
|
|
||||||
|
|
||||||
# Apply filters
|
|
||||||
if args.classification:
|
|
||||||
# Find classification ID by name
|
|
||||||
classification_id = None
|
|
||||||
for c in classifications:
|
|
||||||
if c.ClassificationName.lower() == args.classification.lower():
|
|
||||||
classification_id = c.ClassificationId
|
|
||||||
break
|
|
||||||
|
|
||||||
if classification_id is None:
|
|
||||||
print(f"❌ Classification '{args.classification}' not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
members = cli.member_repo.get_by_classification_ids([classification_id])
|
|
||||||
elif args.active:
|
|
||||||
members = cli.member_repo.get_active()
|
|
||||||
else:
|
|
||||||
members = cli.member_repo.list_all()
|
|
||||||
|
|
||||||
if not members:
|
|
||||||
print("No members found.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Print header
|
|
||||||
print(f"\n{'ID':<3} | {'First Name':<12} | {'Last Name':<15} | {'Classification':<12} | {'Active':<6} | {'Email'}")
|
|
||||||
print("-" * 80)
|
|
||||||
|
|
||||||
# Print members
|
|
||||||
for member in members:
|
|
||||||
classification_name = classification_map.get(member.ClassificationId)
|
|
||||||
print(format_member_row(member, classification_name))
|
|
||||||
|
|
||||||
print(f"\nTotal: {len(members)} members")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_members_show(cli: NimbusFlowCLI, args) -> None:
|
|
||||||
"""Show detailed information about a specific member."""
|
|
||||||
member = cli.member_repo.get_by_id(args.member_id)
|
|
||||||
if not member:
|
|
||||||
print(f"❌ Member with ID {args.member_id} not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get classification name
|
|
||||||
classification = None
|
|
||||||
if member.ClassificationId:
|
|
||||||
classification = cli.classification_repo.get_by_id(member.ClassificationId)
|
|
||||||
|
|
||||||
print(f"\n📋 Member Details (ID: {member.MemberId})")
|
|
||||||
print("-" * 50)
|
|
||||||
print(f"Name: {member.FirstName} {member.LastName}")
|
|
||||||
print(f"Email: {member.Email or 'N/A'}")
|
|
||||||
print(f"Phone: {member.PhoneNumber or 'N/A'}")
|
|
||||||
print(f"Classification: {classification.ClassificationName if classification else 'N/A'}")
|
|
||||||
print(f"Active: {'Yes' if member.IsActive else 'No'}")
|
|
||||||
print(f"Notes: {member.Notes or 'N/A'}")
|
|
||||||
|
|
||||||
print(f"\n⏰ Schedule History:")
|
|
||||||
print(f"Last Scheduled: {member.LastScheduledAt or 'Never'}")
|
|
||||||
print(f"Last Accepted: {member.LastAcceptedAt or 'Never'}")
|
|
||||||
print(f"Last Declined: {member.LastDeclinedAt or 'Never'}")
|
|
||||||
print(f"Decline Streak: {member.DeclineStreak}")
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_schedules_list(cli: NimbusFlowCLI, args) -> None:
|
|
||||||
"""List schedules with optional filters."""
|
"""List schedules with optional filters."""
|
||||||
print("Listing schedules...")
|
print("Listing schedules...")
|
||||||
|
|
||||||
@@ -217,7 +57,7 @@ def cmd_schedules_list(cli: NimbusFlowCLI, args) -> None:
|
|||||||
print(f"\nTotal: {len(schedules)} schedules")
|
print(f"\nTotal: {len(schedules)} schedules")
|
||||||
|
|
||||||
|
|
||||||
def cmd_schedules_show(cli: NimbusFlowCLI, args) -> None:
|
def cmd_schedules_show(cli: "NimbusFlowCLI", args) -> None:
|
||||||
"""Show detailed information about a specific schedule."""
|
"""Show detailed information about a specific schedule."""
|
||||||
schedule = cli.schedule_repo.get_by_id(args.schedule_id)
|
schedule = cli.schedule_repo.get_by_id(args.schedule_id)
|
||||||
if not schedule:
|
if not schedule:
|
||||||
@@ -244,7 +84,7 @@ def cmd_schedules_show(cli: NimbusFlowCLI, args) -> None:
|
|||||||
print(f"Decline Reason: {schedule.DeclineReason}")
|
print(f"Decline Reason: {schedule.DeclineReason}")
|
||||||
|
|
||||||
|
|
||||||
def cmd_schedules_accept(cli: NimbusFlowCLI, args) -> None:
|
def cmd_schedules_accept(cli: "NimbusFlowCLI", args) -> None:
|
||||||
"""Accept a scheduled position."""
|
"""Accept a scheduled position."""
|
||||||
# Interactive mode with date parameter
|
# Interactive mode with date parameter
|
||||||
if hasattr(args, 'date') and args.date:
|
if hasattr(args, 'date') and args.date:
|
||||||
@@ -373,7 +213,7 @@ def cmd_schedules_accept(cli: NimbusFlowCLI, args) -> None:
|
|||||||
print(f" Service: {service_type.TypeName} on {service.ServiceDate}")
|
print(f" Service: {service_type.TypeName} on {service.ServiceDate}")
|
||||||
|
|
||||||
|
|
||||||
def cmd_schedules_decline(cli: NimbusFlowCLI, args) -> None:
|
def cmd_schedules_decline(cli: "NimbusFlowCLI", args) -> None:
|
||||||
"""Decline a scheduled position."""
|
"""Decline a scheduled position."""
|
||||||
# Interactive mode with date parameter
|
# Interactive mode with date parameter
|
||||||
if hasattr(args, 'date') and args.date:
|
if hasattr(args, 'date') and args.date:
|
||||||
@@ -517,91 +357,8 @@ def cmd_schedules_decline(cli: NimbusFlowCLI, args) -> None:
|
|||||||
print(f" Reason: {decline_reason}")
|
print(f" Reason: {decline_reason}")
|
||||||
|
|
||||||
|
|
||||||
def cmd_services_list(cli: NimbusFlowCLI, args) -> None:
|
def setup_schedules_parser(subparsers) -> None:
|
||||||
"""List services with optional filters."""
|
"""Set up schedule-related command parsers."""
|
||||||
print("Listing services...")
|
|
||||||
|
|
||||||
# Apply filters
|
|
||||||
if args.upcoming:
|
|
||||||
services = cli.service_repo.upcoming(limit=args.limit or 50)
|
|
||||||
elif args.date:
|
|
||||||
try:
|
|
||||||
target_date = date.fromisoformat(args.date)
|
|
||||||
# Get services for specific date
|
|
||||||
all_services = cli.service_repo.list_all()
|
|
||||||
services = [s for s in all_services if s.ServiceDate == target_date]
|
|
||||||
except ValueError:
|
|
||||||
print(f"❌ Invalid date format '{args.date}'. Use YYYY-MM-DD")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
services = cli.service_repo.list_all()
|
|
||||||
if args.limit:
|
|
||||||
services = services[:args.limit]
|
|
||||||
|
|
||||||
if not services:
|
|
||||||
print("No services found.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get service type names for display
|
|
||||||
service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()}
|
|
||||||
|
|
||||||
# Get schedule counts for each service
|
|
||||||
schedule_counts = {}
|
|
||||||
for service in services:
|
|
||||||
schedules = cli.schedule_repo.list_all()
|
|
||||||
service_schedules = [s for s in schedules if s.ServiceId == service.ServiceId]
|
|
||||||
|
|
||||||
pending_count = len([s for s in service_schedules if s.Status == ScheduleStatus.PENDING.value])
|
|
||||||
accepted_count = len([s for s in service_schedules if s.Status == ScheduleStatus.ACCEPTED.value])
|
|
||||||
declined_count = len([s for s in service_schedules if s.Status == ScheduleStatus.DECLINED.value])
|
|
||||||
|
|
||||||
schedule_counts[service.ServiceId] = {
|
|
||||||
'pending': pending_count,
|
|
||||||
'accepted': accepted_count,
|
|
||||||
'declined': declined_count,
|
|
||||||
'total': len(service_schedules)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print header
|
|
||||||
print(f"\n{'ID':<3} | {'Date':<12} | {'Service Type':<12} | {'Total':<5} | {'Pending':<7} | {'Accepted':<8} | {'Declined'}")
|
|
||||||
print("-" * 85)
|
|
||||||
|
|
||||||
# Print services
|
|
||||||
for service in sorted(services, key=lambda s: s.ServiceDate):
|
|
||||||
type_name = service_type_map.get(service.ServiceTypeId, "Unknown")
|
|
||||||
counts = schedule_counts.get(service.ServiceId, {'total': 0, 'pending': 0, 'accepted': 0, 'declined': 0})
|
|
||||||
|
|
||||||
# Format date properly
|
|
||||||
date_str = str(service.ServiceDate) if service.ServiceDate else "N/A"
|
|
||||||
|
|
||||||
print(f"{service.ServiceId:<3} | {date_str:<12} | {type_name:<12} | "
|
|
||||||
f"{counts['total']:<5} | {counts['pending']:<7} | {counts['accepted']:<8} | {counts['declined']}")
|
|
||||||
|
|
||||||
print(f"\nTotal: {len(services)} services")
|
|
||||||
|
|
||||||
|
|
||||||
def setup_parser() -> argparse.ArgumentParser:
|
|
||||||
"""Set up the command-line argument parser."""
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="NimbusFlow CLI - Manage the scheduling system",
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
||||||
)
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
||||||
|
|
||||||
# Members commands
|
|
||||||
members_parser = subparsers.add_parser("members", help="Manage members")
|
|
||||||
members_subparsers = members_parser.add_subparsers(dest="members_action", help="Members actions")
|
|
||||||
|
|
||||||
# members list
|
|
||||||
members_list_parser = members_subparsers.add_parser("list", help="List members")
|
|
||||||
members_list_parser.add_argument("--active", action="store_true", help="Show only active members")
|
|
||||||
members_list_parser.add_argument("--classification", type=str, help="Filter by classification name")
|
|
||||||
|
|
||||||
# members show
|
|
||||||
members_show_parser = members_subparsers.add_parser("show", help="Show member details")
|
|
||||||
members_show_parser.add_argument("member_id", type=int, help="Member ID to show")
|
|
||||||
|
|
||||||
# Schedules commands
|
# Schedules commands
|
||||||
schedules_parser = subparsers.add_parser("schedules", help="Manage schedules")
|
schedules_parser = subparsers.add_parser("schedules", help="Manage schedules")
|
||||||
schedules_subparsers = schedules_parser.add_subparsers(dest="schedules_action", help="Schedule actions")
|
schedules_subparsers = schedules_parser.add_subparsers(dest="schedules_action", help="Schedule actions")
|
||||||
@@ -625,77 +382,3 @@ def setup_parser() -> argparse.ArgumentParser:
|
|||||||
schedules_decline_parser.add_argument("schedule_id", type=int, nargs="?", help="Schedule ID to decline (optional if using --date)")
|
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("--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")
|
||||||
|
|
||||||
# Services commands
|
|
||||||
services_parser = subparsers.add_parser("services", help="Manage services")
|
|
||||||
services_subparsers = services_parser.add_subparsers(dest="services_action", help="Services actions")
|
|
||||||
|
|
||||||
# services list
|
|
||||||
services_list_parser = services_subparsers.add_parser("list", help="List services")
|
|
||||||
services_list_parser.add_argument("--date", type=str, help="Filter by specific date (YYYY-MM-DD)")
|
|
||||||
services_list_parser.add_argument("--upcoming", action="store_true", help="Show only upcoming services")
|
|
||||||
services_list_parser.add_argument("--limit", type=int, help="Limit number of results")
|
|
||||||
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main CLI entry point."""
|
|
||||||
parser = setup_parser()
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if not args.command:
|
|
||||||
parser.print_help()
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
cli = NimbusFlowCLI()
|
|
||||||
|
|
||||||
# Route commands
|
|
||||||
if args.command == "members":
|
|
||||||
if args.members_action == "list":
|
|
||||||
cmd_members_list(cli, args)
|
|
||||||
elif args.members_action == "show":
|
|
||||||
cmd_members_show(cli, args)
|
|
||||||
else:
|
|
||||||
print("❌ Unknown members action. Use 'list' or 'show'")
|
|
||||||
|
|
||||||
elif args.command == "schedules":
|
|
||||||
if args.schedules_action == "list":
|
|
||||||
cmd_schedules_list(cli, args)
|
|
||||||
elif args.schedules_action == "show":
|
|
||||||
cmd_schedules_show(cli, args)
|
|
||||||
elif args.schedules_action == "accept":
|
|
||||||
cmd_schedules_accept(cli, args)
|
|
||||||
elif args.schedules_action == "decline":
|
|
||||||
cmd_schedules_decline(cli, args)
|
|
||||||
else:
|
|
||||||
print("❌ Unknown schedules action. Use 'list', 'show', 'accept', or 'decline'")
|
|
||||||
|
|
||||||
elif args.command == "services":
|
|
||||||
if args.services_action == "list":
|
|
||||||
cmd_services_list(cli, args)
|
|
||||||
else:
|
|
||||||
print("❌ Unknown services action. Use 'list'")
|
|
||||||
|
|
||||||
else:
|
|
||||||
print(f"❌ Unknown command: {args.command}")
|
|
||||||
|
|
||||||
except CLIError as e:
|
|
||||||
print(f"❌ Error: {e}")
|
|
||||||
return 1
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\n🛑 Interrupted by user")
|
|
||||||
return 1
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Unexpected error: {e}")
|
|
||||||
return 1
|
|
||||||
finally:
|
|
||||||
if 'cli' in locals():
|
|
||||||
cli.close()
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
88
backend/cli/commands/services.py
Normal file
88
backend/cli/commands/services.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Service-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
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_services_list(cli: "NimbusFlowCLI", args) -> None:
|
||||||
|
"""List services with optional filters."""
|
||||||
|
print("Listing services...")
|
||||||
|
|
||||||
|
# Apply filters
|
||||||
|
if args.upcoming:
|
||||||
|
services = cli.service_repo.upcoming(limit=args.limit or 50)
|
||||||
|
elif args.date:
|
||||||
|
try:
|
||||||
|
target_date = date.fromisoformat(args.date)
|
||||||
|
# Get services for specific date
|
||||||
|
all_services = cli.service_repo.list_all()
|
||||||
|
services = [s for s in all_services if s.ServiceDate == target_date]
|
||||||
|
except ValueError:
|
||||||
|
print(f"❌ Invalid date format '{args.date}'. Use YYYY-MM-DD")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
services = cli.service_repo.list_all()
|
||||||
|
if args.limit:
|
||||||
|
services = services[:args.limit]
|
||||||
|
|
||||||
|
if not services:
|
||||||
|
print("No services found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get service type names for display
|
||||||
|
service_type_map = {st.ServiceTypeId: st.TypeName for st in cli.service_type_repo.list_all()}
|
||||||
|
|
||||||
|
# Get schedule counts for each service
|
||||||
|
schedule_counts = {}
|
||||||
|
for service in services:
|
||||||
|
schedules = cli.schedule_repo.list_all()
|
||||||
|
service_schedules = [s for s in schedules if s.ServiceId == service.ServiceId]
|
||||||
|
|
||||||
|
pending_count = len([s for s in service_schedules if s.Status == ScheduleStatus.PENDING.value])
|
||||||
|
accepted_count = len([s for s in service_schedules if s.Status == ScheduleStatus.ACCEPTED.value])
|
||||||
|
declined_count = len([s for s in service_schedules if s.Status == ScheduleStatus.DECLINED.value])
|
||||||
|
|
||||||
|
schedule_counts[service.ServiceId] = {
|
||||||
|
'pending': pending_count,
|
||||||
|
'accepted': accepted_count,
|
||||||
|
'declined': declined_count,
|
||||||
|
'total': len(service_schedules)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print header
|
||||||
|
print(f"\n{'ID':<3} | {'Date':<12} | {'Service Type':<12} | {'Total':<5} | {'Pending':<7} | {'Accepted':<8} | {'Declined'}")
|
||||||
|
print("-" * 85)
|
||||||
|
|
||||||
|
# Print services
|
||||||
|
for service in sorted(services, key=lambda s: s.ServiceDate):
|
||||||
|
type_name = service_type_map.get(service.ServiceTypeId, "Unknown")
|
||||||
|
counts = schedule_counts.get(service.ServiceId, {'total': 0, 'pending': 0, 'accepted': 0, 'declined': 0})
|
||||||
|
|
||||||
|
# Format date properly
|
||||||
|
date_str = str(service.ServiceDate) if service.ServiceDate else "N/A"
|
||||||
|
|
||||||
|
print(f"{service.ServiceId:<3} | {date_str:<12} | {type_name:<12} | "
|
||||||
|
f"{counts['total']:<5} | {counts['pending']:<7} | {counts['accepted']:<8} | {counts['declined']}")
|
||||||
|
|
||||||
|
print(f"\nTotal: {len(services)} services")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_services_parser(subparsers) -> None:
|
||||||
|
"""Set up service-related command parsers."""
|
||||||
|
# Services commands
|
||||||
|
services_parser = subparsers.add_parser("services", help="Manage services")
|
||||||
|
services_subparsers = services_parser.add_subparsers(dest="services_action", help="Services actions")
|
||||||
|
|
||||||
|
# services list
|
||||||
|
services_list_parser = services_subparsers.add_parser("list", help="List services")
|
||||||
|
services_list_parser.add_argument("--date", type=str, help="Filter by specific date (YYYY-MM-DD)")
|
||||||
|
services_list_parser.add_argument("--upcoming", action="store_true", help="Show only upcoming services")
|
||||||
|
services_list_parser.add_argument("--limit", type=int, help="Limit number of results")
|
||||||
93
backend/cli/main.py
Normal file
93
backend/cli/main.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
"""
|
||||||
|
Main CLI coordination and entry point.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from .base import NimbusFlowCLI, CLIError
|
||||||
|
from .commands import (
|
||||||
|
# Member commands
|
||||||
|
cmd_members_list, cmd_members_show, setup_members_parser,
|
||||||
|
# Schedule commands
|
||||||
|
cmd_schedules_list, cmd_schedules_show, cmd_schedules_accept,
|
||||||
|
cmd_schedules_decline, setup_schedules_parser,
|
||||||
|
# Service commands
|
||||||
|
cmd_services_list, setup_services_parser,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_parser() -> argparse.ArgumentParser:
|
||||||
|
"""Set up the command-line argument parser."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="python -m backend.cli",
|
||||||
|
description="NimbusFlow CLI - Manage the scheduling system",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||||
|
|
||||||
|
# Set up all command parsers using the modular functions
|
||||||
|
setup_members_parser(subparsers)
|
||||||
|
setup_schedules_parser(subparsers)
|
||||||
|
setup_services_parser(subparsers)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main CLI entry point."""
|
||||||
|
parser = setup_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
parser.print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
cli = NimbusFlowCLI()
|
||||||
|
|
||||||
|
# Route commands to their respective handlers
|
||||||
|
if args.command == "members":
|
||||||
|
if args.members_action == "list":
|
||||||
|
cmd_members_list(cli, args)
|
||||||
|
elif args.members_action == "show":
|
||||||
|
cmd_members_show(cli, args)
|
||||||
|
else:
|
||||||
|
print("❌ Unknown members action. Use 'list' or 'show'")
|
||||||
|
|
||||||
|
elif args.command == "schedules":
|
||||||
|
if args.schedules_action == "list":
|
||||||
|
cmd_schedules_list(cli, args)
|
||||||
|
elif args.schedules_action == "show":
|
||||||
|
cmd_schedules_show(cli, args)
|
||||||
|
elif args.schedules_action == "accept":
|
||||||
|
cmd_schedules_accept(cli, args)
|
||||||
|
elif args.schedules_action == "decline":
|
||||||
|
cmd_schedules_decline(cli, args)
|
||||||
|
else:
|
||||||
|
print("❌ Unknown schedules action. Use 'list', 'show', 'accept', or 'decline'")
|
||||||
|
|
||||||
|
elif args.command == "services":
|
||||||
|
if args.services_action == "list":
|
||||||
|
cmd_services_list(cli, args)
|
||||||
|
else:
|
||||||
|
print("❌ Unknown services action. Use 'list'")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"❌ Unknown command: {args.command}")
|
||||||
|
|
||||||
|
except CLIError as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
return 1
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n🛑 Interrupted by user")
|
||||||
|
return 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Unexpected error: {e}")
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
if 'cli' in locals():
|
||||||
|
cli.close()
|
||||||
|
|
||||||
|
return 0
|
||||||
41
backend/cli/utils.py
Normal file
41
backend/cli/utils.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Formatting utilities for NimbusFlow CLI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from backend.models.enums import ScheduleStatus
|
||||||
|
|
||||||
|
|
||||||
|
def format_member_row(member, classification_name: Optional[str] = None) -> str:
|
||||||
|
"""Format a member for table display."""
|
||||||
|
active = "✓" if member.IsActive else "✗"
|
||||||
|
classification = classification_name or "N/A"
|
||||||
|
return f"{member.MemberId:3d} | {member.FirstName:<12} | {member.LastName:<15} | {classification:<12} | {active:^6} | {member.Email or 'N/A'}"
|
||||||
|
|
||||||
|
|
||||||
|
def format_schedule_row(schedule, member_name: str = "", service_info: str = "") -> str:
|
||||||
|
"""Format a schedule for table display."""
|
||||||
|
status_symbols = {
|
||||||
|
ScheduleStatus.PENDING: "⏳",
|
||||||
|
ScheduleStatus.ACCEPTED: "✅",
|
||||||
|
ScheduleStatus.DECLINED: "❌"
|
||||||
|
}
|
||||||
|
status_symbol = status_symbols.get(ScheduleStatus.from_raw(schedule.Status), "❓")
|
||||||
|
|
||||||
|
# Handle ScheduledAt - could be datetime object or string from DB
|
||||||
|
if schedule.ScheduledAt:
|
||||||
|
if isinstance(schedule.ScheduledAt, str):
|
||||||
|
# If it's a string, try to parse and format it, or use as-is
|
||||||
|
try:
|
||||||
|
dt_obj = datetime.fromisoformat(schedule.ScheduledAt.replace('Z', '+00:00'))
|
||||||
|
scheduled_date = dt_obj.strftime("%Y-%m-%d %H:%M")
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
scheduled_date = str(schedule.ScheduledAt)
|
||||||
|
else:
|
||||||
|
# If it's already a datetime object
|
||||||
|
scheduled_date = schedule.ScheduledAt.strftime("%Y-%m-%d %H:%M")
|
||||||
|
else:
|
||||||
|
scheduled_date = "N/A"
|
||||||
|
|
||||||
|
return f"{schedule.ScheduleId:3d} | {status_symbol} {schedule.Status:<8} | {member_name:<20} | {service_info:<15} | {scheduled_date}"
|
||||||
@@ -169,7 +169,7 @@ if __name__ == "__main__":
|
|||||||
from backend.repositories import MemberRepository, ScheduleRepository, ServiceRepository, ServiceAvailabilityRepository
|
from backend.repositories import MemberRepository, ScheduleRepository, ServiceRepository, ServiceAvailabilityRepository
|
||||||
from backend.services.scheduling_service import SchedulingService
|
from backend.services.scheduling_service import SchedulingService
|
||||||
|
|
||||||
DB_PATH = Path(__file__).parent / "database6_accepts_and_declines.db"
|
DB_PATH = Path(__file__).parent / "database6_accepts_and_declines2.db"
|
||||||
|
|
||||||
# Initialise DB connection (adjust DSN as needed)
|
# Initialise DB connection (adjust DSN as needed)
|
||||||
db = DatabaseConnection(DB_PATH)
|
db = DatabaseConnection(DB_PATH)
|
||||||
|
|||||||
Reference in New Issue
Block a user