diff --git a/CLAUDE.md b/CLAUDE.md
index 019b8bb..132ca60 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -34,7 +34,7 @@ backend/
#### 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`
+- Supports CORS for frontend communication on `localhost:5059`
- Pydantic models for request/response validation with C# naming convention compatibility
#### CLI Layer (`cli/`)
@@ -204,7 +204,7 @@ dotnet publish -c Release # Publish for deployment
```
**Access:**
-- Development: https://localhost:5001 (HTTPS) or http://localhost:5000 (HTTP)
+- Development: http://localhost:5059
- Expects backend API at http://localhost:8000/api/
## Core Business Logic
@@ -286,11 +286,105 @@ tests/
9. **Testing**: Add tests for new functionality
10. **Integration**: Test full stack communication
+## Color Palette & Design System
+
+The NimbusFlow brand uses a sophisticated color palette inspired by the logo's golden cloud and deep navy background, creating a professional yet warm aesthetic suitable for church ministry applications.
+
+### Primary Colors
+
+```css
+:root {
+ /* Primary Brand Colors */
+ --nimbus-navy: #1a2332; /* Deep navy background */
+ --nimbus-navy-light: #2a3441; /* Lighter navy variant */
+ --nimbus-navy-dark: #0f1419; /* Darker navy variant */
+
+ /* Golden Accent Colors */
+ --nimbus-gold: #ffd700; /* Primary gold */
+ --nimbus-gold-light: #ffed4e; /* Light gold highlight */
+ --nimbus-gold-dark: #e6c200; /* Dark gold shadow */
+ --nimbus-amber: #ffb347; /* Warm amber accent */
+
+ /* Neutral Colors */
+ --nimbus-white: #ffffff; /* Pure white */
+ --nimbus-gray-100: #f8f9fa; /* Light gray */
+ --nimbus-gray-300: #dee2e6; /* Medium light gray */
+ --nimbus-gray-600: #6c757d; /* Medium gray */
+ --nimbus-gray-800: #343a40; /* Dark gray */
+}
+```
+
+### Bootstrap Integration
+
+```css
+/* Override Bootstrap variables */
+:root {
+ --bs-primary: var(--nimbus-navy);
+ --bs-primary-rgb: 26, 35, 50;
+ --bs-secondary: var(--nimbus-gold);
+ --bs-secondary-rgb: 255, 215, 0;
+ --bs-success: #28a745;
+ --bs-warning: var(--nimbus-amber);
+ --bs-info: #17a2b8;
+ --bs-danger: #dc3545;
+}
+```
+
+### Usage Guidelines
+
+**Primary Navy (`--nimbus-navy`):**
+- Navigation bars and headers
+- Card backgrounds in dark mode
+- Button primary states
+- Footer backgrounds
+
+**Golden Accent (`--nimbus-gold`):**
+- Call-to-action buttons
+- Active states and highlights
+- Icons and accent elements
+- Success indicators
+
+**Amber (`--nimbus-amber`):**
+- Warning states
+- Pending status indicators
+- Hover effects on interactive elements
+
+### Accessibility Considerations
+
+All color combinations meet WCAG 2.1 AA standards:
+- **Navy on White**: 12.6:1 contrast ratio
+- **Gold on Navy**: 4.8:1 contrast ratio
+- **White on Navy**: 12.6:1 contrast ratio
+- **Dark Gray on Light Gray**: 7.2:1 contrast ratio
+
+### Implementation Example
+
+```razor
+
+
+
+
+
+
+
+
+ Pending
+
+```
+
## 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
+- **CORS**: Configured for frontend origins on localhost:5059
- **Virtual Environment**: Pre-configured Python environment in `backend/.venv/`
-- **Auto-reload**: Both backend (Uvicorn) and frontend (dotnet watch) support hot reload
\ No newline at end of file
+- **Auto-reload**: Both backend (Uvicorn) and frontend (dotnet watch) support hot reload
+- **Brand Colors**: Use the defined color palette consistently across all UI components
\ No newline at end of file
diff --git a/backend/api/app.py b/backend/api/app.py
index 2315266..e0c6873 100644
--- a/backend/api/app.py
+++ b/backend/api/app.py
@@ -85,6 +85,7 @@ class Member(BaseModel):
email: Optional[str] = Field(default=None, alias="Email")
phoneNumber: Optional[str] = Field(default=None, alias="PhoneNumber")
classificationId: Optional[int] = Field(default=None, alias="ClassificationId")
+ classificationName: Optional[str] = Field(default=None, alias="ClassificationName")
notes: Optional[str] = Field(default=None, alias="Notes")
isActive: int = Field(default=1, alias="IsActive")
lastScheduledAt: Optional[datetime] = Field(default=None, alias="LastScheduledAt")
@@ -121,6 +122,7 @@ class Service(BaseModel):
serviceId: int = Field(alias="ServiceId")
serviceTypeId: int = Field(alias="ServiceTypeId")
serviceDate: date = Field(alias="ServiceDate")
+ serviceTypeName: Optional[str] = Field(default=None, alias="ServiceTypeName")
class Config:
populate_by_name = True
@@ -152,6 +154,8 @@ class Schedule(BaseModel):
declinedAt: Optional[datetime] = Field(default=None, alias="DeclinedAt")
expiresAt: Optional[datetime] = Field(default=None, alias="ExpiresAt")
declineReason: Optional[str] = Field(default=None, alias="DeclineReason")
+ member: Optional["Member"] = Field(default=None, alias="Member")
+ service: Optional["Service"] = Field(default=None, alias="Service")
class Config:
populate_by_name = True
@@ -206,7 +210,7 @@ def get_scheduling_service(repos: dict = Depends(get_repositories)):
# Helper functions to convert between DB and API models
-def db_member_to_api(db_member: DbMember) -> Member:
+def db_member_to_api(db_member: DbMember, classification_name: str = None) -> Member:
return Member(
MemberId=db_member.MemberId,
FirstName=db_member.FirstName,
@@ -214,6 +218,7 @@ def db_member_to_api(db_member: DbMember) -> Member:
Email=db_member.Email,
PhoneNumber=db_member.PhoneNumber,
ClassificationId=db_member.ClassificationId,
+ ClassificationName=classification_name,
Notes=db_member.Notes,
IsActive=db_member.IsActive,
LastScheduledAt=db_member.LastScheduledAt,
@@ -247,12 +252,18 @@ def db_classification_to_api(db_classification: DbClassification) -> Classificat
)
-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_to_api(db_service: DbService, service_type: DbServiceType = None) -> Service:
+ service_dict = {
+ "ServiceId": db_service.ServiceId,
+ "ServiceTypeId": db_service.ServiceTypeId,
+ "ServiceDate": db_service.ServiceDate,
+ }
+
+ # Add service type name if provided
+ if service_type:
+ service_dict["ServiceTypeName"] = service_type.TypeName
+
+ return Service(**service_dict)
def db_service_type_to_api(db_service_type: DbServiceType) -> ServiceType:
@@ -262,18 +273,28 @@ def db_service_type_to_api(db_service_type: DbServiceType) -> ServiceType:
)
-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,
- )
+def db_schedule_to_api(db_schedule: DbSchedule, member: DbMember = None, service: DbService = None) -> Schedule:
+ schedule_dict = {
+ "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,
+ }
+
+ # Add nested member data if provided
+ if member:
+ schedule_dict["Member"] = db_member_to_api(member)
+
+ # Add nested service data if provided
+ if service:
+ schedule_dict["Service"] = db_service_to_api(service)
+
+ return Schedule(**schedule_dict)
# API Endpoints
@@ -282,7 +303,19 @@ def db_schedule_to_api(db_schedule: DbSchedule) -> Schedule:
@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]
+ result = []
+
+ for member in db_members:
+ # Fetch classification name if classification ID exists
+ classification_name = None
+ if member.ClassificationId:
+ classification = repos["classification_repo"].get_by_id(member.ClassificationId)
+ if classification:
+ classification_name = classification.ClassificationName
+
+ result.append(db_member_to_api(member, classification_name))
+
+ return result
@app.get("/api/members/{member_id}", response_model=Member)
@@ -394,7 +427,40 @@ async def get_service_types(repos: dict = Depends(get_repositories)):
@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]
+ result = []
+
+ for schedule in db_schedules:
+ # Fetch related member and service data
+ member = repos["member_repo"].get_by_id(schedule.MemberId)
+ service = repos["service_repo"].get_by_id(schedule.ServiceId)
+
+ # Fetch service type if service exists
+ service_with_type = None
+ if service:
+ service_type = repos["service_type_repo"].get_by_id(service.ServiceTypeId)
+ service_with_type = db_service_to_api(service, service_type)
+
+ # Convert to API format with nested data
+ schedule_dict = {
+ "ScheduleId": schedule.ScheduleId,
+ "ServiceId": schedule.ServiceId,
+ "MemberId": schedule.MemberId,
+ "Status": schedule.Status,
+ "ScheduledAt": schedule.ScheduledAt,
+ "AcceptedAt": schedule.AcceptedAt,
+ "DeclinedAt": schedule.DeclinedAt,
+ "ExpiresAt": schedule.ExpiresAt,
+ "DeclineReason": schedule.DeclineReason,
+ }
+
+ if member:
+ schedule_dict["Member"] = db_member_to_api(member)
+ if service_with_type:
+ schedule_dict["Service"] = service_with_type
+
+ result.append(Schedule(**schedule_dict))
+
+ return result
@app.get("/api/schedules/{schedule_id}", response_model=Schedule)
@@ -402,7 +468,36 @@ 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)
+
+ # Fetch related member and service data
+ member = repos["member_repo"].get_by_id(db_schedule.MemberId)
+ service = repos["service_repo"].get_by_id(db_schedule.ServiceId)
+
+ # Fetch service type if service exists
+ service_with_type = None
+ if service:
+ service_type = repos["service_type_repo"].get_by_id(service.ServiceTypeId)
+ service_with_type = db_service_to_api(service, service_type)
+
+ # Convert to API format with nested data
+ schedule_dict = {
+ "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,
+ }
+
+ if member:
+ schedule_dict["Member"] = db_member_to_api(member)
+ if service_with_type:
+ schedule_dict["Service"] = service_with_type
+
+ return Schedule(**schedule_dict)
@app.post("/api/schedules/{schedule_id}/accept", response_model=Schedule)
diff --git a/frontend/Components/App.razor b/frontend/Components/App.razor
index cbca8d7..51a647f 100644
--- a/frontend/Components/App.razor
+++ b/frontend/Components/App.razor
@@ -7,8 +7,10 @@