feat(frontend+backend): add theming to the blazor frontend

This commit is contained in:
2025-09-04 22:16:04 -04:00
parent 133efdddea
commit 0768e4816d
30 changed files with 1544 additions and 686 deletions

102
CLAUDE.md
View File

@@ -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
<!-- Primary navigation -->
<nav class="navbar navbar-expand-lg" style="background-color: var(--nimbus-navy);">
<div class="navbar-brand text-white">
<i class="bi bi-cloud" style="color: var(--nimbus-gold);"></i>
NimbusFlow
</div>
</nav>
<!-- Action button -->
<button class="btn" style="background-color: var(--nimbus-gold); color: var(--nimbus-navy);">
Schedule Next Member
</button>
<!-- Status badges -->
<span class="badge" style="background-color: var(--nimbus-amber); color: var(--nimbus-navy);">
Pending
</span>
```
## 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
- **Auto-reload**: Both backend (Uvicorn) and frontend (dotnet watch) support hot reload
- **Brand Colors**: Use the defined color palette consistently across all UI components

View File

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

View File

@@ -7,8 +7,10 @@
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="css/nimbusflow.css" />
<link rel="stylesheet" href="NimbusFlow.Frontend.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet" />
<HeadOutlet />
</head>

View File

@@ -1,15 +1,11 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<div class="sidebar nimbus-sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>

View File

@@ -1,6 +1,8 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">NimbusFlow</a>
<a class="navbar-brand" href="">
<i class="bi bi-cloud nimbus-brand-icon" aria-hidden="true"></i>NimbusFlow
</a>
</div>
</div>
@@ -8,27 +10,27 @@
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<div class="nav-item">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Dashboard
<i class="bi bi-house-door-fill me-2" aria-hidden="true"></i> Dashboard
</NavLink>
</div>
<div class="nav-item px-3">
<div class="nav-item">
<NavLink class="nav-link" href="members">
<span class="bi bi-people-fill-nav-menu" aria-hidden="true"></span> Members
<i class="bi bi-people-fill me-2" aria-hidden="true"></i> Members
</NavLink>
</div>
<div class="nav-item px-3">
<div class="nav-item">
<NavLink class="nav-link" href="schedules">
<span class="bi bi-calendar-event-fill-nav-menu" aria-hidden="true"></span> Schedules
<i class="bi bi-calendar-event-fill me-2" aria-hidden="true"></i> Schedules
</NavLink>
</div>
<div class="nav-item px-3">
<div class="nav-item">
<NavLink class="nav-link" href="services">
<span class="bi bi-calendar-plus-fill-nav-menu" aria-hidden="true"></span> Services
<i class="bi bi-calendar-plus-fill me-2" aria-hidden="true"></i> Services
</NavLink>
</div>
</nav>

View File

@@ -5,90 +5,71 @@
<PageTitle>NimbusFlow Dashboard</PageTitle>
<h1>NimbusFlow Dashboard</h1>
<h1 class="nimbus-page-title">
<i class="bi bi-speedometer2 me-3"></i>NimbusFlow Dashboard
</h1>
<div class="row">
<div class="row g-4">
<div class="col-md-3">
<div class="card text-white bg-primary mb-3">
<div class="card-header">Active Members</div>
<div class="card-body">
<h4 class="card-title">@activeMemberCount</h4>
<div class="card nimbus-dashboard-card card-members mb-3">
<div class="card-header d-flex align-items-center">
<i class="bi bi-people-fill me-2"></i>
Active Members
</div>
<div class="card-body text-center">
<h2 class="card-title mb-0">@activeMemberCount</h2>
<small class="opacity-75">Currently Active</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success mb-3">
<div class="card-header">Pending Schedules</div>
<div class="card-body">
<h4 class="card-title">@pendingScheduleCount</h4>
<div class="card nimbus-dashboard-card card-schedules mb-3">
<div class="card-header d-flex align-items-center">
<i class="bi bi-calendar-event-fill me-2"></i>
Pending Schedules
</div>
<div class="card-body text-center">
<h2 class="card-title mb-0">@pendingScheduleCount</h2>
<small class="opacity-75">Awaiting Response</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-warning mb-3">
<div class="card-header">Upcoming Services</div>
<div class="card-body">
<h4 class="card-title">@upcomingServiceCount</h4>
<div class="card nimbus-dashboard-card card-services mb-3">
<div class="card-header d-flex align-items-center">
<i class="bi bi-calendar-plus-fill me-2"></i>
Upcoming Services
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-info mb-3">
<div class="card-header">Total Classifications</div>
<div class="card-body">
<h4 class="card-title">@classificationCount</h4>
<div class="card-body text-center">
<h2 class="card-title mb-0">@upcomingServiceCount</h2>
<small class="opacity-75">This Week</small>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>Recent Schedules</h5>
</div>
<div class="card-body">
@if (recentSchedules.Any())
{
<div class="list-group">
@foreach (var schedule in recentSchedules.Take(5))
{
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">@($"{schedule.Member?.FullName}")</h6>
<small class="badge @GetStatusBadgeClass(schedule.Status)">@schedule.Status</small>
</div>
<p class="mb-1">Service: @schedule.Service?.ServiceDate.ToString("MMM dd, yyyy")</p>
<small>Scheduled: @schedule.ScheduledAt.ToString("MMM dd, yyyy HH:mm")</small>
</div>
}
</div>
}
else
{
<p class="text-muted">No recent schedules found.</p>
}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>Quick Actions</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="/schedules/create" class="btn btn-primary">Schedule Next Member</a>
<a href="/members/create" class="btn btn-success">Add New Member</a>
<a href="/services/create" class="btn btn-warning">Create New Service</a>
<a href="/schedules" class="btn btn-info">View All Schedules</a>
</div>
</div>
<div class="row mt-5">
<div class="col-md-12">
<div class="nimbus-quick-actions">
<h5><i class="bi bi-lightning-charge me-2"></i>Quick Actions</h5>
<a href="/schedules/schedule-next" class="btn btn-nimbus-primary me-3 mb-2">
<i class="bi bi-calendar-plus-fill me-2"></i>Schedule Next Member
</a>
<a href="/members" class="btn btn-success me-3 mb-2">
<i class="bi bi-person-plus-fill me-2"></i>Add New Member
</a>
<a href="/services" class="btn btn-outline-warning me-3 mb-2">
<i class="bi bi-gear-wide-connected me-2"></i>Create New Service
</a>
<a href="/schedules" class="btn btn-outline-secondary me-3 mb-2">
<i class="bi bi-list-check me-2"></i>View All Schedules
</a>
</div>
</div>
</div>
@@ -97,8 +78,6 @@
private int activeMemberCount = 0;
private int pendingScheduleCount = 0;
private int upcomingServiceCount = 0;
private int classificationCount = 0;
private List<Schedule> recentSchedules = new();
protected override async Task OnInitializedAsync()
{
@@ -109,14 +88,11 @@
activeMemberCount = members.Count(m => m.IsActive == 1);
var schedules = await ApiService.GetSchedulesAsync();
recentSchedules = schedules.OrderByDescending(s => s.ScheduledAt).ToList();
pendingScheduleCount = schedules.Count(s => s.Status == "pending");
var services = await ApiService.GetServicesAsync();
upcomingServiceCount = services.Count(s => s.ServiceDate >= DateTime.Today);
var classifications = await ApiService.GetClassificationsAsync();
classificationCount = classifications.Count;
}
catch (Exception ex)
{
@@ -124,15 +100,4 @@
Console.WriteLine($"Error loading dashboard data: {ex.Message}");
}
}
private string GetStatusBadgeClass(string status)
{
return status switch
{
"pending" => "bg-warning",
"accepted" => "bg-success",
"declined" => "bg-danger",
_ => "bg-secondary"
};
}
}

View File

@@ -8,22 +8,37 @@
<PageTitle>Members</PageTitle>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Members</h1>
<a href="/members/create" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add Member
<h1 class="nimbus-page-title">
<i class="bi bi-people-fill me-3"></i>Members
</h1>
<a href="/members/create" class="btn btn-nimbus-primary">
<i class="bi bi-person-plus-fill me-2"></i>Add Member
</a>
</div>
@if (loading)
{
<div class="text-center">
<div class="spinner-border" role="status">
<div class="text-center py-5">
<div class="spinner-border nimbus-spinner" role="status" style="width: 3rem; height: 3rem;">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-muted">Loading members...</p>
</div>
}
else if (members.Any())
{
<!-- Filter Controls -->
<div class="row mb-3">
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="showInactiveMembers" id="showInactiveCheck">
<label class="form-check-label" for="showInactiveCheck">
Show inactive members
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card">
@@ -50,18 +65,22 @@ else if (members.Any())
<strong>@member.FullName</strong>
</td>
<td>
<span class="badge bg-secondary">@member.ClassificationName</span>
<span class="badge badge-nimbus-classification">@member.ClassificationName</span>
</td>
<td>@member.Email</td>
<td>@member.PhoneNumber</td>
<td>
@if (member.IsActive == 1)
{
<span class="badge bg-success">Active</span>
<span class="badge badge-nimbus-active">
<i class="bi bi-check-circle-fill me-1"></i>Active
</span>
}
else
{
<span class="badge bg-danger">Inactive</span>
<span class="badge badge-nimbus-inactive">
<i class="bi bi-x-circle-fill me-1"></i>Inactive
</span>
}
</td>
<td>
@@ -77,17 +96,23 @@ else if (members.Any())
<td>
@if (member.DeclineStreak > 0)
{
<span class="badge bg-warning">@member.DeclineStreak</span>
<span class="badge badge-nimbus-pending">
<i class="bi bi-exclamation-triangle-fill me-1"></i>@member.DeclineStreak
</span>
}
else
{
<span class="text-muted">0</span>
<span class="text-muted"></span>
}
</td>
<td>
<div class="btn-group" role="group">
<a href="/members/@member.MemberId" class="btn btn-sm btn-outline-primary">View</a>
<a href="/members/@member.MemberId/edit" class="btn btn-sm btn-outline-warning">Edit</a>
<a href="/members/@member.MemberId" class="btn btn-sm btn-nimbus-secondary me-1">
<i class="bi bi-eye-fill me-1"></i>View
</a>
<a href="/members/@member.MemberId/edit" class="btn btn-sm btn-outline-warning">
<i class="bi bi-pencil-fill me-1"></i>Edit
</a>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmDelete(member)">Delete</button>
</div>
</td>
@@ -101,19 +126,12 @@ else if (members.Any())
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
Showing @filteredMembers.Count() of @members.Count members
</small>
<div class="form-check">
<input class="form-check-input" type="checkbox" @bind="showInactiveMembers" id="showInactive">
<label class="form-check-label" for="showInactive">
Show inactive members
</label>
</div>
</div>
<small class="text-muted">
Showing @filteredMembers.Count() of @members.Count members
</small>
</div>
</div>
}

View File

@@ -7,9 +7,11 @@
<PageTitle>Schedules</PageTitle>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Schedules</h1>
<a href="/schedules/create" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Schedule Member
<h1 class="nimbus-page-title">
<i class="bi bi-calendar3 me-3"></i>Schedules
</h1>
<a href="/schedules/create" class="btn btn-nimbus-primary">
<i class="bi bi-calendar-plus-fill me-2"></i>Schedule Member
</a>
</div>
@@ -69,7 +71,7 @@ else if (schedules.Any())
@schedule.Service?.ServiceDate.ToString("MMM dd, yyyy")
</td>
<td>
<span class="badge bg-info">@schedule.Service?.ServiceTypeName</span>
<span class="badge" style="background-color: var(--nimbus-gold); color: var(--nimbus-navy);">@schedule.Service?.ServiceTypeName</span>
</td>
<td>
<span class="badge @GetStatusBadgeClass(schedule.Status)">
@@ -95,18 +97,21 @@ else if (schedules.Any())
</td>
<td>
<div class="btn-group" role="group">
@if (schedule.Status == "pending")
{
<button class="btn btn-sm btn-success" @onclick="() => AcceptSchedule(schedule.ScheduleId)">
Accept
</button>
<button class="btn btn-sm btn-warning" @onclick="() => ShowDeclineModal(schedule)">
Decline
</button>
}
<a href="/schedules/@schedule.ScheduleId" class="btn btn-sm btn-outline-primary">View</a>
<button class="btn btn-sm btn-success me-1"
disabled="@(schedule.Status != "pending")"
@onclick="() => AcceptSchedule(schedule.ScheduleId)">
<i class="bi bi-check-circle-fill me-1"></i>Accept
</button>
<button class="btn btn-sm btn-warning me-1"
disabled="@(schedule.Status != "pending")"
@onclick="() => ShowDeclineModal(schedule)">
<i class="bi bi-x-circle-fill me-1"></i>Decline
</button>
<a href="/schedules/@schedule.ScheduleId" class="btn btn-sm btn-nimbus-secondary me-1">
<i class="bi bi-eye-fill me-1"></i>View
</a>
<button class="btn btn-sm btn-outline-danger" @onclick="() => ConfirmRemove(schedule)">
Remove
<i class="bi bi-trash-fill me-1"></i>Remove
</button>
</div>
</td>
@@ -219,10 +224,10 @@ else
{
return status switch
{
"pending" => "bg-warning text-dark",
"accepted" => "bg-success",
"declined" => "bg-danger",
_ => "bg-secondary"
"pending" => "badge-nimbus-pending",
"accepted" => "badge-nimbus-accepted",
"declined" => "badge-nimbus-declined",
_ => "badge-nimbus-inactive"
};
}

View File

@@ -6,9 +6,11 @@
<PageTitle>Services</PageTitle>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Services</h1>
<a href="/services/create" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Create Service
<h1 class="nimbus-page-title">
<i class="bi bi-gear-fill me-3"></i>Services
</h1>
<a href="/services/create" class="btn btn-nimbus-primary">
<i class="bi bi-gear-wide-connected me-2"></i>Create Service
</a>
</div>
@@ -69,7 +71,7 @@ else if (services.Any())
<strong>@service.ServiceDate.ToString("MMM dd, yyyy (dddd)")</strong>
</td>
<td>
<span class="badge @GetServiceTypeBadgeClass(service.ServiceTypeName)">
<span class="badge" style="background-color: var(--nimbus-gold); color: var(--nimbus-navy);">
@service.ServiceTypeName
</span>
</td>
@@ -115,11 +117,17 @@ else if (services.Any())
</td>
<td>
<div class="btn-group" role="group">
<a href="/services/@service.ServiceId" class="btn btn-sm btn-outline-primary">View</a>
<a href="/services/@service.ServiceId" class="btn btn-sm btn-nimbus-secondary me-1">
<i class="bi bi-eye-fill me-1"></i>View
</a>
@if (service.ServiceDate >= DateTime.Today)
{
<a href="/schedules/create?serviceId=@service.ServiceId" class="btn btn-sm btn-success">Schedule</a>
<a href="/services/@service.ServiceId/edit" class="btn btn-sm btn-outline-warning">Edit</a>
<a href="/schedules/create?serviceId=@service.ServiceId" class="btn btn-sm btn-success me-1">
<i class="bi bi-calendar-plus-fill me-1"></i>Schedule
</a>
<a href="/services/@service.ServiceId/edit" class="btn btn-sm btn-outline-warning">
<i class="bi bi-pencil-fill me-1"></i>Edit
</a>
}
</div>
</td>
@@ -225,14 +233,4 @@ else
}
}
private string GetServiceTypeBadgeClass(string? serviceTypeName)
{
return serviceTypeName switch
{
"9AM" => "bg-info",
"11AM" => "bg-primary",
"6PM" => "bg-dark",
_ => "bg-secondary"
};
}
}

View File

@@ -0,0 +1,7 @@
namespace NimbusFlow.Frontend.Models;
public class Classification
{
public int ClassificationId { get; set; }
public string ClassificationName { get; set; } = string.Empty;
}

View File

@@ -1,59 +1,21 @@
namespace NimbusFlow.Frontend.Models
namespace NimbusFlow.Frontend.Models;
public class Member
{
public class Member
{
public int MemberId { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public int? ClassificationId { get; set; }
public string? Notes { get; set; }
public int IsActive { get; set; } = 1;
public DateTime? LastScheduledAt { get; set; }
public DateTime? LastAcceptedAt { get; set; }
public DateTime? LastDeclinedAt { get; set; }
public int DeclineStreak { get; set; } = 0;
public int MemberId { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
public int? ClassificationId { get; set; }
public string? Notes { get; set; }
public int IsActive { get; set; } = 1;
public DateTime? LastScheduledAt { get; set; }
public DateTime? LastAcceptedAt { get; set; }
public DateTime? LastDeclinedAt { get; set; }
public int DeclineStreak { get; set; } = 0;
// Navigation properties
public string? ClassificationName { get; set; }
public string FullName => $"{FirstName} {LastName}";
}
public class Classification
{
public int ClassificationId { get; set; }
public string ClassificationName { get; set; } = string.Empty;
}
public class Service
{
public int ServiceId { get; set; }
public int ServiceTypeId { get; set; }
public DateTime ServiceDate { get; set; }
public string? ServiceTypeName { get; set; }
}
public class ServiceType
{
public int ServiceTypeId { get; set; }
public string TypeName { get; set; } = string.Empty;
}
public class Schedule
{
public int ScheduleId { get; set; }
public int ServiceId { get; set; }
public int MemberId { get; set; }
public string Status { get; set; } = string.Empty; // pending, accepted, declined
public DateTime ScheduledAt { get; set; }
public DateTime? AcceptedAt { get; set; }
public DateTime? DeclinedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public string? DeclineReason { get; set; }
// Navigation properties
public Member? Member { get; set; }
public Service? Service { get; set; }
}
// Navigation properties
public string? ClassificationName { get; set; }
public string FullName => $"{FirstName} {LastName}";
}

View File

@@ -0,0 +1,18 @@
namespace NimbusFlow.Frontend.Models;
public class Schedule
{
public int ScheduleId { get; set; }
public int ServiceId { get; set; }
public int MemberId { get; set; }
public string Status { get; set; } = string.Empty; // pending, accepted, declined
public DateTime ScheduledAt { get; set; }
public DateTime? AcceptedAt { get; set; }
public DateTime? DeclinedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public string? DeclineReason { get; set; }
// Navigation properties
public Member? Member { get; set; }
public Service? Service { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace NimbusFlow.Frontend.Models;
public class Service
{
public int ServiceId { get; set; }
public int ServiceTypeId { get; set; }
public DateTime ServiceDate { get; set; }
public string? ServiceTypeName { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace NimbusFlow.Frontend.Models;
public class ServiceType
{
public int ServiceTypeId { get; set; }
public string TypeName { get; set; } = string.Empty;
}

View File

@@ -1 +1 @@
{"ContentRoots":["/home/t2/Development/nimbusflow/frontend/wwwroot/","/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/scopedcss/bundle/"],"Root":{"Children":{"app.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"app.css"},"Patterns":null},"bootstrap":{"Children":{"bootstrap.min.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"bootstrap/bootstrap.min.css"},"Patterns":null},"bootstrap.min.css.map":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"bootstrap/bootstrap.min.css.map"},"Patterns":null}},"Asset":null,"Patterns":null},"favicon.png":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"favicon.png"},"Patterns":null},"NimbusFlow.Frontend.styles.css":{"Children":null,"Asset":{"ContentRootIndex":1,"SubPath":"NimbusFlow.Frontend.styles.css"},"Patterns":null}},"Asset":null,"Patterns":[{"ContentRootIndex":0,"Pattern":"**","Depth":0}]}}
{"ContentRoots":["/home/t2/Development/nimbusflow/frontend/wwwroot/","/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/scopedcss/bundle/"],"Root":{"Children":{"app.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"app.css"},"Patterns":null},"bootstrap":{"Children":{"bootstrap.min.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"bootstrap/bootstrap.min.css"},"Patterns":null},"bootstrap.min.css.map":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"bootstrap/bootstrap.min.css.map"},"Patterns":null}},"Asset":null,"Patterns":null},"css":{"Children":{"nimbusflow.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"css/nimbusflow.css"},"Patterns":null}},"Asset":null,"Patterns":null},"favicon.png":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"favicon.png"},"Patterns":null},"NimbusFlow.Frontend.styles.css":{"Children":null,"Asset":{"ContentRootIndex":1,"SubPath":"NimbusFlow.Frontend.styles.css"},"Patterns":null}},"Asset":null,"Patterns":[{"ContentRootIndex":0,"Pattern":"**","Depth":0}]}}

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+6063ed62e03c4a6c1d5aea04009fca83dcfa3ff6")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+133efdddeaca67de9c657b22a9a336c76ff65dfb")]
[assembly: System.Reflection.AssemblyProductAttribute("NimbusFlow.Frontend")]
[assembly: System.Reflection.AssemblyTitleAttribute("NimbusFlow.Frontend")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
193745f5da23d1ece0e2e39cb2746e078b8a35590f34b2bb7083bc28cced0dfb
f301bdfdc7b594589c917e1256f8f410c1893af1ef23d88133340e83a19fc068

View File

@@ -1 +1 @@
27628f432c885c6d6664e08021d1b1f4ca5058cc599948362511b2ad9f9fbc8f
72e9b9308a1f905093ef5ce6ebcb221be89fd0ba2dc586d8cf8c992ab0511185

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"Version": 1,
"Hash": "13XSTsKieyma8ttrJ8+acV7o8zAargmbFi1oQKIMZZk=",
"Hash": "HQj5X023GHr+MwF0GlqlwaxDgNjaO62JkHxsoleuqe0=",
"Source": "NimbusFlow.Frontend",
"BasePath": "_content/NimbusFlow.Frontend",
"Mode": "Default",
@@ -111,6 +111,25 @@
"CopyToPublishDirectory": "PreserveNewest",
"OriginalItemSpec": "wwwroot/bootstrap/bootstrap.min.css.map"
},
{
"Identity": "/home/t2/Development/nimbusflow/frontend/wwwroot/css/nimbusflow.css",
"SourceId": "NimbusFlow.Frontend",
"SourceType": "Discovered",
"ContentRoot": "/home/t2/Development/nimbusflow/frontend/wwwroot/",
"BasePath": "_content/NimbusFlow.Frontend",
"RelativePath": "css/nimbusflow.css",
"AssetKind": "All",
"AssetMode": "All",
"AssetRole": "Primary",
"AssetMergeBehavior": "PreferTarget",
"AssetMergeSource": "",
"RelatedAsset": "",
"AssetTraitName": "",
"AssetTraitValue": "",
"CopyToOutputDirectory": "Never",
"CopyToPublishDirectory": "PreserveNewest",
"OriginalItemSpec": "wwwroot/css/nimbusflow.css"
},
{
"Identity": "/home/t2/Development/nimbusflow/frontend/wwwroot/favicon.png",
"SourceId": "NimbusFlow.Frontend",

View File

@@ -1 +1 @@
{"ContentRoots":["/home/t2/Development/nimbusflow/frontend/wwwroot/","/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/scopedcss/bundle/"],"Root":{"Children":{"app.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"app.css"},"Patterns":null},"bootstrap":{"Children":{"bootstrap.min.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"bootstrap/bootstrap.min.css"},"Patterns":null},"bootstrap.min.css.map":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"bootstrap/bootstrap.min.css.map"},"Patterns":null}},"Asset":null,"Patterns":null},"favicon.png":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"favicon.png"},"Patterns":null},"NimbusFlow.Frontend.styles.css":{"Children":null,"Asset":{"ContentRootIndex":1,"SubPath":"NimbusFlow.Frontend.styles.css"},"Patterns":null}},"Asset":null,"Patterns":[{"ContentRootIndex":0,"Pattern":"**","Depth":0}]}}
{"ContentRoots":["/home/t2/Development/nimbusflow/frontend/wwwroot/","/home/t2/Development/nimbusflow/frontend/obj/Debug/net8.0/scopedcss/bundle/"],"Root":{"Children":{"app.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"app.css"},"Patterns":null},"bootstrap":{"Children":{"bootstrap.min.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"bootstrap/bootstrap.min.css"},"Patterns":null},"bootstrap.min.css.map":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"bootstrap/bootstrap.min.css.map"},"Patterns":null}},"Asset":null,"Patterns":null},"css":{"Children":{"nimbusflow.css":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"css/nimbusflow.css"},"Patterns":null}},"Asset":null,"Patterns":null},"favicon.png":{"Children":null,"Asset":{"ContentRootIndex":0,"SubPath":"favicon.png"},"Patterns":null},"NimbusFlow.Frontend.styles.css":{"Children":null,"Asset":{"ContentRootIndex":1,"SubPath":"NimbusFlow.Frontend.styles.css"},"Patterns":null}},"Asset":null,"Patterns":[{"ContentRootIndex":0,"Pattern":"**","Depth":0}]}}

View File

@@ -16,6 +16,10 @@
"Id": "/home/t2/Development/nimbusflow/frontend/wwwroot/bootstrap/bootstrap.min.css.map",
"PackagePath": "staticwebassets/bootstrap/bootstrap.min.css.map"
},
{
"Id": "/home/t2/Development/nimbusflow/frontend/wwwroot/css/nimbusflow.css",
"PackagePath": "staticwebassets/css/nimbusflow.css"
},
{
"Id": "/home/t2/Development/nimbusflow/frontend/wwwroot/favicon.png",
"PackagePath": "staticwebassets/favicon.png"

View File

@@ -48,6 +48,22 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<OriginalItemSpec>$([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)..\staticwebassets\bootstrap\bootstrap.min.css.map))</OriginalItemSpec>
</StaticWebAsset>
<StaticWebAsset Include="$([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)..\staticwebassets\css\nimbusflow.css))">
<SourceType>Package</SourceType>
<SourceId>NimbusFlow.Frontend</SourceId>
<ContentRoot>$(MSBuildThisFileDirectory)..\staticwebassets\</ContentRoot>
<BasePath>_content/NimbusFlow.Frontend</BasePath>
<RelativePath>css/nimbusflow.css</RelativePath>
<AssetKind>All</AssetKind>
<AssetMode>All</AssetMode>
<AssetRole>Primary</AssetRole>
<RelatedAsset></RelatedAsset>
<AssetTraitName></AssetTraitName>
<AssetTraitValue></AssetTraitValue>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<OriginalItemSpec>$([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)..\staticwebassets\css\nimbusflow.css))</OriginalItemSpec>
</StaticWebAsset>
<StaticWebAsset Include="$([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)..\staticwebassets\favicon.png))">
<SourceType>Package</SourceType>
<SourceId>NimbusFlow.Frontend</SourceId>

View File

@@ -0,0 +1,641 @@
/* NimbusFlow Custom Styles */
: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 overrides */
--bs-primary: var(--nimbus-navy);
--bs-primary-rgb: 26, 35, 50;
--bs-secondary: var(--nimbus-gold);
--bs-secondary-rgb: 255, 215, 0;
--bs-warning: var(--nimbus-amber);
--bs-warning-rgb: 255, 179, 71;
}
/* Body and base styles - Dark Mode */
body {
background-color: var(--nimbus-navy-dark);
color: var(--nimbus-white);
}
.content {
background-color: var(--nimbus-navy-dark);
min-height: 100vh;
}
/* Navigation styles */
.nimbus-navbar {
background-color: var(--nimbus-navy) !important;
border-bottom: 3px solid var(--nimbus-gold);
box-shadow: 0 2px 10px rgba(26, 35, 50, 0.2);
}
.nimbus-navbar .navbar-brand {
color: var(--nimbus-white) !important;
font-weight: 600;
font-size: 1.5rem;
}
.nimbus-navbar .navbar-brand:hover {
color: var(--nimbus-gold-light) !important;
}
.nimbus-navbar .nav-link {
color: var(--nimbus-white) !important;
font-weight: 500;
transition: color 0.3s ease;
}
.nimbus-navbar .nav-link:hover,
.nimbus-navbar .nav-link.active {
color: var(--nimbus-gold) !important;
background-color: rgba(255, 215, 0, 0.1);
border-radius: 0.375rem;
}
/* Brand icon */
.nimbus-brand-icon {
color: var(--nimbus-gold);
margin-right: 0.5rem;
font-size: 1.8rem;
}
/* Card styles - Dark Mode */
.nimbus-card {
border: none;
border-radius: 0.75rem;
background-color: var(--nimbus-navy);
color: var(--nimbus-white);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.nimbus-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.4);
}
.nimbus-card-header {
background-color: var(--nimbus-navy-light);
color: var(--nimbus-white);
border-radius: 0.75rem 0.75rem 0 0 !important;
padding: 1rem 1.25rem;
font-weight: 600;
}
/* Button styles */
.btn-nimbus-primary {
background-color: var(--nimbus-gold);
border-color: var(--nimbus-gold-dark);
color: var(--nimbus-navy);
font-weight: 600;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
.btn-nimbus-primary:hover {
background-color: var(--nimbus-gold-dark);
border-color: var(--nimbus-gold-dark);
color: var(--nimbus-navy);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(255, 215, 0, 0.3);
}
.btn-nimbus-secondary {
background-color: var(--nimbus-navy);
border-color: var(--nimbus-navy);
color: var(--nimbus-white);
font-weight: 600;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
.btn-nimbus-secondary:hover {
background-color: var(--nimbus-navy-light);
border-color: var(--nimbus-navy-light);
color: var(--nimbus-white);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(26, 35, 50, 0.3);
}
/* Badge styles */
.badge-nimbus-active {
background-color: var(--nimbus-gold);
color: var(--nimbus-navy);
font-weight: 600;
}
.badge-nimbus-inactive {
background-color: var(--nimbus-gray-600);
color: var(--nimbus-white);
}
.badge-nimbus-pending {
background-color: var(--nimbus-amber);
color: var(--nimbus-navy);
font-weight: 600;
}
.badge-nimbus-accepted {
background-color: #28a745;
color: var(--nimbus-white);
font-weight: 600;
}
.badge-nimbus-declined {
background-color: #dc3545;
color: var(--nimbus-white);
font-weight: 600;
}
.badge-nimbus-classification {
background-color: var(--nimbus-gray-600) !important;
color: var(--nimbus-white) !important;
font-weight: 600;
}
/* Dashboard cards - Refined Dark Mode */
.nimbus-dashboard-card {
border: none;
border-radius: 0.75rem;
overflow: hidden;
position: relative;
transition: all 0.2s ease;
background: linear-gradient(135deg, var(--nimbus-navy-light) 0%, var(--nimbus-navy) 100%);
color: var(--nimbus-white);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 215, 0, 0.1);
border-left: 3px solid transparent;
}
.nimbus-dashboard-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 215, 0, 0.2);
}
.nimbus-dashboard-card .card-header {
background: rgba(255, 215, 0, 0.05);
border-bottom: 1px solid rgba(255, 215, 0, 0.15);
padding: 1rem 1.25rem 0.75rem;
color: var(--nimbus-gray-300);
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.nimbus-dashboard-card .card-body {
padding: 1rem 1.25rem 1.25rem;
}
.nimbus-dashboard-card .card-title {
font-size: 2.25rem;
font-weight: 800;
line-height: 1;
margin-bottom: 0.25rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.nimbus-dashboard-card .card-body small {
color: var(--nimbus-gray-300);
font-size: 0.8rem;
font-weight: 500;
opacity: 0.8;
}
.nimbus-dashboard-card.card-members {
border-left-color: #4dabf7;
}
.nimbus-dashboard-card.card-members .card-title {
color: #4dabf7;
}
.nimbus-dashboard-card.card-members .card-header i {
color: #4dabf7;
}
.nimbus-dashboard-card.card-schedules {
border-left-color: var(--nimbus-gold);
}
.nimbus-dashboard-card.card-schedules .card-title {
color: var(--nimbus-gold);
}
.nimbus-dashboard-card.card-schedules .card-header i {
color: var(--nimbus-gold);
}
.nimbus-dashboard-card.card-services {
border-left-color: var(--nimbus-amber);
}
.nimbus-dashboard-card.card-services .card-title {
color: var(--nimbus-amber);
}
.nimbus-dashboard-card.card-services .card-header i {
color: var(--nimbus-amber);
}
.nimbus-dashboard-card.card-classifications {
border-left-color: #8b5cf6;
}
.nimbus-dashboard-card.card-classifications .card-title {
color: #8b5cf6;
}
.nimbus-dashboard-card.card-classifications .card-header i {
color: #8b5cf6;
}
/* Table styles - Dark Mode */
.nimbus-table {
border-radius: 0 0 0.75rem 0.75rem;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
background-color: var(--nimbus-navy);
}
.nimbus-table thead th {
background-color: var(--nimbus-navy-light);
color: var(--nimbus-white);
border: none;
font-weight: 600;
padding: 1rem 0.75rem;
}
.nimbus-table tbody tr {
background-color: var(--nimbus-navy);
color: var(--nimbus-white);
border-color: rgba(255, 215, 0, 0.1);
}
.nimbus-table tbody tr:hover {
background-color: rgba(255, 215, 0, 0.15);
}
.nimbus-table tbody td {
border-color: rgba(255, 215, 0, 0.1);
}
/* Page titles - Dark Mode */
.nimbus-page-title {
color: var(--nimbus-white);
font-weight: 700;
margin-bottom: 1.5rem;
position: relative;
}
.nimbus-page-title::after {
content: '';
position: absolute;
bottom: -0.5rem;
left: 0;
width: 3rem;
height: 0.25rem;
background-color: var(--nimbus-gold);
border-radius: 0.125rem;
}
/* Loading spinner */
.nimbus-spinner {
color: var(--nimbus-gold);
}
/* Form controls */
.form-control:focus {
border-color: var(--nimbus-gold);
box-shadow: 0 0 0 0.2rem rgba(255, 215, 0, 0.25);
}
.form-select:focus {
border-color: var(--nimbus-gold);
box-shadow: 0 0 0 0.2rem rgba(255, 215, 0, 0.25);
}
/* Sidebar navigation */
.nimbus-sidebar, .sidebar {
background: var(--nimbus-navy) !important;
background-image: none !important;
min-height: 100vh;
border-right: 3px solid var(--nimbus-gold);
position: relative;
}
.nimbus-sidebar .nav-item .nav-link,
.sidebar .nav-item .nav-link {
color: var(--nimbus-white) !important;
padding: 0.75rem 1.25rem;
margin: 0.25rem 0.5rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
display: flex !important;
align-items: center !important;
text-decoration: none;
}
.nimbus-sidebar .nav-item .nav-link:hover,
.nimbus-sidebar .nav-item .nav-link.active,
.sidebar .nav-item .nav-link:hover,
.sidebar .nav-item .nav-link.active {
background-color: var(--nimbus-gold) !important;
color: var(--nimbus-navy) !important;
font-weight: 600;
}
/* Fix Bootstrap Icons alignment in sidebar */
.sidebar .nav-item .nav-link i,
.nimbus-sidebar .nav-item .nav-link i {
margin-right: 0.5rem !important;
width: 1.25rem !important;
height: 1.25rem !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
background: none !important;
background-image: none !important;
}
/* Alert styles */
.alert-nimbus-info {
background-color: rgba(255, 215, 0, 0.1);
border-color: var(--nimbus-gold);
color: var(--nimbus-navy);
}
.alert-nimbus-warning {
background-color: rgba(255, 179, 71, 0.1);
border-color: var(--nimbus-amber);
color: var(--nimbus-navy);
}
/* Quick Actions - Refined Dark Mode */
.nimbus-quick-actions {
background: linear-gradient(135deg, var(--nimbus-navy-light) 0%, var(--nimbus-navy) 100%);
color: var(--nimbus-white);
border-radius: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 215, 0, 0.1);
padding: 1.25rem;
}
.nimbus-quick-actions h5 {
color: var(--nimbus-white);
font-weight: 700;
margin-bottom: 1rem;
font-size: 1rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.nimbus-action-btn {
display: block;
width: 100%;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
border: none;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.875rem;
text-decoration: none;
transition: all 0.2s ease;
text-align: left;
position: relative;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.nimbus-action-btn:last-child {
margin-bottom: 0;
}
.nimbus-action-btn:hover {
transform: translateY(-1px);
text-decoration: none;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.nimbus-action-btn-primary {
background: linear-gradient(135deg, var(--nimbus-gold) 0%, var(--nimbus-gold-dark) 100%);
color: var(--nimbus-navy);
border: none;
}
.nimbus-action-btn-primary:hover {
background: linear-gradient(135deg, var(--nimbus-gold-light) 0%, var(--nimbus-gold) 100%);
color: var(--nimbus-navy);
}
.nimbus-action-btn-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: var(--nimbus-white);
}
.nimbus-action-btn-success:hover {
background: linear-gradient(135deg, #34d399 0%, #10b981 100%);
color: var(--nimbus-white);
}
.nimbus-action-btn-warning {
background: linear-gradient(135deg, var(--nimbus-amber) 0%, #f59e0b 100%);
color: var(--nimbus-navy);
}
.nimbus-action-btn-warning:hover {
background: linear-gradient(135deg, #fbbf24 0%, var(--nimbus-amber) 100%);
color: var(--nimbus-navy);
}
.nimbus-action-btn-info {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
color: var(--nimbus-white);
}
.nimbus-action-btn-info:hover {
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
color: var(--nimbus-white);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.nimbus-navbar .navbar-brand {
font-size: 1.25rem;
}
.nimbus-brand-icon {
font-size: 1.5rem;
}
.nimbus-dashboard-card .card-title {
font-size: 2rem;
}
}
/* Recent schedules list styling - Refined Dark Mode */
.nimbus-quick-actions .list-group {
border: none;
}
.nimbus-quick-actions .list-group-item {
border: none;
border-bottom: 1px solid rgba(255, 215, 0, 0.1);
padding: 0.75rem 0;
background: transparent;
color: var(--nimbus-white);
transition: all 0.2s ease;
}
.nimbus-quick-actions .list-group-item:last-child {
border-bottom: none;
}
.nimbus-quick-actions .list-group-item:hover {
background: rgba(255, 215, 0, 0.05);
border-radius: 0.375rem;
margin: 0 -0.5rem;
padding: 0.75rem 0.5rem;
}
.nimbus-quick-actions .list-group-item h6 {
color: var(--nimbus-white);
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.125rem;
}
.nimbus-quick-actions .list-group-item p {
color: var(--nimbus-gray-300);
font-size: 0.8rem;
margin-bottom: 0.125rem;
opacity: 0.9;
}
.nimbus-quick-actions .list-group-item small {
color: var(--nimbus-gray-300);
font-size: 0.7rem;
opacity: 0.7;
}
/* Dark Mode Bootstrap Overrides */
.card {
background-color: var(--nimbus-navy) !important;
color: var(--nimbus-white) !important;
border: 1px solid rgba(255, 215, 0, 0.2) !important;
}
.card-header {
background-color: var(--nimbus-navy-light) !important;
color: var(--nimbus-white) !important;
border-bottom: 1px solid rgba(255, 215, 0, 0.2) !important;
}
.table {
color: var(--nimbus-white) !important;
background-color: var(--nimbus-navy) !important;
}
.table th {
color: var(--nimbus-white) !important;
border-color: rgba(255, 215, 0, 0.2) !important;
}
.table td {
color: var(--nimbus-white) !important;
border-color: rgba(255, 215, 0, 0.1) !important;
}
.table-striped > tbody > tr:nth-of-type(odd) > td {
background-color: rgba(255, 215, 0, 0.05) !important;
}
.table-hover > tbody > tr:hover > td {
background-color: rgba(255, 215, 0, 0.15) !important;
}
.form-control {
background-color: var(--nimbus-navy-light) !important;
color: var(--nimbus-white) !important;
border: 1px solid rgba(255, 215, 0, 0.3) !important;
}
.form-control:focus {
background-color: var(--nimbus-navy-light) !important;
color: var(--nimbus-white) !important;
border-color: var(--nimbus-gold) !important;
box-shadow: 0 0 0 0.2rem rgba(255, 215, 0, 0.25) !important;
}
.form-select {
background-color: var(--nimbus-navy-light) !important;
color: var(--nimbus-white) !important;
border: 1px solid rgba(255, 215, 0, 0.3) !important;
}
.form-select:focus {
background-color: var(--nimbus-navy-light) !important;
color: var(--nimbus-white) !important;
border-color: var(--nimbus-gold) !important;
box-shadow: 0 0 0 0.2rem rgba(255, 215, 0, 0.25) !important;
}
.form-check-input {
background-color: var(--nimbus-navy-light) !important;
border: 1px solid rgba(255, 215, 0, 0.3) !important;
}
.form-check-input:checked {
background-color: var(--nimbus-gold) !important;
border-color: var(--nimbus-gold) !important;
}
.form-label {
color: var(--nimbus-white) !important;
}
.alert-info {
background-color: rgba(255, 215, 0, 0.1) !important;
border-color: var(--nimbus-gold) !important;
color: var(--nimbus-white) !important;
}
.text-muted {
color: var(--nimbus-gray-300) !important;
}
h1, h2, h3, h4, h5, h6 {
color: var(--nimbus-white) !important;
}
.modal-content {
background-color: var(--nimbus-navy) !important;
color: var(--nimbus-white) !important;
border: 1px solid rgba(255, 215, 0, 0.3) !important;
}
.modal-header {
border-bottom: 1px solid rgba(255, 215, 0, 0.2) !important;
}
.modal-footer {
border-top: 1px solid rgba(255, 215, 0, 0.2) !important;
}
.btn-close {
filter: invert(1) grayscale(100%) brightness(200%) !important;
}