feat(frontend+backend): connect api backend with frontend
This commit is contained in:
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal 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
41
.vscode/tasks.json
vendored
Normal 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
306
CLAUDE.md
@@ -2,145 +2,295 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
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
|
### Architecture Overview
|
||||||
- **Frontend**: .NET Blazor Server application with Bootstrap UI
|
|
||||||
|
|
||||||
### 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:
|
The backend follows a **layered architecture** with clear separation of concerns:
|
||||||
|
|
||||||
```
|
```
|
||||||
backend/
|
backend/
|
||||||
├── db/ # Database connection layer
|
├── api/ # FastAPI REST API server
|
||||||
├── models/ # Data models and enums
|
├── cli/ # Command-line interface
|
||||||
├── repositories/ # Data access layer
|
├── db/ # Database connection layer
|
||||||
├── services/ # Business logic layer
|
├── models/ # Data models and enums
|
||||||
└── tests/ # Test suite
|
├── 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
|
#### API Layer (`api/`)
|
||||||
- **Repository Pattern**: Each domain entity has its own repository (Member, Service, Classification, etc.)
|
- **app.py**: FastAPI application with comprehensive REST endpoints
|
||||||
- **SchedulingService** (`services/scheduling_service.py`): Core business logic for round-robin scheduling with boost/cooldown algorithms
|
- **__main__.py**: Uvicorn server entry point with auto-reload
|
||||||
- **Data Models** (`models/dataclasses.py`): Dataclasses with SQLite row conversion utilities
|
- 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:
|
.NET 8 Blazor Server application with the following structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
frontend/
|
frontend/
|
||||||
├── Components/
|
├── Components/
|
||||||
│ ├── Layout/ # Layout components (NavMenu, MainLayout)
|
│ ├── Layout/ # Layout components (NavMenu, MainLayout)
|
||||||
│ └── Pages/ # Razor pages (Dashboard, Members, Schedules, Services)
|
│ ├── Pages/ # Razor pages (Members, Schedules, Services)
|
||||||
├── Models/ # C# models matching backend data structure
|
│ ├── App.razor # Root application component
|
||||||
|
│ └── _Imports.razor # Global imports
|
||||||
|
├── Models/ # C# models matching backend structure
|
||||||
├── Services/ # HTTP client services for API communication
|
├── 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
|
#### Services Layer
|
||||||
- **Models** (`Models/Member.cs`): C# data models (Member, Schedule, Service, Classification, etc.)
|
- **ApiService.cs**: HTTP client implementation with JSON serialization
|
||||||
- **Razor Components**: Interactive pages for member management, scheduling, and service administration
|
- **IApiService.cs**: Service interface for dependency injection
|
||||||
- **Bootstrap UI**: Responsive design with Bootstrap 5 styling
|
- 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
|
## Development Commands
|
||||||
|
|
||||||
### Backend
|
### Backend Development
|
||||||
|
|
||||||
**Setup:**
|
**Environment Setup:**
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
# Activate virtual environment (already exists)
|
# Virtual environment is pre-configured in .venv/
|
||||||
source venv/bin/activate
|
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:**
|
**Testing:**
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
# Run tests using pytest from virtual environment
|
source .venv/bin/activate
|
||||||
venv/bin/pytest
|
pytest # Run all tests
|
||||||
# Run specific test file
|
pytest tests/repositories/ # Run repository tests
|
||||||
venv/bin/pytest tests/repositories/test_member.py
|
pytest --cov # Run with coverage
|
||||||
# Run with coverage
|
|
||||||
venv/bin/pytest --cov
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Run Application:**
|
### Frontend Development
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
# Run the demo script
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
**Setup:**
|
**Setup:**
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
# Restore NuGet packages
|
|
||||||
dotnet restore
|
dotnet restore
|
||||||
```
|
```
|
||||||
|
|
||||||
**Development:**
|
**Development Server:**
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
# Start development server (with hot reload)
|
dotnet watch # Hot reload development
|
||||||
dotnet watch
|
# Or: dotnet run
|
||||||
# Or run without watch
|
```
|
||||||
dotnet run
|
|
||||||
|
|
||||||
# Build for production
|
**Build & Deploy:**
|
||||||
dotnet build
|
```bash
|
||||||
# Publish for deployment
|
cd frontend
|
||||||
dotnet publish -c Release
|
dotnet build # Development build
|
||||||
|
dotnet build -c Release # Production build
|
||||||
|
dotnet publish -c Release # Publish for deployment
|
||||||
```
|
```
|
||||||
|
|
||||||
**Access:**
|
**Access:**
|
||||||
- Development: https://localhost:5001 (HTTPS) or http://localhost:5000 (HTTP)
|
- 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
|
## Core Business Logic
|
||||||
|
|
||||||
The **SchedulingService** implements a sophisticated member scheduling algorithm:
|
### Scheduling Algorithm
|
||||||
|
|
||||||
1. **Round-robin scheduling** based on `Members.LastAcceptedAt` timestamps
|
The **SchedulingService** implements sophisticated member scheduling:
|
||||||
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
|
1. **Round-robin fairness**: Based on `LastAcceptedAt` timestamps
|
||||||
4. **Service availability** - members must be eligible for the specific service type
|
2. **Decline boost**: 5-day priority for recently declined members
|
||||||
5. **Status constraints** - prevents double-booking (pending/accepted/declined statuses)
|
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:**
|
**Key Methods:**
|
||||||
- `schedule_next_member()` - Core scheduling logic with multi-classification support
|
- `schedule_next_member(classification_ids, service_id)`: Core scheduling with multi-role support
|
||||||
- `decline_service_for_user()` - Handle member declining assignments
|
- `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:
|
**Member Management:**
|
||||||
- `Members` - Core member data with scheduling timestamps
|
- `GET /api/members` - List all members
|
||||||
- `Services` - Service instances on specific dates
|
- `GET /api/members/{id}` - Get member details
|
||||||
- `Schedules` - Member-service assignments with status tracking
|
- `POST /api/members` - Create new member
|
||||||
- `Classifications` - Member roles (Soprano, Alto, Tenor, Baritone)
|
- `PUT /api/members/{id}` - Update member
|
||||||
- `ServiceTypes` - Time slots (9AM, 11AM, 6PM)
|
- `DELETE /api/members/{id}` - Delete member
|
||||||
- `ServiceAvailability` - Member eligibility for service types
|
|
||||||
|
|
||||||
**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
|
## Testing
|
||||||
|
|
||||||
Backend uses **pytest** with fixtures for database setup. Tests cover:
|
**Backend Testing with pytest:**
|
||||||
- Repository layer functionality
|
- Repository layer unit tests with in-memory SQLite
|
||||||
- Business logic in services
|
- Service layer business logic tests
|
||||||
- Database schema constraints
|
- 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
7
backend/api/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
FastAPI module for NimbusFlow backend.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .app import app
|
||||||
|
|
||||||
|
__all__ = ["app"]
|
||||||
14
backend/api/__main__.py
Normal file
14
backend/api/__main__.py
Normal 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
479
backend/api/app.py
Normal 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 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"}
|
||||||
19
backend/requirements.txt
Normal file
19
backend/requirements.txt
Normal 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
368
frontend/.gitignore
vendored
Normal 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
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
{
|
{
|
||||||
// Load dashboard data
|
// Load dashboard data
|
||||||
var members = await ApiService.GetMembersAsync();
|
var members = await ApiService.GetMembersAsync();
|
||||||
activeMemberCount = members.Count(m => m.IsActive);
|
activeMemberCount = members.Count(m => m.IsActive == 1);
|
||||||
|
|
||||||
var schedules = await ApiService.GetSchedulesAsync();
|
var schedules = await ApiService.GetSchedulesAsync();
|
||||||
recentSchedules = schedules.OrderByDescending(s => s.ScheduledAt).ToList();
|
recentSchedules = schedules.OrderByDescending(s => s.ScheduledAt).ToList();
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ else if (members.Any())
|
|||||||
<td>@member.Email</td>
|
<td>@member.Email</td>
|
||||||
<td>@member.PhoneNumber</td>
|
<td>@member.PhoneNumber</td>
|
||||||
<td>
|
<td>
|
||||||
@if (member.IsActive)
|
@if (member.IsActive == 1)
|
||||||
{
|
{
|
||||||
<span class="badge bg-success">Active</span>
|
<span class="badge bg-success">Active</span>
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ else
|
|||||||
private bool showInactiveMembers = false;
|
private bool showInactiveMembers = false;
|
||||||
|
|
||||||
private IEnumerable<Member> filteredMembers =>
|
private IEnumerable<Member> filteredMembers =>
|
||||||
showInactiveMembers ? members : members.Where(m => m.IsActive);
|
showInactiveMembers ? members : members.Where(m => m.IsActive == 1);
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace NimbusFlow.Frontend.Models
|
|||||||
public string? PhoneNumber { get; set; }
|
public string? PhoneNumber { get; set; }
|
||||||
public int? ClassificationId { get; set; }
|
public int? ClassificationId { get; set; }
|
||||||
public string? Notes { 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? LastScheduledAt { get; set; }
|
||||||
public DateTime? LastAcceptedAt { get; set; }
|
public DateTime? LastAcceptedAt { get; set; }
|
||||||
public DateTime? LastDeclinedAt { get; set; }
|
public DateTime? LastDeclinedAt { get; set; }
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ builder.Services.AddHttpClient<IApiService, ApiService>(client =>
|
|||||||
client.BaseAddress = new Uri("http://localhost:8000/api/"); // Python backend API endpoint
|
client.BaseAddress = new Uri("http://localhost:8000/api/"); // Python backend API endpoint
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddScoped<IApiService, ApiService>();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -13,7 +13,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("NimbusFlow.Frontend")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("NimbusFlow.Frontend")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[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.AssemblyProductAttribute("NimbusFlow.Frontend")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("NimbusFlow.Frontend")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("NimbusFlow.Frontend")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
903b0bd78cb44f079d9cac7109f5762d014e600b7d6925e89a6fe451b40172ed
|
193745f5da23d1ece0e2e39cb2746e078b8a35590f34b2bb7083bc28cced0dfb
|
||||||
|
|||||||
@@ -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.Development.json
|
||||||
/home/t2/Development/nimbusflow/frontend/bin/Debug/net8.0/appsettings.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
|
/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.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.dll
|
||||||
/home/t2/Development/nimbusflow/frontend/bin/Debug/net8.0/NimbusFlow.Frontend.pdb
|
/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.build.json
|
||||||
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/staticwebassets.development.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
|
/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/staticwebassets/msbuild.NimbusFlow.Frontend.Microsoft.AspNetCore.StaticWebAssets.props
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user