diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b3d7fe7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/frontend/bin/Debug/net8.0/NimbusFlow.Frontend.dll", + "args": [], + "cwd": "${workspaceFolder}/frontend", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..ff0c17f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/frontend/NimbusFlow.Frontend.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/frontend/NimbusFlow.Frontend.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/frontend/NimbusFlow.Frontend.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 2225b60..019b8bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,145 +2,295 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Architecture +## Project Overview -NimbusFlow is a scheduling system with a **monorepo structure** containing separate backend and frontend applications: +NimbusFlow is a comprehensive scheduling system with a **monorepo structure** containing separate backend and frontend applications designed for church music ministry scheduling. The system handles round-robin member scheduling with sophisticated algorithms for fair distribution and availability management. -- **Backend**: Python-based scheduling service using SQLite with a repository pattern -- **Frontend**: .NET Blazor Server application with Bootstrap UI +### Architecture Overview -### Backend Architecture +- **Backend**: Python-based scheduling service with FastAPI REST API, SQLite database, and comprehensive CLI +- **Frontend**: .NET 8 Blazor Server application with Bootstrap UI for web-based management + +## Backend Architecture The backend follows a **layered architecture** with clear separation of concerns: ``` backend/ -├── db/ # Database connection layer -├── models/ # Data models and enums -├── repositories/ # Data access layer -├── services/ # Business logic layer -└── tests/ # Test suite +├── api/ # FastAPI REST API server +├── cli/ # Command-line interface +├── db/ # Database connection layer +├── models/ # Data models and enums +├── repositories/ # Data access layer (Repository pattern) +├── services/ # Business logic layer +├── tests/ # Comprehensive test suite +├── utils/ # Utility functions +├── requirements.txt # Python dependencies +└── schema.sql # Database schema ``` -**Key Components:** +### Key Components -- **DatabaseConnection** (`db/connection.py`): SQLite wrapper with context manager support -- **Repository Pattern**: Each domain entity has its own repository (Member, Service, Classification, etc.) -- **SchedulingService** (`services/scheduling_service.py`): Core business logic for round-robin scheduling with boost/cooldown algorithms -- **Data Models** (`models/dataclasses.py`): Dataclasses with SQLite row conversion utilities +#### API Layer (`api/`) +- **app.py**: FastAPI application with comprehensive REST endpoints +- **__main__.py**: Uvicorn server entry point with auto-reload +- Supports CORS for frontend communication on `localhost:5000/5001` +- Pydantic models for request/response validation with C# naming convention compatibility -**Database Schema** (`schema.sql`): SQLite-based with tables for Members, Services, Classifications, Schedules, and audit logging. Uses foreign key constraints and indexes for performance. +#### CLI Layer (`cli/`) +- **main.py**: Command-line interface coordinator with subcommands +- **interactive.py**: Interactive mode for user-friendly operations +- **commands/**: Modular command implementations (members, schedules, services) +- **base.py**: Base CLI class with common functionality -### Frontend Architecture +#### Database Layer (`db/`) +- **connection.py**: SQLite wrapper with context manager support +- **base_repository.py**: Abstract base repository with common CRUD operations +- Thread-safe connection handling for FastAPI + +#### Repository Pattern (`repositories/`) +- **MemberRepository** (`member.py`): Member CRUD operations +- **ScheduleRepository** (`schedule.py`): Schedule management with status updates +- **ServiceRepository** (`service.py`): Service instance management +- **ClassificationRepository** (`classification.py`): Member role management +- **ServiceTypeRepository** (`service_type.py`): Time slot definitions +- **ServiceAvailabilityRepository** (`service_availability.py`): Member eligibility + +#### Business Logic (`services/`) +- **SchedulingService** (`scheduling_service.py`): Core scheduling algorithms + - Round-robin scheduling based on `LastAcceptedAt` timestamps + - 5-day decline boost for recently declined members + - Same-day exclusion rules + - Multi-classification support + - Status-aware scheduling (pending/accepted/declined) + +#### Data Models (`models/`) +- **dataclasses.py**: Core data models with SQLite row conversion utilities +- **enums.py**: Enumerated types for system constants +- Models: Member, Service, Schedule, Classification, ServiceType, etc. + +### Database Schema (`schema.sql`) + +SQLite-based with comprehensive relational design: + +**Core Tables:** +- `Members`: Member information with scheduling timestamps +- `Services`: Service instances with dates and types +- `Schedules`: Member-service assignments with status tracking +- `Classifications`: Member roles (Soprano, Alto, Tenor, Baritone) +- `ServiceTypes`: Time slots (9AM, 11AM, 6PM) +- `ServiceAvailability`: Member eligibility for service types + +**Audit Tables:** +- `AcceptedLog`, `DeclineLog`, `ScheduledLog` for comprehensive tracking +- Foreign key constraints and indexes for performance + +## Frontend Architecture .NET 8 Blazor Server application with the following structure: ``` frontend/ ├── Components/ -│ ├── Layout/ # Layout components (NavMenu, MainLayout) -│ └── Pages/ # Razor pages (Dashboard, Members, Schedules, Services) -├── Models/ # C# models matching backend data structure +│ ├── Layout/ # Layout components (NavMenu, MainLayout) +│ ├── Pages/ # Razor pages (Members, Schedules, Services) +│ ├── App.razor # Root application component +│ └── _Imports.razor # Global imports +├── Models/ # C# models matching backend structure ├── Services/ # HTTP client services for API communication -└── wwwroot/ # Static files and assets +├── Properties/ # Launch settings and configuration +├── wwwroot/ # Static files and assets +└── Program.cs # Application startup and DI configuration ``` -**Key Components:** +### Key Components -- **ApiService** (`Services/ApiService.cs`): HTTP client wrapper for Python backend API communication -- **Models** (`Models/Member.cs`): C# data models (Member, Schedule, Service, Classification, etc.) -- **Razor Components**: Interactive pages for member management, scheduling, and service administration -- **Bootstrap UI**: Responsive design with Bootstrap 5 styling +#### Services Layer +- **ApiService.cs**: HTTP client implementation with JSON serialization +- **IApiService.cs**: Service interface for dependency injection +- Configured for `http://localhost:8000/api/` backend communication +- Camel case JSON handling for API compatibility -The frontend communicates with the Python backend via HTTP API calls, expecting JSON responses that match the C# model structure. +#### Models (`Models/`) +- **Member.cs**: Complete data models matching backend structure +- Models: Member, Classification, Service, ServiceType, Schedule +- Navigation properties for related data +- Compatible with FastAPI Pydantic models + +#### Razor Components (`Components/Pages/`) +- **Members.razor**: Member management interface +- **Schedules.razor**: Schedule management and workflow +- **Services.razor**: Service administration +- **Home.razor**: Dashboard and overview +- Bootstrap 5 styling with responsive design + +#### Dependency Injection (`Program.cs`) +- HTTP client configuration for backend API +- Service registration with scoped lifetime +- HTTPS redirection and static file serving ## Development Commands -### Backend +### Backend Development -**Setup:** +**Environment Setup:** ```bash cd backend -# Activate virtual environment (already exists) -source venv/bin/activate +# Virtual environment is pre-configured in .venv/ +source .venv/bin/activate +``` + +**Dependencies:** +```bash +# Install from requirements.txt +pip install -r requirements.txt +``` + +**API Server:** +```bash +cd backend +source .venv/bin/activate +python -m api +# Starts FastAPI server on http://localhost:8000 +# API documentation at http://localhost:8000/docs +``` + +**CLI Usage:** +```bash +cd backend +source .venv/bin/activate +python -m cli --help +python -m cli members list +python -m cli schedules schedule --help +``` + +**Demo/Development Data:** +```bash +cd backend +source .venv/bin/activate +python main.py # Runs demo data population ``` **Testing:** ```bash cd backend -# Run tests using pytest from virtual environment -venv/bin/pytest -# Run specific test file -venv/bin/pytest tests/repositories/test_member.py -# Run with coverage -venv/bin/pytest --cov +source .venv/bin/activate +pytest # Run all tests +pytest tests/repositories/ # Run repository tests +pytest --cov # Run with coverage ``` -**Run Application:** -```bash -cd backend -# Run the demo script -python main.py -``` - -### Frontend +### Frontend Development **Setup:** ```bash cd frontend -# Restore NuGet packages dotnet restore ``` -**Development:** +**Development Server:** ```bash cd frontend -# Start development server (with hot reload) -dotnet watch -# Or run without watch -dotnet run +dotnet watch # Hot reload development +# Or: dotnet run +``` -# Build for production -dotnet build -# Publish for deployment -dotnet publish -c Release +**Build & Deploy:** +```bash +cd frontend +dotnet build # Development build +dotnet build -c Release # Production build +dotnet publish -c Release # Publish for deployment ``` **Access:** - Development: https://localhost:5001 (HTTPS) or http://localhost:5000 (HTTP) -- The application expects the Python backend API to be available at http://localhost:8000/api/ +- Expects backend API at http://localhost:8000/api/ ## Core Business Logic -The **SchedulingService** implements a sophisticated member scheduling algorithm: +### Scheduling Algorithm -1. **Round-robin scheduling** based on `Members.LastAcceptedAt` timestamps -2. **5-day decline boost** - recently declined members get priority -3. **Same-day exclusion** - members can't be scheduled for multiple services on the same day -4. **Service availability** - members must be eligible for the specific service type -5. **Status constraints** - prevents double-booking (pending/accepted/declined statuses) +The **SchedulingService** implements sophisticated member scheduling: + +1. **Round-robin fairness**: Based on `LastAcceptedAt` timestamps +2. **Decline boost**: 5-day priority for recently declined members +3. **Same-day exclusion**: Members cannot serve multiple services per day +4. **Classification matching**: Multi-classification support for flexible roles +5. **Availability filtering**: Service type eligibility checking +6. **Status management**: Prevents double-booking across pending/accepted states **Key Methods:** -- `schedule_next_member()` - Core scheduling logic with multi-classification support -- `decline_service_for_user()` - Handle member declining assignments +- `schedule_next_member(classification_ids, service_id)`: Core scheduling with multi-role support +- `decline_service_for_user(member_id, service_id, reason)`: Decline handling with boost logic -## Database +### API Endpoints -The system uses **SQLite** with the following key tables: -- `Members` - Core member data with scheduling timestamps -- `Services` - Service instances on specific dates -- `Schedules` - Member-service assignments with status tracking -- `Classifications` - Member roles (Soprano, Alto, Tenor, Baritone) -- `ServiceTypes` - Time slots (9AM, 11AM, 6PM) -- `ServiceAvailability` - Member eligibility for service types +**Member Management:** +- `GET /api/members` - List all members +- `GET /api/members/{id}` - Get member details +- `POST /api/members` - Create new member +- `PUT /api/members/{id}` - Update member +- `DELETE /api/members/{id}` - Delete member -**Audit Tables**: `AcceptedLog`, `DeclineLog`, `ScheduledLog` for comprehensive tracking. +**Scheduling Operations:** +- `GET /api/schedules` - List all schedules +- `POST /api/schedules/schedule-next` - Schedule next available member +- `POST /api/schedules/{id}/accept` - Accept assignment +- `POST /api/schedules/{id}/decline` - Decline with reason +- `DELETE /api/schedules/{id}` - Remove schedule + +**Service Management:** +- `GET /api/services` - List services +- `POST /api/services` - Create new service +- `GET /api/service-types` - List service types + +## Database Management + +**Schema Location:** `backend/schema.sql` + +**Database File:** `backend/db/sqlite/database.db` (auto-created) + +**Key Features:** +- Automatic database creation from schema if missing +- Foreign key constraints enforced +- Comprehensive indexing for performance +- Audit logging for all schedule operations ## Testing -Backend uses **pytest** with fixtures for database setup. Tests cover: -- Repository layer functionality -- Business logic in services -- Database schema constraints +**Backend Testing with pytest:** +- Repository layer unit tests with in-memory SQLite +- Service layer business logic tests +- Fixtures for database setup and test data +- Comprehensive coverage of scheduling algorithms -All tests use in-memory SQLite databases created fresh for each test. \ No newline at end of file +**Test Structure:** +``` +tests/ +├── conftest.py # Shared fixtures +├── repositories/ # Repository layer tests +└── services/ # Business logic tests +``` + +## Development Workflow + +1. **Backend First**: Start with API development and testing +2. **Database Schema**: Modify `schema.sql` for data model changes +3. **Repository Layer**: Update repositories for new database operations +4. **Service Layer**: Implement business logic in services +5. **API Layer**: Add FastAPI endpoints with Pydantic models +6. **Frontend Models**: Update C# models to match API +7. **Frontend Services**: Extend ApiService for new endpoints +8. **Frontend UI**: Create/update Razor components +9. **Testing**: Add tests for new functionality +10. **Integration**: Test full stack communication + +## Important Notes + +- **Thread Safety**: FastAPI uses custom DatabaseConnection for thread safety +- **JSON Compatibility**: Backend uses PascalCase/camelCase aliasing for C# compatibility +- **Error Handling**: Comprehensive HTTP status codes and error responses +- **CORS**: Configured for frontend origins on localhost:5000/5001 +- **Virtual Environment**: Pre-configured Python environment in `backend/.venv/` +- **Auto-reload**: Both backend (Uvicorn) and frontend (dotnet watch) support hot reload \ No newline at end of file diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000..f35899a --- /dev/null +++ b/backend/api/__init__.py @@ -0,0 +1,7 @@ +""" +FastAPI module for NimbusFlow backend. +""" + +from .app import app + +__all__ = ["app"] \ No newline at end of file diff --git a/backend/api/__main__.py b/backend/api/__main__.py new file mode 100644 index 0000000..3211f57 --- /dev/null +++ b/backend/api/__main__.py @@ -0,0 +1,14 @@ +""" +Entry point for running the FastAPI server with python -m backend.api +""" + +import uvicorn + +if __name__ == "__main__": + uvicorn.run( + "backend.api:app", + host="0.0.0.0", + port=8000, + reload=True, + log_level="info" + ) \ No newline at end of file diff --git a/backend/api/app.py b/backend/api/app.py new file mode 100644 index 0000000..2315266 --- /dev/null +++ b/backend/api/app.py @@ -0,0 +1,479 @@ +""" +FastAPI application for NimbusFlow backend. +Provides REST API endpoints for the frontend Blazor application. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import List, Optional, Union +from datetime import datetime, date + +from fastapi import FastAPI, HTTPException, Depends +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field + +# Import the existing backend modules +from backend.db import DatabaseConnection +from backend.repositories import ( + MemberRepository, + ClassificationRepository, + ServiceRepository, + ServiceTypeRepository, + ScheduleRepository, + ServiceAvailabilityRepository, +) +from backend.services.scheduling_service import SchedulingService +from backend.models.dataclasses import ( + Member as DbMember, + Classification as DbClassification, + Service as DbService, + ServiceType as DbServiceType, + Schedule as DbSchedule, +) + +# Initialize FastAPI app +app = FastAPI(title="NimbusFlow API", version="1.0.0") + +# Add CORS middleware to allow frontend access +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://localhost:5059", + "https://localhost:5059" + ], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Database path +DB_PATH = Path(__file__).parent.parent / "db" / "sqlite" / "database.db" + +# Configure SQLite to allow threading +import sqlite3 +sqlite3.threadsafety = 3 + + +# Custom DatabaseConnection for FastAPI that handles threading +class FastAPIDatabaseConnection(DatabaseConnection): + """DatabaseConnection that allows cross-thread usage for FastAPI.""" + + def __init__( + self, + db_path: Union[str, Path], + *, + timeout: float = 5.0, + detect_types: int = sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, + ) -> None: + # Call parent constructor but modify connection to allow threading + self._conn: sqlite3.Connection = sqlite3.connect( + str(db_path), + timeout=timeout, + detect_types=detect_types, + check_same_thread=False # Allow cross-thread usage + ) + # ``Row`` makes column access dictionary‑like and preserves order. + self._conn.row_factory = sqlite3.Row + self._cursor: sqlite3.Cursor = self._conn.cursor() + +# Pydantic models for API requests/responses +class Member(BaseModel): + memberId: int = Field(alias="MemberId") + firstName: str = Field(alias="FirstName") + lastName: str = Field(alias="LastName") + email: Optional[str] = Field(default=None, alias="Email") + phoneNumber: Optional[str] = Field(default=None, alias="PhoneNumber") + classificationId: Optional[int] = Field(default=None, alias="ClassificationId") + notes: Optional[str] = Field(default=None, alias="Notes") + isActive: int = Field(default=1, alias="IsActive") + lastScheduledAt: Optional[datetime] = Field(default=None, alias="LastScheduledAt") + lastAcceptedAt: Optional[datetime] = Field(default=None, alias="LastAcceptedAt") + lastDeclinedAt: Optional[datetime] = Field(default=None, alias="LastDeclinedAt") + declineStreak: int = Field(default=0, alias="DeclineStreak") + + class Config: + populate_by_name = True + + +class MemberCreate(BaseModel): + firstName: str = Field(alias="FirstName") + lastName: str = Field(alias="LastName") + email: Optional[str] = Field(default=None, alias="Email") + phoneNumber: Optional[str] = Field(default=None, alias="PhoneNumber") + classificationId: Optional[int] = Field(default=None, alias="ClassificationId") + notes: Optional[str] = Field(default=None, alias="Notes") + isActive: int = Field(default=1, alias="IsActive") + + class Config: + populate_by_name = True + + +class Classification(BaseModel): + classificationId: int = Field(alias="ClassificationId") + classificationName: str = Field(alias="ClassificationName") + + class Config: + populate_by_name = True + + +class Service(BaseModel): + serviceId: int = Field(alias="ServiceId") + serviceTypeId: int = Field(alias="ServiceTypeId") + serviceDate: date = Field(alias="ServiceDate") + + class Config: + populate_by_name = True + + +class ServiceCreate(BaseModel): + serviceTypeId: int = Field(alias="ServiceTypeId") + serviceDate: date = Field(alias="ServiceDate") + + class Config: + populate_by_name = True + + +class ServiceType(BaseModel): + serviceTypeId: int = Field(alias="ServiceTypeId") + typeName: str = Field(alias="TypeName") + + class Config: + populate_by_name = True + + +class Schedule(BaseModel): + scheduleId: int = Field(alias="ScheduleId") + serviceId: int = Field(alias="ServiceId") + memberId: int = Field(alias="MemberId") + status: str = Field(alias="Status") + scheduledAt: datetime = Field(alias="ScheduledAt") + acceptedAt: Optional[datetime] = Field(default=None, alias="AcceptedAt") + declinedAt: Optional[datetime] = Field(default=None, alias="DeclinedAt") + expiresAt: Optional[datetime] = Field(default=None, alias="ExpiresAt") + declineReason: Optional[str] = Field(default=None, alias="DeclineReason") + + class Config: + populate_by_name = True + + +class ScheduleNextRequest(BaseModel): + serviceId: int + classificationIds: List[int] + + +class DeclineRequest(BaseModel): + reason: Optional[str] = None + + +# Context manager for database operations +from contextlib import contextmanager + +@contextmanager +def get_db_context(): + """Context manager to handle database connections properly.""" + db = DatabaseConnection(DB_PATH) + try: + with db: + yield db + finally: + pass # Context manager handles cleanup + + +# Dependency to get database connection and repositories +def get_repositories(): + # Create a new database connection for each request to avoid thread safety issues + db = FastAPIDatabaseConnection(DB_PATH) + return { + "db": db, + "member_repo": MemberRepository(db), + "classification_repo": ClassificationRepository(db), + "service_repo": ServiceRepository(db), + "service_type_repo": ServiceTypeRepository(db), + "schedule_repo": ScheduleRepository(db), + "availability_repo": ServiceAvailabilityRepository(db), + } + + +def get_scheduling_service(repos: dict = Depends(get_repositories)): + return SchedulingService( + classification_repo=repos["classification_repo"], + member_repo=repos["member_repo"], + service_repo=repos["service_repo"], + availability_repo=repos["availability_repo"], + schedule_repo=repos["schedule_repo"], + ) + + +# Helper functions to convert between DB and API models +def db_member_to_api(db_member: DbMember) -> Member: + return Member( + MemberId=db_member.MemberId, + FirstName=db_member.FirstName, + LastName=db_member.LastName, + Email=db_member.Email, + PhoneNumber=db_member.PhoneNumber, + ClassificationId=db_member.ClassificationId, + Notes=db_member.Notes, + IsActive=db_member.IsActive, + LastScheduledAt=db_member.LastScheduledAt, + LastAcceptedAt=db_member.LastAcceptedAt, + LastDeclinedAt=db_member.LastDeclinedAt, + DeclineStreak=db_member.DeclineStreak, + ) + + +def api_member_to_db(api_member: Member) -> DbMember: + return DbMember( + MemberId=api_member.memberId, + FirstName=api_member.firstName, + LastName=api_member.lastName, + Email=api_member.email, + PhoneNumber=api_member.phoneNumber, + ClassificationId=api_member.classificationId, + Notes=api_member.notes, + IsActive=api_member.isActive, + LastScheduledAt=api_member.lastScheduledAt, + LastAcceptedAt=api_member.lastAcceptedAt, + LastDeclinedAt=api_member.lastDeclinedAt, + DeclineStreak=api_member.declineStreak, + ) + + +def db_classification_to_api(db_classification: DbClassification) -> Classification: + return Classification( + ClassificationId=db_classification.ClassificationId, + ClassificationName=db_classification.ClassificationName, + ) + + +def db_service_to_api(db_service: DbService) -> Service: + return Service( + ServiceId=db_service.ServiceId, + ServiceTypeId=db_service.ServiceTypeId, + ServiceDate=db_service.ServiceDate, + ) + + +def db_service_type_to_api(db_service_type: DbServiceType) -> ServiceType: + return ServiceType( + ServiceTypeId=db_service_type.ServiceTypeId, + TypeName=db_service_type.TypeName, + ) + + +def db_schedule_to_api(db_schedule: DbSchedule) -> Schedule: + return Schedule( + ScheduleId=db_schedule.ScheduleId, + ServiceId=db_schedule.ServiceId, + MemberId=db_schedule.MemberId, + Status=db_schedule.Status, + ScheduledAt=db_schedule.ScheduledAt, + AcceptedAt=db_schedule.AcceptedAt, + DeclinedAt=db_schedule.DeclinedAt, + ExpiresAt=db_schedule.ExpiresAt, + DeclineReason=db_schedule.DeclineReason, + ) + + +# API Endpoints + +# Member endpoints +@app.get("/api/members", response_model=List[Member]) +async def get_members(repos: dict = Depends(get_repositories)): + db_members = repos["member_repo"].list_all() + return [db_member_to_api(member) for member in db_members] + + +@app.get("/api/members/{member_id}", response_model=Member) +async def get_member(member_id: int, repos: dict = Depends(get_repositories)): + db_member = repos["member_repo"].get_by_id(member_id) + if not db_member: + raise HTTPException(status_code=404, detail="Member not found") + return db_member_to_api(db_member) + + +@app.post("/api/members", response_model=Member) +async def create_member(member_data: MemberCreate, repos: dict = Depends(get_repositories)): + db_member = repos["member_repo"].create( + first_name=member_data.firstName, + last_name=member_data.lastName, + email=member_data.email, + phone_number=member_data.phoneNumber, + classification_id=member_data.classificationId, + notes=member_data.notes, + is_active=member_data.isActive, + ) + return db_member_to_api(db_member) + + +@app.put("/api/members/{member_id}", response_model=Member) +async def update_member(member_id: int, member_data: Member, repos: dict = Depends(get_repositories)): + existing_member = repos["member_repo"].get_by_id(member_id) + if not existing_member: + raise HTTPException(status_code=404, detail="Member not found") + + # Use the base repository _update method + updates = { + "FirstName": member_data.firstName, + "LastName": member_data.lastName, + "Email": member_data.email, + "PhoneNumber": member_data.phoneNumber, + "ClassificationId": member_data.classificationId, + "Notes": member_data.notes, + "IsActive": member_data.isActive, + } + repos["member_repo"]._update("Members", "MemberId", member_id, updates) + + # Return the updated member + updated_member = repos["member_repo"].get_by_id(member_id) + return db_member_to_api(updated_member) + + +@app.delete("/api/members/{member_id}") +async def delete_member(member_id: int, repos: dict = Depends(get_repositories)): + existing_member = repos["member_repo"].get_by_id(member_id) + if not existing_member: + raise HTTPException(status_code=404, detail="Member not found") + + repos["member_repo"]._delete("Members", "MemberId", member_id) + return {"message": "Member deleted successfully"} + + +@app.get("/api/members/{member_id}/schedules", response_model=List[Schedule]) +async def get_member_schedules(member_id: int, repos: dict = Depends(get_repositories)): + existing_member = repos["member_repo"].get_by_id(member_id) + if not existing_member: + raise HTTPException(status_code=404, detail="Member not found") + + # Get all schedules and filter by member ID (since there's no specific method) + all_schedules = repos["schedule_repo"].list_all() + member_schedules = [s for s in all_schedules if s.MemberId == member_id] + return [db_schedule_to_api(schedule) for schedule in member_schedules] + + +# Classification endpoints +@app.get("/api/classifications", response_model=List[Classification]) +async def get_classifications(repos: dict = Depends(get_repositories)): + db_classifications = repos["classification_repo"].list_all() + return [db_classification_to_api(classification) for classification in db_classifications] + + +# Service endpoints +@app.get("/api/services", response_model=List[Service]) +async def get_services(repos: dict = Depends(get_repositories)): + db_services = repos["service_repo"].list_all() + return [db_service_to_api(service) for service in db_services] + + +@app.get("/api/services/{service_id}", response_model=Service) +async def get_service(service_id: int, repos: dict = Depends(get_repositories)): + db_service = repos["service_repo"].get_by_id(service_id) + if not db_service: + raise HTTPException(status_code=404, detail="Service not found") + return db_service_to_api(db_service) + + +@app.post("/api/services", response_model=Service) +async def create_service(service_data: ServiceCreate, repos: dict = Depends(get_repositories)): + db_service = repos["service_repo"].create( + service_type_id=service_data.serviceTypeId, + service_date=service_data.serviceDate, + ) + return db_service_to_api(db_service) + + +# Service Type endpoints +@app.get("/api/service-types", response_model=List[ServiceType]) +async def get_service_types(repos: dict = Depends(get_repositories)): + db_service_types = repos["service_type_repo"].list_all() + return [db_service_type_to_api(service_type) for service_type in db_service_types] + + +# Schedule endpoints +@app.get("/api/schedules", response_model=List[Schedule]) +async def get_schedules(repos: dict = Depends(get_repositories)): + db_schedules = repos["schedule_repo"].list_all() + return [db_schedule_to_api(schedule) for schedule in db_schedules] + + +@app.get("/api/schedules/{schedule_id}", response_model=Schedule) +async def get_schedule(schedule_id: int, repos: dict = Depends(get_repositories)): + db_schedule = repos["schedule_repo"].get_by_id(schedule_id) + if not db_schedule: + raise HTTPException(status_code=404, detail="Schedule not found") + return db_schedule_to_api(db_schedule) + + +@app.post("/api/schedules/{schedule_id}/accept", response_model=Schedule) +async def accept_schedule(schedule_id: int, repos: dict = Depends(get_repositories)): + db_schedule = repos["schedule_repo"].get_by_id(schedule_id) + if not db_schedule: + raise HTTPException(status_code=404, detail="Schedule not found") + + repos["schedule_repo"].mark_accepted(schedule_id) + + # Return the updated schedule + updated_schedule = repos["schedule_repo"].get_by_id(schedule_id) + return db_schedule_to_api(updated_schedule) + + +@app.post("/api/schedules/{schedule_id}/decline", response_model=Schedule) +async def decline_schedule( + schedule_id: int, + decline_data: DeclineRequest, + repos: dict = Depends(get_repositories), + scheduling_service: SchedulingService = Depends(get_scheduling_service) +): + db_schedule = repos["schedule_repo"].get_by_id(schedule_id) + if not db_schedule: + raise HTTPException(status_code=404, detail="Schedule not found") + + # Use the scheduling service to decline (handles the business logic) + scheduling_service.decline_service_for_user( + member_id=db_schedule.MemberId, + service_id=db_schedule.ServiceId, + reason=decline_data.reason, + ) + + # Return the updated schedule + updated_schedule = repos["schedule_repo"].get_by_id(schedule_id) + return db_schedule_to_api(updated_schedule) + + +@app.delete("/api/schedules/{schedule_id}") +async def remove_schedule(schedule_id: int, repos: dict = Depends(get_repositories)): + existing_schedule = repos["schedule_repo"].get_by_id(schedule_id) + if not existing_schedule: + raise HTTPException(status_code=404, detail="Schedule not found") + + repos["schedule_repo"]._delete("Schedules", "ScheduleId", schedule_id) + return {"message": "Schedule removed successfully"} + + +@app.post("/api/schedules/schedule-next", response_model=Optional[Schedule]) +async def schedule_next_member( + request: ScheduleNextRequest, + scheduling_service: SchedulingService = Depends(get_scheduling_service), + repos: dict = Depends(get_repositories) +): + result = scheduling_service.schedule_next_member( + classification_ids=request.classificationIds, + service_id=request.serviceId, + only_active=True, + exclude_member_ids=set(), + ) + + if not result: + raise HTTPException(status_code=404, detail="No eligible member found") + + # result is a tuple: (schedule_id, first_name, last_name, member_id) + schedule_id = result[0] + db_schedule = repos["schedule_repo"].get_by_id(schedule_id) + return db_schedule_to_api(db_schedule) + + +# Health check endpoint +@app.get("/api/health") +async def health_check(): + return {"status": "healthy", "message": "NimbusFlow API is running"} \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..f3bf7f6 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,19 @@ +annotated-types==0.7.0 +anyio==4.10.0 +click==8.2.1 +fastapi==0.116.1 +h11==0.16.0 +httptools==0.6.4 +idna==3.10 +pydantic==2.11.7 +pydantic_core==2.33.2 +python-dotenv==1.1.1 +PyYAML==6.0.2 +sniffio==1.3.1 +starlette==0.47.3 +typing-inspection==0.4.1 +typing_extensions==4.15.0 +uvicorn==0.35.0 +uvloop==0.21.0 +watchfiles==1.1.0 +websockets==15.0.1 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..f81e3e3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,368 @@ +# Build results +bin/ +obj/ +out/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ + +# Visual Studio Code +.vscode/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these files may be created as clear text +*.azurePubxml + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +CDF_Data/ +l3codegen.ps1 + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml \ No newline at end of file diff --git a/frontend/Components/Pages/Home.razor b/frontend/Components/Pages/Home.razor index 7a0b250..e69e8fc 100644 --- a/frontend/Components/Pages/Home.razor +++ b/frontend/Components/Pages/Home.razor @@ -106,7 +106,7 @@ { // Load dashboard data var members = await ApiService.GetMembersAsync(); - activeMemberCount = members.Count(m => m.IsActive); + activeMemberCount = members.Count(m => m.IsActive == 1); var schedules = await ApiService.GetSchedulesAsync(); recentSchedules = schedules.OrderByDescending(s => s.ScheduledAt).ToList(); diff --git a/frontend/Components/Pages/Members.razor b/frontend/Components/Pages/Members.razor index b473473..7876365 100644 --- a/frontend/Components/Pages/Members.razor +++ b/frontend/Components/Pages/Members.razor @@ -55,7 +55,7 @@ else if (members.Any())