feat(frontend+backend): connect api backend with frontend

This commit is contained in:
2025-08-30 21:54:36 -04:00
parent 6063ed62e0
commit 133efdddea
21 changed files with 1202 additions and 91 deletions

35
.vscode/launch.json vendored Normal file
View File

@@ -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"
}
]
}

41
.vscode/tasks.json vendored Normal file
View File

@@ -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"
}
]
}

306
CLAUDE.md
View File

@@ -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.
**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

7
backend/api/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
"""
FastAPI module for NimbusFlow backend.
"""
from .app import app
__all__ = ["app"]

14
backend/api/__main__.py Normal file
View File

@@ -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"
)

479
backend/api/app.py Normal file
View File

@@ -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 dictionarylike 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"}

19
backend/requirements.txt Normal file
View File

@@ -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

368
frontend/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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();

View File

@@ -55,7 +55,7 @@ else if (members.Any())
<td>@member.Email</td>
<td>@member.PhoneNumber</td>
<td>
@if (member.IsActive)
@if (member.IsActive == 1)
{
<span class="badge bg-success">Active</span>
}
@@ -134,7 +134,7 @@ else
private bool showInactiveMembers = false;
private IEnumerable<Member> filteredMembers =>
showInactiveMembers ? members : members.Where(m => m.IsActive);
showInactiveMembers ? members : members.Where(m => m.IsActive == 1);
protected override async Task OnInitializedAsync()
{

View File

@@ -9,7 +9,7 @@ namespace NimbusFlow.Frontend.Models
public string? PhoneNumber { get; set; }
public int? ClassificationId { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; } = true;
public int IsActive { get; set; } = 1;
public DateTime? LastScheduledAt { get; set; }
public DateTime? LastAcceptedAt { get; set; }
public DateTime? LastDeclinedAt { get; set; }

View File

@@ -13,8 +13,6 @@ builder.Services.AddHttpClient<IApiService, ApiService>(client =>
client.BaseAddress = new Uri("http://localhost:8000/api/"); // Python backend API endpoint
});
builder.Services.AddScoped<IApiService, ApiService>();
var app = builder.Build();
// Configure the HTTP request pipeline.

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("NimbusFlow.Frontend")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+3b9c074bc7f40cf57f01ecdca33cb850ee4eb7dc")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+6063ed62e03c4a6c1d5aea04009fca83dcfa3ff6")]
[assembly: System.Reflection.AssemblyProductAttribute("NimbusFlow.Frontend")]
[assembly: System.Reflection.AssemblyTitleAttribute("NimbusFlow.Frontend")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
903b0bd78cb44f079d9cac7109f5762d014e600b7d6925e89a6fe451b40172ed
193745f5da23d1ece0e2e39cb2746e078b8a35590f34b2bb7083bc28cced0dfb

View File

@@ -1,8 +1,3 @@
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.GeneratedMSBuildEditorConfig.editorconfig
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.AssemblyInfoInputs.cache
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.AssemblyInfo.cs
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.csproj.CoreCompileInputs.cache
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.MvcApplicationPartsAssemblyInfo.cache
/home/t2/Development/nimbusflow/frontend/bin/Debug/net8.0/appsettings.Development.json
/home/t2/Development/nimbusflow/frontend/bin/Debug/net8.0/appsettings.json
/home/t2/Development/nimbusflow/frontend/bin/Debug/net8.0/NimbusFlow.Frontend.staticwebassets.runtime.json
@@ -11,6 +6,11 @@
/home/t2/Development/nimbusflow/frontend/bin/Debug/net8.0/NimbusFlow.Frontend.runtimeconfig.json
/home/t2/Development/nimbusflow/frontend/bin/Debug/net8.0/NimbusFlow.Frontend.dll
/home/t2/Development/nimbusflow/frontend/bin/Debug/net8.0/NimbusFlow.Frontend.pdb
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.GeneratedMSBuildEditorConfig.editorconfig
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.AssemblyInfoInputs.cache
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.AssemblyInfo.cs
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.csproj.CoreCompileInputs.cache
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/NimbusFlow.Frontend.MvcApplicationPartsAssemblyInfo.cache
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/staticwebassets.build.json
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/staticwebassets.development.json
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/staticwebassets/msbuild.NimbusFlow.Frontend.Microsoft.AspNetCore.StaticWebAssets.props