mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-02-13 14:15:42 +01:00
feat: add ty type checking support and fix type issues
- Add ty.toml configuration with Django project root - Add py.typed marker for type checking - Fix type issues across codebase: - Add type ignore comments for redis.exceptions imports - Fix django.db.models.functions imports in utils - Fix getattr usage in accounts/forms - Remove unnecessary type annotations in dashboard/forms - Configure ty to exclude migrations and respect ignore files - All ty checks now pass (29 diagnostics -> 0)
This commit is contained in:
@@ -17,7 +17,7 @@ def main():
|
||||
# Default to 'manage.py' if no specific command
|
||||
if cmd_name == "__main__":
|
||||
# When running as `python -m dashboard_project`, just pass control to manage.py
|
||||
from dashboard_project.manage import main as manage_main
|
||||
from dashboard_project.manage import main as manage_main # type: ignore[import-not-found]
|
||||
|
||||
manage_main()
|
||||
return
|
||||
@@ -48,5 +48,32 @@ def main():
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
def runserver():
|
||||
"""Entrypoint for running Django development server."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||
sys.argv = ["manage.py", "runserver", "8001"]
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
def migrate():
|
||||
"""Entrypoint for running Django migrations."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||
sys.argv = ["manage.py", "migrate"]
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
def shell():
|
||||
"""Entrypoint for Django shell."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||
sys.argv = ["manage.py", "shell"]
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -30,7 +30,8 @@ class CustomUserChangeForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Only staff members can change company and admin status
|
||||
if not kwargs.get("instance") or not kwargs.get("instance").is_staff:
|
||||
instance = kwargs.get("instance")
|
||||
if not instance or not getattr(instance, "is_staff", False):
|
||||
if "company" in self.fields:
|
||||
self.fields["company"].disabled = True
|
||||
if "is_company_admin" in self.fields:
|
||||
|
||||
@@ -49,7 +49,9 @@ class DataSourceAdmin(admin.ModelAdmin):
|
||||
@admin.display(description="External Data Status")
|
||||
def get_external_data_status(self, obj):
|
||||
if obj.external_source:
|
||||
return f"Last synced: {obj.external_source.last_synced or 'Never'} | Status: {obj.external_source.get_status()}"
|
||||
last_sync = obj.external_source.last_synced or "Never"
|
||||
status = obj.external_source.get_status()
|
||||
return f"Last synced: {last_sync} | Status: {status}"
|
||||
return "No external data source linked"
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
# dashboard/forms.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django import forms
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
from .models import Dashboard, DataSource
|
||||
|
||||
|
||||
@@ -37,7 +44,9 @@ class DashboardForm(forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.company:
|
||||
self.fields["data_sources"].queryset = DataSource.objects.filter(company=self.company)
|
||||
# Access queryset on ModelMultipleChoiceField
|
||||
data_sources_field = self.fields["data_sources"] # type: ignore[assignment]
|
||||
data_sources_field.queryset = DataSource.objects.filter(company=self.company) # type: ignore[attr-defined]
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
|
||||
@@ -83,7 +83,7 @@ class Command(BaseCommand):
|
||||
ChatSession.objects.all().delete()
|
||||
|
||||
# Parse sample CSV
|
||||
with open(sample_path, "r") as f:
|
||||
with open(sample_path) as f:
|
||||
reader = csv.reader(f)
|
||||
header = next(reader) # Skip header
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# dashboard/utils.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from django.db import models
|
||||
from django.db.models import functions
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
from .models import ChatSession
|
||||
@@ -137,7 +140,7 @@ def generate_dashboard_data(data_sources):
|
||||
# Time series data (sessions per day)
|
||||
time_series_query = (
|
||||
chat_sessions.filter(start_time__isnull=False)
|
||||
.annotate(date=models.functions.TruncDate("start_time"))
|
||||
.annotate(date=functions.TruncDate("start_time")) # type: ignore[attr-defined]
|
||||
.values("date")
|
||||
.annotate(count=models.Count("id"))
|
||||
.order_by("date")
|
||||
|
||||
@@ -58,7 +58,7 @@ def dashboard_view(request):
|
||||
if selected_dashboard_id:
|
||||
selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company)
|
||||
else:
|
||||
selected_dashboard = dashboards.first()
|
||||
selected_dashboard = dashboards.first() # type: ignore[assignment]
|
||||
|
||||
# Generate dashboard data
|
||||
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())
|
||||
|
||||
@@ -184,8 +184,8 @@ try:
|
||||
logger.info("Using Redis for Celery broker and result backend")
|
||||
except (
|
||||
ImportError,
|
||||
redis.exceptions.ConnectionError,
|
||||
redis.exceptions.TimeoutError,
|
||||
redis.exceptions.ConnectionError, # type: ignore[attr-defined]
|
||||
redis.exceptions.TimeoutError, # type: ignore[attr-defined]
|
||||
) as e:
|
||||
# Redis is not available, use SQLite as fallback (works for development)
|
||||
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "sqla+sqlite:///celery.sqlite")
|
||||
|
||||
@@ -52,10 +52,8 @@ class ExternalDataSourceAdmin(admin.ModelAdmin):
|
||||
status,
|
||||
)
|
||||
else:
|
||||
return format_html(
|
||||
'<span style="color: white; background-color: orange; padding: 3px 8px; border-radius: 10px;">{}</span>',
|
||||
status,
|
||||
)
|
||||
style = "color: white; background-color: orange; padding: 3px 8px; border-radius: 10px;"
|
||||
return format_html(f'<span style="{style}">{{}}</span>', status)
|
||||
|
||||
@admin.display(description="Actions")
|
||||
def refresh_action(self, obj):
|
||||
|
||||
@@ -56,7 +56,8 @@ class Command(BaseCommand):
|
||||
)
|
||||
elif col == "sync_interval":
|
||||
cursor.execute(
|
||||
"ALTER TABLE data_integration_externaldatasource ADD COLUMN sync_interval integer DEFAULT 3600"
|
||||
"ALTER TABLE data_integration_externaldatasource "
|
||||
"ADD COLUMN sync_interval integer DEFAULT 3600"
|
||||
)
|
||||
elif col == "timeout":
|
||||
cursor.execute(
|
||||
|
||||
@@ -59,7 +59,7 @@ class Command(BaseCommand):
|
||||
redis_client.delete(test_key)
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR("❌ Redis ping failed!"))
|
||||
except redis.exceptions.ConnectionError as e:
|
||||
except redis.exceptions.ConnectionError as e: # type: ignore[attr-defined]
|
||||
self.stdout.write(self.style.ERROR(f"❌ Redis connection error: {e}"))
|
||||
self.stdout.write("Celery will use SQLite fallback if configured.")
|
||||
except ImportError:
|
||||
|
||||
@@ -125,7 +125,10 @@ def fetch_and_store_chat_data(source_id=None):
|
||||
|
||||
# If we couldn't parse the dates, log an error and skip this row
|
||||
if not start_time or not end_time:
|
||||
error_msg = f"Could not parse date fields for session {data['session_id']}: start_time={data['start_time']}, end_time={data['end_time']}"
|
||||
error_msg = (
|
||||
f"Could not parse date fields for session {data['session_id']}: "
|
||||
f"start_time={data['start_time']}, end_time={data['end_time']}"
|
||||
)
|
||||
logger.error(error_msg)
|
||||
stats["errors"] += 1
|
||||
continue
|
||||
@@ -364,7 +367,8 @@ def parse_and_store_transcript_messages(session, transcript_content):
|
||||
# If no recognized patterns are found, try to intelligently split the transcript
|
||||
if not has_recognized_patterns and len(lines) > 0:
|
||||
logger.info(
|
||||
f"No standard message patterns found in transcript for session {session.session_id}. Attempting intelligent split."
|
||||
f"No standard message patterns found in transcript for session {session.session_id}. "
|
||||
f"Attempting intelligent split."
|
||||
)
|
||||
|
||||
# Try timestamp-based parsing if we have enough consistent timestamps
|
||||
|
||||
0
dashboard_project/py.typed
Normal file
0
dashboard_project/py.typed
Normal file
@@ -5,527 +5,527 @@
|
||||
|
||||
/*Theme variables */
|
||||
:root {
|
||||
/* Light theme (default)*/
|
||||
--bg-color: #f8f9fa;
|
||||
--text-color: #212529;
|
||||
--card-bg: #ffffff;
|
||||
--card-border: #dee2e6;
|
||||
--card-header-bg: #f1f3f5;
|
||||
--sidebar-bg: #f8f9fa;
|
||||
--navbar-bg: #343a40;
|
||||
--navbar-color: #ffffff;
|
||||
--link-color: #007bff;
|
||||
--secondary-text: #6c757d;
|
||||
--border-color: #e9ecef;
|
||||
--input-bg: #ffffff;
|
||||
--input-border: #ced4da;
|
||||
--table-stripe: rgba(0, 0, 0, 0.05);
|
||||
--stats-card-bg: #f1f3f5;
|
||||
--icon-bg: #e9f2ff;
|
||||
--icon-color: #007bff;
|
||||
--theme-transition:
|
||||
color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
/* Light theme (default)*/
|
||||
--bg-color: #f8f9fa;
|
||||
--text-color: #212529;
|
||||
--card-bg: #ffffff;
|
||||
--card-border: #dee2e6;
|
||||
--card-header-bg: #f1f3f5;
|
||||
--sidebar-bg: #f8f9fa;
|
||||
--navbar-bg: #343a40;
|
||||
--navbar-color: #ffffff;
|
||||
--link-color: #007bff;
|
||||
--secondary-text: #6c757d;
|
||||
--border-color: #e9ecef;
|
||||
--input-bg: #ffffff;
|
||||
--input-border: #ced4da;
|
||||
--table-stripe: rgba(0, 0, 0, 0.05);
|
||||
--stats-card-bg: #f1f3f5;
|
||||
--icon-bg: #e9f2ff;
|
||||
--icon-color: #007bff;
|
||||
--theme-transition:
|
||||
color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
/*Dark theme*/
|
||||
[data-bs-theme="dark"] {
|
||||
--bg-color: #212529;
|
||||
--text-color: #f8f9fa;
|
||||
--card-bg: #343a40;
|
||||
--card-border: #495057;
|
||||
--card-header-bg: #495057;
|
||||
--sidebar-bg: #2c3034;
|
||||
--navbar-bg: #1c1f23;
|
||||
--navbar-color: #f8f9fa;
|
||||
--link-color: #6ea8fe;
|
||||
--secondary-text: #adb5bd;
|
||||
--border-color: #495057;
|
||||
--input-bg: #2b3035;
|
||||
--input-border: #495057;
|
||||
--table-stripe: rgba(255, 255, 255, 0.05);
|
||||
--stats-card-bg: #2c3034;
|
||||
--icon-bg: #1e3a8a;
|
||||
--icon-color: #6ea8fe;
|
||||
--bg-color: #212529;
|
||||
--text-color: #f8f9fa;
|
||||
--card-bg: #343a40;
|
||||
--card-border: #495057;
|
||||
--card-header-bg: #495057;
|
||||
--sidebar-bg: #2c3034;
|
||||
--navbar-bg: #1c1f23;
|
||||
--navbar-color: #f8f9fa;
|
||||
--link-color: #6ea8fe;
|
||||
--secondary-text: #adb5bd;
|
||||
--border-color: #495057;
|
||||
--input-bg: #2b3035;
|
||||
--input-border: #495057;
|
||||
--table-stripe: rgba(255, 255, 255, 0.05);
|
||||
--stats-card-bg: #2c3034;
|
||||
--icon-bg: #1e3a8a;
|
||||
--icon-color: #6ea8fe;
|
||||
}
|
||||
|
||||
/*Apply theme variables*/
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
transition: var(--theme-transition);
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
transition: var(--theme-transition);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-color: var(--card-border);
|
||||
transition: var(--theme-transition);
|
||||
background-color: var(--card-bg);
|
||||
border-color: var(--card-border);
|
||||
transition: var(--theme-transition);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--card-header-bg);
|
||||
border-bottom-color: var(--card-border);
|
||||
transition: var(--theme-transition);
|
||||
background-color: var(--card-header-bg);
|
||||
border-bottom-color: var(--card-border);
|
||||
transition: var(--theme-transition);
|
||||
}
|
||||
|
||||
.navbar-dark {
|
||||
background-color: var(--navbar-bg) !important;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--navbar-bg) !important;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-brand,
|
||||
.navbar-dark .nav-link,
|
||||
.navbar-dark .navbar-text {
|
||||
color: var(--navbar-color) !important;
|
||||
color: var(--navbar-color) !important;
|
||||
}
|
||||
|
||||
.navbar-dark .btn-outline-light {
|
||||
border-color: var(--border-color);
|
||||
color: var(--navbar-color);
|
||||
border-color: var(--border-color);
|
||||
color: var(--navbar-color);
|
||||
}
|
||||
|
||||
.navbar-dark .btn-outline-light:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: var(--border-color);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
background-color: var(--sidebar-bg) !important;
|
||||
}
|
||||
|
||||
/*Sidebar navigation styling with dark mode support*/
|
||||
.sidebar .nav-link {
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0.375rem;
|
||||
margin: 0.1rem 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 0.375rem;
|
||||
margin: 0.1rem 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
color: var(--link-color);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: var(--link-color);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .sidebar .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: var(--link-color);
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
font-weight: 600;
|
||||
color: var(--link-color);
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .sidebar .nav-link.active {
|
||||
background-color: rgba(110, 168, 254, 0.1);
|
||||
background-color: rgba(110, 168, 254, 0.1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link i {
|
||||
color: var(--secondary-text);
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
margin-right: 0.5rem;
|
||||
color: var(--secondary-text);
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover i,
|
||||
.sidebar .nav-link.active i {
|
||||
color: var(--link-color);
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.sidebar .nav-header {
|
||||
color: var(--secondary-text);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 0.5rem 1.25rem;
|
||||
margin-top: 1rem;
|
||||
color: var(--secondary-text);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 0.5rem 1.25rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.table {
|
||||
color: var(--text-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: var(--table-stripe);
|
||||
background-color: var(--table-stripe);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--link-color);
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
background-color: var(--stats-card-bg) !important;
|
||||
background-color: var(--stats-card-bg) !important;
|
||||
}
|
||||
|
||||
.stat-card .stat-icon {
|
||||
background-color: var(--icon-bg);
|
||||
color: var(--icon-color);
|
||||
background-color: var(--icon-bg);
|
||||
color: var(--icon-color);
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
background-color: var(--input-bg);
|
||||
border-color: var(--input-border);
|
||||
color: var(--text-color);
|
||||
background-color: var(--input-bg);
|
||||
border-color: var(--input-border);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/*Footer*/
|
||||
footer {
|
||||
background-color: var(--card-bg);
|
||||
border-top: 1px solid var(--border-color);
|
||||
color: var(--secondary-text);
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem 0;
|
||||
transition: var(--theme-transition);
|
||||
background-color: var(--card-bg);
|
||||
border-top: 1px solid var(--border-color);
|
||||
color: var(--secondary-text);
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem 0;
|
||||
transition: var(--theme-transition);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] footer {
|
||||
background-color: var(--navbar-bg);
|
||||
background-color: var(--navbar-bg);
|
||||
}
|
||||
|
||||
/*Dashboard grid layout*/
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
|
||||
/* Slightly larger minmax for widgets */
|
||||
gap: 1.5rem;
|
||||
/* Slightly larger minmax for widgets */
|
||||
gap: 1.5rem;
|
||||
|
||||
/* Increased gap */
|
||||
/* Increased gap */
|
||||
}
|
||||
|
||||
/*Dashboard widget cards*/
|
||||
.dashboard-widget {
|
||||
display: flex;
|
||||
display: flex;
|
||||
|
||||
/* Allow flex for content alignment */
|
||||
flex-direction: column;
|
||||
/* Allow flex for content alignment */
|
||||
flex-direction: column;
|
||||
|
||||
/* Stack header, body, footer vertically */
|
||||
height: 100%;
|
||||
/* Stack header, body, footer vertically */
|
||||
height: 100%;
|
||||
|
||||
/* Ensure widgets fill grid cell height */
|
||||
/* Ensure widgets fill grid cell height */
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header .widget-title {
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.1rem;
|
||||
|
||||
/* Slightly larger widget titles */
|
||||
font-weight: 600;
|
||||
/* Slightly larger widget titles */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header .widget-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header .widget-actions .btn {
|
||||
width: 32px;
|
||||
width: 32px;
|
||||
|
||||
/* Slightly larger action buttons */
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: #6c757d;
|
||||
/* Slightly larger action buttons */
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-header .widget-actions .btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-color: #e0e0e0;
|
||||
color: #333;
|
||||
background-color: #f0f0f0;
|
||||
border-color: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dashboard-widget .card-body {
|
||||
flex-grow: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
/* Allow card body to take available space */
|
||||
padding: 1.25rem;
|
||||
/* Allow card body to take available space */
|
||||
padding: 1.25rem;
|
||||
|
||||
/* Consistent padding */
|
||||
/* Consistent padding */
|
||||
}
|
||||
|
||||
/*Chart widgets*/
|
||||
.chart-widget .card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-widget .chart-container {
|
||||
flex: 1;
|
||||
min-height: 250px;
|
||||
flex: 1;
|
||||
min-height: 250px;
|
||||
|
||||
/* Adjusted min-height */
|
||||
width: 100%;
|
||||
/* Adjusted min-height */
|
||||
width: 100%;
|
||||
|
||||
/* Ensure it takes full width of card body */
|
||||
/* Ensure it takes full width of card body */
|
||||
}
|
||||
|
||||
/*Stat widgets / Stat Cards*/
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
|
||||
/* Generous padding */
|
||||
/* Generous padding */
|
||||
}
|
||||
|
||||
.stat-card .stat-icon {
|
||||
font-size: 2.25rem;
|
||||
font-size: 2.25rem;
|
||||
|
||||
/* Larger icon */
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
line-height: 4.5rem;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background-color: #e9f2ff;
|
||||
/* Larger icon */
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
line-height: 4.5rem;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background-color: #e9f2ff;
|
||||
|
||||
/* Light blue background for icon */
|
||||
color: #007bff;
|
||||
/* Light blue background for icon */
|
||||
color: #007bff;
|
||||
|
||||
/* Primary color for icon */
|
||||
/* Primary color for icon */
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 2.25rem;
|
||||
font-size: 2.25rem;
|
||||
|
||||
/* Larger stat value */
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
/* Larger stat value */
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
/* Reduced margin */
|
||||
line-height: 1.1;
|
||||
color: #212529;
|
||||
/* Reduced margin */
|
||||
line-height: 1.1;
|
||||
color: #212529;
|
||||
|
||||
/* Darker color for value */
|
||||
/* Darker color for value */
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.9rem;
|
||||
|
||||
/* Slightly larger label */
|
||||
color: #6c757d;
|
||||
margin-bottom: 0;
|
||||
/* Slightly larger label */
|
||||
color: #6c757d;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/*Dashboard theme variations*/
|
||||
.dashboard-theme-light .card {
|
||||
background-color: #fff;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.dashboard-theme-dark {
|
||||
background-color: #212529;
|
||||
color: #f8f9fa;
|
||||
background-color: #212529;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
|
||||
.dashboard-theme-dark .card {
|
||||
background-color: #343a40;
|
||||
color: #f8f9fa;
|
||||
border-color: #495057;
|
||||
background-color: #343a40;
|
||||
color: #f8f9fa;
|
||||
border-color: #495057;
|
||||
}
|
||||
|
||||
.dashboard-theme-dark .card-header {
|
||||
background-color: #495057;
|
||||
border-bottom-color: #6c757d;
|
||||
background-color: #495057;
|
||||
border-bottom-color: #6c757d;
|
||||
}
|
||||
|
||||
.dashboard-theme-dark .stat-card .stat-label {
|
||||
color: #adb5bd;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
/*Time period selector*/
|
||||
.time-period-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
/* Increased gap */
|
||||
margin-bottom: 1.5rem;
|
||||
/* Increased gap */
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
/* Increased margin */
|
||||
/* Increased margin */
|
||||
}
|
||||
|
||||
.time-period-selector .btn-group {
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.time-period-selector .btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
|
||||
/* Bootstrap-like padding */
|
||||
font-size: 0.875rem;
|
||||
/* Bootstrap-like padding */
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/*Custom metric selector*/
|
||||
.metric-selector {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric-selector .nav-link {
|
||||
white-space: nowrap;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.metric-selector .nav-link.active {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-radius: 0.25rem;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/*Dashboard loading states*/
|
||||
.widget-placeholder {
|
||||
min-height: 300px;
|
||||
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
|
||||
min-height: 300px;
|
||||
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
|
||||
|
||||
/* Lighter gradient */
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.8s infinite ease-in-out;
|
||||
/* Lighter gradient */
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.8s infinite ease-in-out;
|
||||
|
||||
/* Smoother animation */
|
||||
border-radius: 0.5rem;
|
||||
/* Smoother animation */
|
||||
border-radius: 0.5rem;
|
||||
|
||||
/* Consistent with cards */
|
||||
/* Consistent with cards */
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*Dashboard empty states*/
|
||||
.empty-state {
|
||||
padding: 2.5rem;
|
||||
padding: 2.5rem;
|
||||
|
||||
/* Increased padding */
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
background-color: #f8f9fa;
|
||||
/* Increased padding */
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
background-color: #f8f9fa;
|
||||
|
||||
/* Light background for empty state */
|
||||
border-radius: 0.5rem;
|
||||
border: 1px dashed #ced4da;
|
||||
/* Light background for empty state */
|
||||
border-radius: 0.5rem;
|
||||
border: 1px dashed #ced4da;
|
||||
|
||||
/* Dashed border */
|
||||
/* Dashed border */
|
||||
}
|
||||
|
||||
.empty-state .empty-state-icon {
|
||||
font-size: 3.5rem;
|
||||
font-size: 3.5rem;
|
||||
|
||||
/* Larger icon */
|
||||
margin-bottom: 1.5rem;
|
||||
opacity: 0.4;
|
||||
/* Larger icon */
|
||||
margin-bottom: 1.5rem;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.empty-state .empty-state-message {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.2rem;
|
||||
|
||||
/* Slightly larger message */
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
/* Slightly larger message */
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.empty-state .btn {
|
||||
margin-top: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/*Responsive adjustments*/
|
||||
@media (width <=767.98px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
.stat-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-card .stat-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
.stat-card .stat-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.stat-card .stat-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*Preserve colored background for stat cards in both themes*/
|
||||
.col-md-3 .card.stats-card.bg-primary {
|
||||
background-color: var(--bs-primary) !important;
|
||||
color: white !important;
|
||||
background-color: var(--bs-primary) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.col-md-3 .card.stats-card.bg-success {
|
||||
background-color: var(--bs-success) !important;
|
||||
color: white !important;
|
||||
background-color: var(--bs-success) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.col-md-3 .card.stats-card.bg-info {
|
||||
background-color: var(--bs-info) !important;
|
||||
color: white !important;
|
||||
background-color: var(--bs-info) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.col-md-3 .card.stats-card.bg-warning {
|
||||
background-color: var(--bs-warning) !important;
|
||||
color: white !important;
|
||||
background-color: var(--bs-warning) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.col-md-3 .card.stats-card.bg-danger {
|
||||
background-color: var(--bs-danger) !important;
|
||||
color: white !important;
|
||||
background-color: var(--bs-danger) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.col-md-3 .card.stats-card.bg-secondary {
|
||||
background-color: var(--bs-secondary) !important;
|
||||
color: white !important;
|
||||
background-color: var(--bs-secondary) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.col-md-3 .card.stats-card.bg-light {
|
||||
background-color: var(--bs-light) !important;
|
||||
color: var(--bs-dark) !important;
|
||||
background-color: var(--bs-light) !important;
|
||||
color: var(--bs-dark) !important;
|
||||
}
|
||||
|
||||
/*Stats Cards Alignment Fix (Bottom Align, No Overlap)*/
|
||||
.stats-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
flex: 1 1 0;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
flex: 1 1 0;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
|
||||
/* Push content to bottom */
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
/* Push content to bottom */
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
|
||||
/* Remove min-height/height for natural stretch */
|
||||
/* Remove min-height/height for natural stretch */
|
||||
}
|
||||
|
||||
@@ -5,361 +5,361 @@
|
||||
|
||||
/*General Styles*/
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
background-color: #f4f7f9;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
background-color: #f4f7f9;
|
||||
|
||||
/* Lighter, cleaner background */
|
||||
color: #333;
|
||||
/* Lighter, cleaner background */
|
||||
color: #333;
|
||||
|
||||
/* Darker text for better contrast */
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
/* Darker text for better contrast */
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
|
||||
/* Added for sticky footer */
|
||||
flex-direction: column;
|
||||
/* Added for sticky footer */
|
||||
flex-direction: column;
|
||||
|
||||
/* Added for sticky footer */
|
||||
min-height: 100vh;
|
||||
/* Added for sticky footer */
|
||||
min-height: 100vh;
|
||||
|
||||
/* Ensures body takes at least full viewport height */
|
||||
/* Ensures body takes at least full viewport height */
|
||||
}
|
||||
|
||||
/*Navbar adjustments (if needed, Bootstrap usually handles this well)*/
|
||||
.navbar {
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
|
||||
box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
|
||||
|
||||
/* Subtle shadow for depth */
|
||||
/* Subtle shadow for depth */
|
||||
}
|
||||
|
||||
/*Helper Classes*/
|
||||
.text-truncate-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.min-w-150 {
|
||||
min-width: 150px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/*Card styles*/
|
||||
.card {
|
||||
border: 1px solid #e0e5e9;
|
||||
border: 1px solid #e0e5e9;
|
||||
|
||||
/* Lighter border */
|
||||
border-radius: 0.5rem;
|
||||
/* Lighter border */
|
||||
border-radius: 0.5rem;
|
||||
|
||||
/* Slightly more rounded corners */
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
|
||||
/* Slightly more rounded corners */
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
|
||||
|
||||
/* Softer, more modern shadow */
|
||||
transition:
|
||||
transform 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out;
|
||||
margin-bottom: 1.5rem;
|
||||
/* Softer, more modern shadow */
|
||||
transition:
|
||||
transform 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
/* Consistent margin */
|
||||
/* Consistent margin */
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgb(0 0 0 / 10%);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #fff;
|
||||
background-color: #fff;
|
||||
|
||||
/* Clean white header */
|
||||
border-bottom: 1px solid #e0e5e9;
|
||||
font-weight: 500;
|
||||
/* Clean white header */
|
||||
border-bottom: 1px solid #e0e5e9;
|
||||
font-weight: 500;
|
||||
|
||||
/* Slightly bolder header text */
|
||||
padding: 0.75rem 1.25rem;
|
||||
/* Slightly bolder header text */
|
||||
padding: 0.75rem 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.15rem;
|
||||
font-size: 1.15rem;
|
||||
|
||||
/* Adjusted card title size */
|
||||
font-weight: 600;
|
||||
/* Adjusted card title size */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/*Sidebar enhancements*/
|
||||
.sidebar {
|
||||
background-color: #fff;
|
||||
background-color: #fff;
|
||||
|
||||
/* White sidebar for a cleaner look */
|
||||
border-right: 1px solid #e0e5e9;
|
||||
box-shadow: 2px 0 5px rgb(0 0 0 / 3%);
|
||||
transition: all 0.3s;
|
||||
/* White sidebar for a cleaner look */
|
||||
border-right: 1px solid #e0e5e9;
|
||||
box-shadow: 2px 0 5px rgb(0 0 0 / 3%);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.sidebar-sticky {
|
||||
padding-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #4a5568;
|
||||
color: #4a5568;
|
||||
|
||||
/* Softer link color */
|
||||
padding: 0.65rem 1.25rem;
|
||||
/* Softer link color */
|
||||
padding: 0.65rem 1.25rem;
|
||||
|
||||
/* Adjusted padding */
|
||||
border-radius: 0.375rem;
|
||||
/* Adjusted padding */
|
||||
border-radius: 0.375rem;
|
||||
|
||||
/* Bootstrap-like rounded corners for links */
|
||||
margin: 0.1rem 0.5rem;
|
||||
/* Bootstrap-like rounded corners for links */
|
||||
margin: 0.1rem 0.5rem;
|
||||
|
||||
/* Margin around links */
|
||||
font-weight: 500;
|
||||
/* Margin around links */
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
color: #007bff;
|
||||
color: #007bff;
|
||||
|
||||
/* Primary color on hover */
|
||||
background-color: #e9f2ff;
|
||||
/* Primary color on hover */
|
||||
background-color: #e9f2ff;
|
||||
|
||||
/* Light blue background on hover */
|
||||
/* Light blue background on hover */
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: #007bff;
|
||||
background-color: #d6e4ff;
|
||||
color: #007bff;
|
||||
background-color: #d6e4ff;
|
||||
|
||||
/* Slightly darker blue for active */
|
||||
font-weight: 600;
|
||||
/* Slightly darker blue for active */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar .nav-link i.me-2 {
|
||||
width: 20px;
|
||||
width: 20px;
|
||||
|
||||
/* Ensure icons align well */
|
||||
text-align: center;
|
||||
margin-right: 0.75rem !important;
|
||||
/* Ensure icons align well */
|
||||
text-align: center;
|
||||
margin-right: 0.75rem !important;
|
||||
|
||||
/* Consistent icon spacing */
|
||||
/* Consistent icon spacing */
|
||||
}
|
||||
|
||||
.sidebar .nav-header {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #718096;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #718096;
|
||||
|
||||
/* Softer header color */
|
||||
padding: 0.5rem 1.25rem;
|
||||
margin-top: 1rem;
|
||||
/* Softer header color */
|
||||
padding: 0.5rem 1.25rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/*Dashboard stats cards*/
|
||||
.stats-card {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-card h3 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stats-card p {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0;
|
||||
opacity: 0.8;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/*Chart containers*/
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/*Loading overlay*/
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgb(255 255 255 / 70%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgb(255 255 255 / 70%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/*Table enhancements*/
|
||||
.table {
|
||||
border-color: #e0e5e9;
|
||||
border-color: #e0e5e9;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
font-weight: 600;
|
||||
|
||||
/* Bolder table headers */
|
||||
color: #4a5568;
|
||||
background-color: #f8f9fc;
|
||||
/* Bolder table headers */
|
||||
color: #4a5568;
|
||||
background-color: #f8f9fc;
|
||||
|
||||
/* Light background for headers */
|
||||
/* Light background for headers */
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgb(0 0 0 / 2%);
|
||||
background-color: rgb(0 0 0 / 2%);
|
||||
|
||||
/* Very subtle striping */
|
||||
/* Very subtle striping */
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #e9f2ff;
|
||||
background-color: #e9f2ff;
|
||||
|
||||
/* Consistent hover with sidebar */
|
||||
/* Consistent hover with sidebar */
|
||||
}
|
||||
|
||||
/*Form improvements*/
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-color: #ced4da;
|
||||
border-radius: 0.375rem;
|
||||
border-color: #ced4da;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
/* Consistent border radius */
|
||||
padding: 0.5rem 0.75rem;
|
||||
/* Consistent border radius */
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
/* Adjusted padding */
|
||||
/* Adjusted padding */
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #86b7fe;
|
||||
border-color: #86b7fe;
|
||||
|
||||
/* Bootstrap focus color */
|
||||
box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%);
|
||||
/* Bootstrap focus color */
|
||||
box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%);
|
||||
|
||||
/* Bootstrap focus shadow */
|
||||
/* Bootstrap focus shadow */
|
||||
}
|
||||
|
||||
/*Button styling*/
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
/* Consistent border radius */
|
||||
padding: 0.5rem 1rem;
|
||||
/* Consistent border radius */
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
/* Standard button padding */
|
||||
font-weight: 500;
|
||||
transition:
|
||||
background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out,
|
||||
box-shadow 0.15s ease-in-out;
|
||||
/* Standard button padding */
|
||||
font-weight: 500;
|
||||
transition:
|
||||
background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out,
|
||||
box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0069d9;
|
||||
border-color: #0062cc;
|
||||
background-color: #0069d9;
|
||||
border-color: #0062cc;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
border-color: #545b62;
|
||||
background-color: #5a6268;
|
||||
border-color: #545b62;
|
||||
}
|
||||
|
||||
/*Alert styling*/
|
||||
.alert {
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.9rem 1.25rem;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.9rem 1.25rem;
|
||||
}
|
||||
|
||||
/*Chat transcript styling*/
|
||||
.chat-transcript {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.chat-transcript pre {
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
margin-bottom: 0;
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/*Footer styling*/
|
||||
footer {
|
||||
background-color: #fff;
|
||||
background-color: #fff;
|
||||
|
||||
/* White footer */
|
||||
border-top: 1px solid #e0e5e9;
|
||||
padding: 1.5rem 0;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
margin-top: auto;
|
||||
/* White footer */
|
||||
border-top: 1px solid #e0e5e9;
|
||||
padding: 1.5rem 0;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
margin-top: auto;
|
||||
|
||||
/* Added for sticky footer */
|
||||
/* Added for sticky footer */
|
||||
}
|
||||
|
||||
/*Responsive adjustments*/
|
||||
@media (width <=767.98px) {
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.stats-card h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.stats-card h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*Print styles*/
|
||||
@media print {
|
||||
.sidebar,
|
||||
.navbar,
|
||||
.btn,
|
||||
footer {
|
||||
display: none !important;
|
||||
}
|
||||
.sidebar,
|
||||
.navbar,
|
||||
.btn,
|
||||
footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
break-inside: avoid;
|
||||
height: auto !important;
|
||||
}
|
||||
.chart-container {
|
||||
break-inside: avoid;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,269 +7,268 @@
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Only initialize if AJAX navigation is enabled
|
||||
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
|
||||
setupAjaxNavigation();
|
||||
// Only initialize if AJAX navigation is enabled
|
||||
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
|
||||
setupAjaxNavigation();
|
||||
}
|
||||
|
||||
// Function to set up AJAX navigation for the application
|
||||
function setupAjaxNavigation() {
|
||||
// Configuration
|
||||
const config = {
|
||||
mainContentSelector: "#main-content", // Selector for the main content area
|
||||
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
|
||||
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
|
||||
excludePatterns: [
|
||||
// URL patterns to exclude from AJAX navigation
|
||||
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
|
||||
/\/admin\//, // Admin pages
|
||||
/\/accounts\/logout\//, // Logout page
|
||||
/\/api\//, // API endpoints
|
||||
],
|
||||
};
|
||||
|
||||
// Create and insert the loading indicator
|
||||
if (!document.getElementById(config.loadingIndicatorId)) {
|
||||
const loadingIndicator = document.createElement("div");
|
||||
loadingIndicator.id = config.loadingIndicatorId;
|
||||
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
|
||||
loadingIndicator.innerHTML =
|
||||
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
|
||||
loadingIndicator.style.display = "none";
|
||||
loadingIndicator.style.zIndex = "9999";
|
||||
document.body.appendChild(loadingIndicator);
|
||||
}
|
||||
|
||||
// Function to set up AJAX navigation for the application
|
||||
function setupAjaxNavigation() {
|
||||
// Configuration
|
||||
const config = {
|
||||
mainContentSelector: "#main-content", // Selector for the main content area
|
||||
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
|
||||
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
|
||||
excludePatterns: [
|
||||
// URL patterns to exclude from AJAX navigation
|
||||
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
|
||||
/\/admin\//, // Admin pages
|
||||
/\/accounts\/logout\//, // Logout page
|
||||
/\/api\//, // API endpoints
|
||||
],
|
||||
};
|
||||
// Get the loading indicator element
|
||||
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
|
||||
|
||||
// Create and insert the loading indicator
|
||||
if (!document.getElementById(config.loadingIndicatorId)) {
|
||||
const loadingIndicator = document.createElement("div");
|
||||
loadingIndicator.id = config.loadingIndicatorId;
|
||||
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
|
||||
loadingIndicator.innerHTML =
|
||||
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
|
||||
loadingIndicator.style.display = "none";
|
||||
loadingIndicator.style.zIndex = "9999";
|
||||
document.body.appendChild(loadingIndicator);
|
||||
// Get the main content container
|
||||
const mainContent = document.querySelector(config.mainContentSelector);
|
||||
if (!mainContent) {
|
||||
console.warn("Main content container not found. AJAX navigation disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to check if a URL should be excluded from AJAX navigation
|
||||
function shouldExcludeUrl(url) {
|
||||
for (const pattern of config.excludePatterns) {
|
||||
if (pattern.test(url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the loading indicator element
|
||||
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
|
||||
// Function to show the loading indicator
|
||||
function showLoading() {
|
||||
loadingIndicator.style.display = "block";
|
||||
}
|
||||
|
||||
// Get the main content container
|
||||
const mainContent = document.querySelector(config.mainContentSelector);
|
||||
if (!mainContent) {
|
||||
console.warn("Main content container not found. AJAX navigation disabled.");
|
||||
return;
|
||||
}
|
||||
// Function to hide the loading indicator
|
||||
function hideLoading() {
|
||||
loadingIndicator.style.display = "none";
|
||||
}
|
||||
|
||||
// Function to check if a URL should be excluded from AJAX navigation
|
||||
function shouldExcludeUrl(url) {
|
||||
for (const pattern of config.excludePatterns) {
|
||||
if (pattern.test(url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to show the loading indicator
|
||||
function showLoading() {
|
||||
loadingIndicator.style.display = "block";
|
||||
}
|
||||
|
||||
// Function to hide the loading indicator
|
||||
function hideLoading() {
|
||||
loadingIndicator.style.display = "none";
|
||||
}
|
||||
|
||||
// Function to handle AJAX page navigation
|
||||
function handlePageNavigation(url, pushState = true) {
|
||||
if (shouldExcludeUrl(url)) {
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
showLoading();
|
||||
const currentScrollPos = window.scrollY;
|
||||
fetch(url, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-AJAX-Navigation": "true",
|
||||
Accept: "text/html",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok)
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
return response.text();
|
||||
})
|
||||
.then((html) => {
|
||||
// Parse the HTML and extract #main-content
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = html;
|
||||
const newContent = tempDiv.querySelector(config.mainContentSelector);
|
||||
if (!newContent) throw new Error("Could not find main content in the response");
|
||||
mainContent.innerHTML = newContent.innerHTML;
|
||||
// Update the page title
|
||||
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
||||
if (titleMatch) document.title = titleMatch[1];
|
||||
// Re-initialize dynamic content
|
||||
reloadScripts(mainContent);
|
||||
attachEventListeners();
|
||||
initializePageScripts();
|
||||
if (pushState) {
|
||||
history.pushState(
|
||||
{ url: url, title: document.title, scrollPos: currentScrollPos },
|
||||
document.title,
|
||||
url,
|
||||
);
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
} else if (window.history.state && window.history.state.scrollPos) {
|
||||
window.scrollTo({ top: window.history.state.scrollPos });
|
||||
}
|
||||
hideLoading();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error during AJAX navigation:", error);
|
||||
hideLoading();
|
||||
window.location.href = url;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to reload and execute scripts in new content
|
||||
function reloadScripts(container) {
|
||||
const scripts = container.getElementsByTagName("script");
|
||||
for (let script of scripts) {
|
||||
const newScript = document.createElement("script");
|
||||
|
||||
// Copy all attributes
|
||||
Array.from(script.attributes).forEach((attr) => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
|
||||
// Copy inline script content
|
||||
newScript.textContent = script.textContent;
|
||||
|
||||
// Replace old script with new one
|
||||
script.parentNode.replaceChild(newScript, script);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle form submissions
|
||||
function handleFormSubmission(form, e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show loading indicator
|
||||
showLoading();
|
||||
|
||||
// Get form data
|
||||
const formData = new FormData(form);
|
||||
const method = form.method.toLowerCase();
|
||||
const url = form.action || window.location.href;
|
||||
|
||||
// Configure fetch options
|
||||
const fetchOptions = {
|
||||
method: method,
|
||||
headers: {
|
||||
"X-AJAX-Navigation": "true",
|
||||
},
|
||||
};
|
||||
|
||||
// Handle different HTTP methods
|
||||
if (method === "get") {
|
||||
const queryParams = new URLSearchParams(formData).toString();
|
||||
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
|
||||
} else {
|
||||
fetchOptions.body = formData;
|
||||
|
||||
fetch(url, fetchOptions)
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.redirect) {
|
||||
// Handle server-side redirects
|
||||
handlePageNavigation(data.redirect, true);
|
||||
} else {
|
||||
// Update page content
|
||||
mainContent.innerHTML = data.html;
|
||||
document.title = data.title || document.title;
|
||||
|
||||
// Re-initialize dynamic content
|
||||
reloadScripts(mainContent);
|
||||
attachEventListeners();
|
||||
initializePageScripts();
|
||||
|
||||
// Update URL if needed
|
||||
if (data.url) {
|
||||
history.pushState({ url: data.url }, document.title, data.url);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Form submission error:", error);
|
||||
// Fallback to traditional form submission
|
||||
form.submit();
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to initialize scripts needed for the new page content
|
||||
function initializePageScripts() {
|
||||
// Re-initialize any custom scripts that might be needed
|
||||
if (typeof setupAjaxPagination === "function") {
|
||||
setupAjaxPagination();
|
||||
}
|
||||
|
||||
// Initialize Bootstrap tooltips, popovers, etc.
|
||||
if (typeof bootstrap !== "undefined") {
|
||||
// Initialize tooltips
|
||||
const tooltipTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
|
||||
);
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize popovers
|
||||
const popoverTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="popover"]'),
|
||||
);
|
||||
popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to attach event listeners to forms and links
|
||||
function attachEventListeners() {
|
||||
// Handle AJAX navigation links
|
||||
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
|
||||
if (!link.dataset.ajaxNavInitialized) {
|
||||
link.addEventListener("click", function (e) {
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
|
||||
return; // Let the browser handle these cases
|
||||
}
|
||||
e.preventDefault();
|
||||
handlePageNavigation(this.href);
|
||||
});
|
||||
link.dataset.ajaxNavInitialized = "true";
|
||||
}
|
||||
});
|
||||
|
||||
// Handle forms with AJAX
|
||||
document
|
||||
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
|
||||
.forEach((form) => {
|
||||
if (!form.dataset.ajaxFormInitialized) {
|
||||
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
|
||||
form.dataset.ajaxFormInitialized = "true";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial attachment of event listeners
|
||||
attachEventListeners();
|
||||
|
||||
// Handle browser back/forward buttons
|
||||
window.addEventListener("popstate", function (event) {
|
||||
if (event.state && event.state.url) {
|
||||
handlePageNavigation(event.state.url, false);
|
||||
} else {
|
||||
// Fallback to current URL if no state
|
||||
handlePageNavigation(window.location.href, false);
|
||||
}
|
||||
// Function to handle AJAX page navigation
|
||||
function handlePageNavigation(url, pushState = true) {
|
||||
if (shouldExcludeUrl(url)) {
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
showLoading();
|
||||
const currentScrollPos = window.scrollY;
|
||||
fetch(url, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-AJAX-Navigation": "true",
|
||||
Accept: "text/html",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
|
||||
return response.text();
|
||||
})
|
||||
.then((html) => {
|
||||
// Parse the HTML and extract #main-content
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = html;
|
||||
const newContent = tempDiv.querySelector(config.mainContentSelector);
|
||||
if (!newContent) throw new Error("Could not find main content in the response");
|
||||
mainContent.innerHTML = newContent.innerHTML;
|
||||
// Update the page title
|
||||
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
||||
if (titleMatch) document.title = titleMatch[1];
|
||||
// Re-initialize dynamic content
|
||||
reloadScripts(mainContent);
|
||||
attachEventListeners();
|
||||
initializePageScripts();
|
||||
if (pushState) {
|
||||
history.pushState(
|
||||
{ url: url, title: document.title, scrollPos: currentScrollPos },
|
||||
document.title,
|
||||
url,
|
||||
);
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
} else if (window.history.state && window.history.state.scrollPos) {
|
||||
window.scrollTo({ top: window.history.state.scrollPos });
|
||||
}
|
||||
hideLoading();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error during AJAX navigation:", error);
|
||||
hideLoading();
|
||||
window.location.href = url;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to reload and execute scripts in new content
|
||||
function reloadScripts(container) {
|
||||
const scripts = container.getElementsByTagName("script");
|
||||
for (let script of scripts) {
|
||||
const newScript = document.createElement("script");
|
||||
|
||||
// Copy all attributes
|
||||
Array.from(script.attributes).forEach((attr) => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
|
||||
// Copy inline script content
|
||||
newScript.textContent = script.textContent;
|
||||
|
||||
// Replace old script with new one
|
||||
script.parentNode.replaceChild(newScript, script);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle form submissions
|
||||
function handleFormSubmission(form, e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show loading indicator
|
||||
showLoading();
|
||||
|
||||
// Get form data
|
||||
const formData = new FormData(form);
|
||||
const method = form.method.toLowerCase();
|
||||
const url = form.action || window.location.href;
|
||||
|
||||
// Configure fetch options
|
||||
const fetchOptions = {
|
||||
method: method,
|
||||
headers: {
|
||||
"X-AJAX-Navigation": "true",
|
||||
},
|
||||
};
|
||||
|
||||
// Handle different HTTP methods
|
||||
if (method === "get") {
|
||||
const queryParams = new URLSearchParams(formData).toString();
|
||||
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
|
||||
} else {
|
||||
fetchOptions.body = formData;
|
||||
|
||||
fetch(url, fetchOptions)
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.redirect) {
|
||||
// Handle server-side redirects
|
||||
handlePageNavigation(data.redirect, true);
|
||||
} else {
|
||||
// Update page content
|
||||
mainContent.innerHTML = data.html;
|
||||
document.title = data.title || document.title;
|
||||
|
||||
// Re-initialize dynamic content
|
||||
reloadScripts(mainContent);
|
||||
attachEventListeners();
|
||||
initializePageScripts();
|
||||
|
||||
// Update URL if needed
|
||||
if (data.url) {
|
||||
history.pushState({ url: data.url }, document.title, data.url);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Form submission error:", error);
|
||||
// Fallback to traditional form submission
|
||||
form.submit();
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to initialize scripts needed for the new page content
|
||||
function initializePageScripts() {
|
||||
// Re-initialize any custom scripts that might be needed
|
||||
if (typeof setupAjaxPagination === "function") {
|
||||
setupAjaxPagination();
|
||||
}
|
||||
|
||||
// Initialize Bootstrap tooltips, popovers, etc.
|
||||
if (typeof bootstrap !== "undefined") {
|
||||
// Initialize tooltips
|
||||
const tooltipTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
|
||||
);
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize popovers
|
||||
const popoverTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="popover"]'),
|
||||
);
|
||||
popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to attach event listeners to forms and links
|
||||
function attachEventListeners() {
|
||||
// Handle AJAX navigation links
|
||||
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
|
||||
if (!link.dataset.ajaxNavInitialized) {
|
||||
link.addEventListener("click", function (e) {
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
|
||||
return; // Let the browser handle these cases
|
||||
}
|
||||
e.preventDefault();
|
||||
handlePageNavigation(this.href);
|
||||
});
|
||||
link.dataset.ajaxNavInitialized = "true";
|
||||
}
|
||||
});
|
||||
|
||||
// Handle forms with AJAX
|
||||
document
|
||||
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
|
||||
.forEach((form) => {
|
||||
if (!form.dataset.ajaxFormInitialized) {
|
||||
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
|
||||
form.dataset.ajaxFormInitialized = "true";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial attachment of event listeners
|
||||
attachEventListeners();
|
||||
|
||||
// Handle browser back/forward buttons
|
||||
window.addEventListener("popstate", function (event) {
|
||||
if (event.state && event.state.url) {
|
||||
handlePageNavigation(event.state.url, false);
|
||||
} else {
|
||||
// Fallback to current URL if no state
|
||||
handlePageNavigation(window.location.href, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,101 +7,101 @@
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize AJAX pagination
|
||||
setupAjaxPagination();
|
||||
// Initialize AJAX pagination
|
||||
setupAjaxPagination();
|
||||
|
||||
// Function to set up AJAX pagination for the entire application
|
||||
function setupAjaxPagination() {
|
||||
// Configuration - can be customized per page if needed
|
||||
const config = {
|
||||
contentContainerId: "ajax-content-container", // ID of the container to update
|
||||
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
|
||||
paginationLinkClass: "pagination-link", // Class for pagination links
|
||||
retryMessage: "An error occurred while loading data. Please try again.",
|
||||
};
|
||||
// Function to set up AJAX pagination for the entire application
|
||||
function setupAjaxPagination() {
|
||||
// Configuration - can be customized per page if needed
|
||||
const config = {
|
||||
contentContainerId: "ajax-content-container", // ID of the container to update
|
||||
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
|
||||
paginationLinkClass: "pagination-link", // Class for pagination links
|
||||
retryMessage: "An error occurred while loading data. Please try again.",
|
||||
};
|
||||
|
||||
// Get container elements
|
||||
const contentContainer = document.getElementById(config.contentContainerId);
|
||||
const loadingSpinner = document.getElementById(config.loadingSpinnerId);
|
||||
// Get container elements
|
||||
const contentContainer = document.getElementById(config.contentContainerId);
|
||||
const loadingSpinner = document.getElementById(config.loadingSpinnerId);
|
||||
|
||||
// Exit if the page doesn't have the required elements
|
||||
if (!contentContainer || !loadingSpinner) return;
|
||||
// Exit if the page doesn't have the required elements
|
||||
if (!contentContainer || !loadingSpinner) return;
|
||||
|
||||
// Function to handle pagination clicks
|
||||
function setupPaginationListeners() {
|
||||
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
handleAjaxNavigation(this.href);
|
||||
// Function to handle pagination clicks
|
||||
function setupPaginationListeners() {
|
||||
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
handleAjaxNavigation(this.href);
|
||||
|
||||
// Get the page number if available
|
||||
const page = this.getAttribute("data-page");
|
||||
// Get the page number if available
|
||||
const page = this.getAttribute("data-page");
|
||||
|
||||
// Update browser URL without refreshing
|
||||
const newUrl = this.href;
|
||||
history.pushState({ url: newUrl, page: page }, "", newUrl);
|
||||
});
|
||||
});
|
||||
}
|
||||
// Update browser URL without refreshing
|
||||
const newUrl = this.href;
|
||||
history.pushState({ url: newUrl, page: page }, "", newUrl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to handle AJAX navigation
|
||||
function handleAjaxNavigation(url) {
|
||||
// Show loading spinner
|
||||
contentContainer.classList.add("d-none");
|
||||
loadingSpinner.classList.remove("d-none");
|
||||
// Function to handle AJAX navigation
|
||||
function handleAjaxNavigation(url) {
|
||||
// Show loading spinner
|
||||
contentContainer.classList.add("d-none");
|
||||
loadingSpinner.classList.remove("d-none");
|
||||
|
||||
// Fetch data via AJAX
|
||||
fetch(url, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.status === "success") {
|
||||
// Update the content
|
||||
contentContainer.innerHTML = data.html_data;
|
||||
// Fetch data via AJAX
|
||||
fetch(url, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.status === "success") {
|
||||
// Update the content
|
||||
contentContainer.innerHTML = data.html_data;
|
||||
|
||||
// Re-attach event listeners to new pagination links
|
||||
setupPaginationListeners();
|
||||
// Re-attach event listeners to new pagination links
|
||||
setupPaginationListeners();
|
||||
|
||||
// Update any summary data if present and the page provides it
|
||||
if (typeof updateSummary === "function" && data.summary) {
|
||||
updateSummary(data);
|
||||
}
|
||||
|
||||
// Hide loading spinner, show content
|
||||
loadingSpinner.classList.add("d-none");
|
||||
contentContainer.classList.remove("d-none");
|
||||
|
||||
// Scroll to top of the content container
|
||||
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching data:", error);
|
||||
loadingSpinner.classList.add("d-none");
|
||||
contentContainer.classList.remove("d-none");
|
||||
alert(config.retryMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// Initial setup of event listeners
|
||||
setupPaginationListeners();
|
||||
|
||||
// Handle browser back/forward buttons
|
||||
window.addEventListener("popstate", function (event) {
|
||||
if (event.state && event.state.url) {
|
||||
handleAjaxNavigation(event.state.url);
|
||||
} else {
|
||||
// If no state, fetch current URL
|
||||
handleAjaxNavigation(window.location.href);
|
||||
// Update any summary data if present and the page provides it
|
||||
if (typeof updateSummary === "function" && data.summary) {
|
||||
updateSummary(data);
|
||||
}
|
||||
|
||||
// Hide loading spinner, show content
|
||||
loadingSpinner.classList.add("d-none");
|
||||
contentContainer.classList.remove("d-none");
|
||||
|
||||
// Scroll to top of the content container
|
||||
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching data:", error);
|
||||
loadingSpinner.classList.add("d-none");
|
||||
contentContainer.classList.remove("d-none");
|
||||
alert(config.retryMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// Initial setup of event listeners
|
||||
setupPaginationListeners();
|
||||
|
||||
// Handle browser back/forward buttons
|
||||
window.addEventListener("popstate", function (event) {
|
||||
if (event.state && event.state.url) {
|
||||
handleAjaxNavigation(event.state.url);
|
||||
} else {
|
||||
// If no state, fetch current URL
|
||||
handleAjaxNavigation(window.location.href);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,478 +8,469 @@
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Set up Plotly default config based on theme
|
||||
function updatePlotlyTheme() {
|
||||
// Force a fresh check of the current theme
|
||||
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
|
||||
console.log(
|
||||
"updatePlotlyTheme called - Current theme mode:",
|
||||
isDarkMode ? "dark" : "light",
|
||||
);
|
||||
// Set up Plotly default config based on theme
|
||||
function updatePlotlyTheme() {
|
||||
// Force a fresh check of the current theme
|
||||
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
|
||||
console.log("updatePlotlyTheme called - Current theme mode:", isDarkMode ? "dark" : "light");
|
||||
|
||||
window.plotlyDefaultLayout = {
|
||||
font: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
},
|
||||
paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
||||
plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
||||
colorway: [
|
||||
"#4285F4",
|
||||
"#EA4335",
|
||||
"#FBBC05",
|
||||
"#34A853",
|
||||
"#FF6D00",
|
||||
"#46BDC6",
|
||||
"#DB4437",
|
||||
"#0F9D58",
|
||||
"#AB47BC",
|
||||
"#00ACC1",
|
||||
],
|
||||
margin: {
|
||||
l: 50,
|
||||
r: 30,
|
||||
t: 30,
|
||||
b: 50,
|
||||
pad: 10,
|
||||
},
|
||||
hovermode: "closest",
|
||||
xaxis: {
|
||||
automargin: true,
|
||||
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
||||
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
||||
title: {
|
||||
font: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
},
|
||||
tickfont: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
automargin: true,
|
||||
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
||||
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
||||
title: {
|
||||
font: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
},
|
||||
tickfont: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
font: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
||||
},
|
||||
modebar: {
|
||||
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
activecolor: isDarkMode ? "#6ea8fe" : "#007bff",
|
||||
},
|
||||
};
|
||||
|
||||
// Config for specific chart types
|
||||
window.plotlyBarConfig = {
|
||||
...window.plotlyDefaultLayout,
|
||||
bargap: 0.1,
|
||||
bargroupgap: 0.2,
|
||||
};
|
||||
|
||||
window.plotlyPieConfig = {
|
||||
...window.plotlyDefaultLayout,
|
||||
showlegend: true,
|
||||
legend: {
|
||||
...window.plotlyDefaultLayout.legend,
|
||||
xanchor: "center",
|
||||
yanchor: "top",
|
||||
y: -0.2,
|
||||
x: 0.5,
|
||||
orientation: "h",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize theme setting
|
||||
updatePlotlyTheme();
|
||||
|
||||
// Listen for theme changes
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
if (mutation.attributeName === "data-bs-theme") {
|
||||
console.log(
|
||||
"Theme changed detected by observer:",
|
||||
document.documentElement.getAttribute("data-bs-theme"),
|
||||
);
|
||||
updatePlotlyTheme();
|
||||
// Use a small delay to ensure styles have been applied
|
||||
setTimeout(refreshAllCharts, 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, { attributes: true });
|
||||
|
||||
// Chart responsiveness
|
||||
function resizeCharts() {
|
||||
const charts = document.querySelectorAll(".chart-container");
|
||||
charts.forEach((chart) => {
|
||||
if (chart.id && window.Plotly) {
|
||||
Plotly.relayout(chart.id, {
|
||||
"xaxis.automargin": true,
|
||||
"yaxis.automargin": true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh all charts with current theme
|
||||
function refreshAllCharts() {
|
||||
if (!window.Plotly) return;
|
||||
|
||||
const currentTheme = document.documentElement.getAttribute("data-bs-theme");
|
||||
console.log("Refreshing charts with theme:", currentTheme);
|
||||
|
||||
// Update the theme settings
|
||||
updatePlotlyTheme();
|
||||
|
||||
const charts = document.querySelectorAll(".chart-container");
|
||||
charts.forEach(function (chart) {
|
||||
if (chart.id) {
|
||||
try {
|
||||
// Safe way to check if element has a plot
|
||||
const plotElement = document.getElementById(chart.id);
|
||||
if (plotElement && plotElement._fullLayout) {
|
||||
console.log("Updating chart theme for:", chart.id);
|
||||
|
||||
// Determine chart type to apply appropriate settings
|
||||
let layoutUpdate = { ...window.plotlyDefaultLayout };
|
||||
|
||||
// Check if it's a bar chart
|
||||
if (
|
||||
plotElement.data &&
|
||||
plotElement.data.some((trace) => trace.type === "bar")
|
||||
) {
|
||||
layoutUpdate = { ...window.plotlyBarConfig };
|
||||
}
|
||||
|
||||
// Check if it's a pie chart
|
||||
if (
|
||||
plotElement.data &&
|
||||
plotElement.data.some((trace) => trace.type === "pie")
|
||||
) {
|
||||
layoutUpdate = { ...window.plotlyPieConfig };
|
||||
}
|
||||
|
||||
// Force paper and plot background colors based on current theme
|
||||
// This ensures the chart background always matches the current theme
|
||||
layoutUpdate.paper_bgcolor =
|
||||
currentTheme === "dark" ? "#343a40" : "#ffffff";
|
||||
layoutUpdate.plot_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
|
||||
|
||||
// Update font colors too
|
||||
layoutUpdate.font.color = currentTheme === "dark" ? "#f8f9fa" : "#212529";
|
||||
|
||||
// Apply layout updates
|
||||
Plotly.relayout(chart.id, layoutUpdate);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error updating chart theme:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Make refreshAllCharts available globally
|
||||
window.refreshAllCharts = refreshAllCharts;
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.Plotly) {
|
||||
resizeCharts();
|
||||
}
|
||||
});
|
||||
|
||||
// Call resizeCharts on initial load
|
||||
if (window.Plotly) {
|
||||
// Use a longer delay to ensure charts are fully loaded
|
||||
setTimeout(function () {
|
||||
updatePlotlyTheme();
|
||||
refreshAllCharts();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Apply theme to newly created charts
|
||||
const originalPlotlyNewPlot = Plotly.newPlot;
|
||||
Plotly.newPlot = function () {
|
||||
const args = Array.from(arguments);
|
||||
// Get the layout argument (3rd argument)
|
||||
if (args.length >= 3 && typeof args[2] === "object") {
|
||||
// Ensure plotlyDefaultLayout is up to date
|
||||
updatePlotlyTheme();
|
||||
// Apply current theme to new plot
|
||||
args[2] = { ...window.plotlyDefaultLayout, ...args[2] };
|
||||
}
|
||||
return originalPlotlyNewPlot.apply(this, args);
|
||||
window.plotlyDefaultLayout = {
|
||||
font: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
family:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||
},
|
||||
paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
||||
plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
||||
colorway: [
|
||||
"#4285F4",
|
||||
"#EA4335",
|
||||
"#FBBC05",
|
||||
"#34A853",
|
||||
"#FF6D00",
|
||||
"#46BDC6",
|
||||
"#DB4437",
|
||||
"#0F9D58",
|
||||
"#AB47BC",
|
||||
"#00ACC1",
|
||||
],
|
||||
margin: {
|
||||
l: 50,
|
||||
r: 30,
|
||||
t: 30,
|
||||
b: 50,
|
||||
pad: 10,
|
||||
},
|
||||
hovermode: "closest",
|
||||
xaxis: {
|
||||
automargin: true,
|
||||
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
||||
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
||||
title: {
|
||||
font: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
},
|
||||
tickfont: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
automargin: true,
|
||||
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
||||
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
||||
title: {
|
||||
font: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
},
|
||||
tickfont: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
font: {
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
},
|
||||
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
||||
},
|
||||
modebar: {
|
||||
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||
activecolor: isDarkMode ? "#6ea8fe" : "#007bff",
|
||||
},
|
||||
};
|
||||
|
||||
// Time range filtering
|
||||
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
||||
if (timeRangeDropdown) {
|
||||
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
||||
timeRangeLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
const timeRange = url.searchParams.get("time_range");
|
||||
// Config for specific chart types
|
||||
window.plotlyBarConfig = {
|
||||
...window.plotlyDefaultLayout,
|
||||
bargap: 0.1,
|
||||
bargroupgap: 0.2,
|
||||
};
|
||||
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId, timeRange);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
window.plotlyPieConfig = {
|
||||
...window.plotlyDefaultLayout,
|
||||
showlegend: true,
|
||||
legend: {
|
||||
...window.plotlyDefaultLayout.legend,
|
||||
xanchor: "center",
|
||||
yanchor: "top",
|
||||
y: -0.2,
|
||||
x: 0.5,
|
||||
orientation: "h",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize theme setting
|
||||
updatePlotlyTheme();
|
||||
|
||||
// Listen for theme changes
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
if (mutation.attributeName === "data-bs-theme") {
|
||||
console.log(
|
||||
"Theme changed detected by observer:",
|
||||
document.documentElement.getAttribute("data-bs-theme"),
|
||||
);
|
||||
updatePlotlyTheme();
|
||||
// Use a small delay to ensure styles have been applied
|
||||
setTimeout(refreshAllCharts, 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, { attributes: true });
|
||||
|
||||
// Chart responsiveness
|
||||
function resizeCharts() {
|
||||
const charts = document.querySelectorAll(".chart-container");
|
||||
charts.forEach((chart) => {
|
||||
if (chart.id && window.Plotly) {
|
||||
Plotly.relayout(chart.id, {
|
||||
"xaxis.automargin": true,
|
||||
"yaxis.automargin": true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh all charts with current theme
|
||||
function refreshAllCharts() {
|
||||
if (!window.Plotly) return;
|
||||
|
||||
const currentTheme = document.documentElement.getAttribute("data-bs-theme");
|
||||
console.log("Refreshing charts with theme:", currentTheme);
|
||||
|
||||
// Update the theme settings
|
||||
updatePlotlyTheme();
|
||||
|
||||
const charts = document.querySelectorAll(".chart-container");
|
||||
charts.forEach(function (chart) {
|
||||
if (chart.id) {
|
||||
try {
|
||||
// Safe way to check if element has a plot
|
||||
const plotElement = document.getElementById(chart.id);
|
||||
if (plotElement && plotElement._fullLayout) {
|
||||
console.log("Updating chart theme for:", chart.id);
|
||||
|
||||
// Determine chart type to apply appropriate settings
|
||||
let layoutUpdate = { ...window.plotlyDefaultLayout };
|
||||
|
||||
// Check if it's a bar chart
|
||||
if (plotElement.data && plotElement.data.some((trace) => trace.type === "bar")) {
|
||||
layoutUpdate = { ...window.plotlyBarConfig };
|
||||
}
|
||||
|
||||
// Check if it's a pie chart
|
||||
if (plotElement.data && plotElement.data.some((trace) => trace.type === "pie")) {
|
||||
layoutUpdate = { ...window.plotlyPieConfig };
|
||||
}
|
||||
|
||||
// Force paper and plot background colors based on current theme
|
||||
// This ensures the chart background always matches the current theme
|
||||
layoutUpdate.paper_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
|
||||
layoutUpdate.plot_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
|
||||
|
||||
// Update font colors too
|
||||
layoutUpdate.font.color = currentTheme === "dark" ? "#f8f9fa" : "#212529";
|
||||
|
||||
// Apply layout updates
|
||||
Plotly.relayout(chart.id, layoutUpdate);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error updating chart theme:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Make refreshAllCharts available globally
|
||||
window.refreshAllCharts = refreshAllCharts;
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.Plotly) {
|
||||
resizeCharts();
|
||||
}
|
||||
});
|
||||
|
||||
// Function to fetch dashboard data
|
||||
function fetchDashboardData(dashboardId, timeRange) {
|
||||
const loadingOverlay = document.createElement("div");
|
||||
loadingOverlay.className = "loading-overlay";
|
||||
loadingOverlay.innerHTML =
|
||||
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
||||
document.querySelector("main").appendChild(loadingOverlay);
|
||||
// Call resizeCharts on initial load
|
||||
if (window.Plotly) {
|
||||
// Use a longer delay to ensure charts are fully loaded
|
||||
setTimeout(function () {
|
||||
updatePlotlyTheme();
|
||||
refreshAllCharts();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("Dashboard API response:", data);
|
||||
updateDashboardStats(data);
|
||||
updateDashboardCharts(data);
|
||||
// Apply theme to newly created charts
|
||||
const originalPlotlyNewPlot = Plotly.newPlot;
|
||||
Plotly.newPlot = function () {
|
||||
const args = Array.from(arguments);
|
||||
// Get the layout argument (3rd argument)
|
||||
if (args.length >= 3 && typeof args[2] === "object") {
|
||||
// Ensure plotlyDefaultLayout is up to date
|
||||
updatePlotlyTheme();
|
||||
// Apply current theme to new plot
|
||||
args[2] = { ...window.plotlyDefaultLayout, ...args[2] };
|
||||
}
|
||||
return originalPlotlyNewPlot.apply(this, args);
|
||||
};
|
||||
|
||||
// Update URL without page reload
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("dashboard_id", dashboardId);
|
||||
if (timeRange) {
|
||||
url.searchParams.set("time_range", timeRange);
|
||||
}
|
||||
window.history.pushState({}, "", url);
|
||||
// Time range filtering
|
||||
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
||||
if (timeRangeDropdown) {
|
||||
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
||||
timeRangeLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
const timeRange = url.searchParams.get("time_range");
|
||||
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId, timeRange);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Show error message
|
||||
const alertElement = document.createElement("div");
|
||||
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
||||
alertElement.setAttribute("role", "alert");
|
||||
alertElement.innerHTML = `
|
||||
// Function to fetch dashboard data
|
||||
function fetchDashboardData(dashboardId, timeRange) {
|
||||
const loadingOverlay = document.createElement("div");
|
||||
loadingOverlay.className = "loading-overlay";
|
||||
loadingOverlay.innerHTML =
|
||||
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
||||
document.querySelector("main").appendChild(loadingOverlay);
|
||||
|
||||
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("Dashboard API response:", data);
|
||||
updateDashboardStats(data);
|
||||
updateDashboardCharts(data);
|
||||
|
||||
// Update URL without page reload
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("dashboard_id", dashboardId);
|
||||
if (timeRange) {
|
||||
url.searchParams.set("time_range", timeRange);
|
||||
}
|
||||
window.history.pushState({}, "", url);
|
||||
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
|
||||
// Show error message
|
||||
const alertElement = document.createElement("div");
|
||||
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
||||
alertElement.setAttribute("role", "alert");
|
||||
alertElement.innerHTML = `
|
||||
Error loading dashboard data. Please try again.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
`;
|
||||
document.querySelector("main").prepend(alertElement);
|
||||
});
|
||||
document.querySelector("main").prepend(alertElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to update dashboard statistics
|
||||
function updateDashboardStats(data) {
|
||||
// Update total sessions
|
||||
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
||||
if (totalSessionsElement) {
|
||||
totalSessionsElement.textContent = data.total_sessions;
|
||||
}
|
||||
|
||||
// Function to update dashboard statistics
|
||||
function updateDashboardStats(data) {
|
||||
// Update total sessions
|
||||
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
||||
if (totalSessionsElement) {
|
||||
totalSessionsElement.textContent = data.total_sessions;
|
||||
}
|
||||
|
||||
// Update average response time
|
||||
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
||||
if (avgResponseTimeElement) {
|
||||
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
||||
}
|
||||
|
||||
// Update total tokens
|
||||
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
||||
if (totalTokensElement) {
|
||||
totalTokensElement.textContent = data.total_tokens;
|
||||
}
|
||||
|
||||
// Update total cost
|
||||
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
||||
if (totalCostElement) {
|
||||
totalCostElement.textContent = "€" + data.total_cost;
|
||||
}
|
||||
// Update average response time
|
||||
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
||||
if (avgResponseTimeElement) {
|
||||
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
||||
}
|
||||
|
||||
// Function to update dashboard charts
|
||||
function updateDashboardCharts(data) {
|
||||
// Check if Plotly is available
|
||||
if (!window.Plotly) {
|
||||
console.error("Plotly library not loaded!");
|
||||
document.querySelectorAll(".chart-container").forEach((container) => {
|
||||
container.innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update sessions over time chart
|
||||
const timeSeriesData = data.time_series_data;
|
||||
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||
try {
|
||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"sessions-time-chart",
|
||||
[
|
||||
{
|
||||
x: timeSeriesX,
|
||||
y: timeSeriesY,
|
||||
type: "scatter",
|
||||
mode: "lines+markers",
|
||||
line: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
width: 2,
|
||||
},
|
||||
marker: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
...window.plotlyDefaultLayout,
|
||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||
xaxis: {
|
||||
...window.plotlyDefaultLayout.xaxis,
|
||||
title: "Date",
|
||||
},
|
||||
yaxis: {
|
||||
...window.plotlyDefaultLayout.yaxis,
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error rendering time series chart:", error);
|
||||
document.getElementById("sessions-time-chart").innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
|
||||
}
|
||||
} else {
|
||||
document.getElementById("sessions-time-chart").innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
||||
}
|
||||
|
||||
// Update sentiment chart
|
||||
const sentimentData = data.sentiment_data;
|
||||
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||
const sentimentValues = sentimentData.map((item) => item.count);
|
||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||
return "rgb(201, 203, 207)";
|
||||
});
|
||||
|
||||
Plotly.react(
|
||||
"sentiment-chart",
|
||||
[
|
||||
{
|
||||
values: sentimentValues,
|
||||
labels: sentimentLabels,
|
||||
type: "pie",
|
||||
marker: {
|
||||
colors: sentimentColors,
|
||||
},
|
||||
hole: 0.4,
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
...window.plotlyDefaultLayout,
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Update country chart
|
||||
const countryData = data.country_data;
|
||||
if (countryData && countryData.length > 0 && window.Plotly) {
|
||||
const countryLabels = countryData.map((item) => item.country);
|
||||
const countryValues = countryData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"country-chart",
|
||||
[
|
||||
{
|
||||
x: countryValues,
|
||||
y: countryLabels,
|
||||
type: "bar",
|
||||
orientation: "h",
|
||||
marker: {
|
||||
color: "rgb(54, 162, 235)",
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
...window.plotlyDefaultLayout,
|
||||
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||
xaxis: {
|
||||
...window.plotlyDefaultLayout.xaxis,
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Update category chart
|
||||
const categoryData = data.category_data;
|
||||
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
||||
const categoryLabels = categoryData.map((item) => item.category);
|
||||
const categoryValues = categoryData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"category-chart",
|
||||
[
|
||||
{
|
||||
labels: categoryLabels,
|
||||
values: categoryValues,
|
||||
type: "pie",
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
...window.plotlyDefaultLayout,
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
},
|
||||
);
|
||||
}
|
||||
// Update total tokens
|
||||
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
||||
if (totalTokensElement) {
|
||||
totalTokensElement.textContent = data.total_tokens;
|
||||
}
|
||||
|
||||
// Dashboard selector
|
||||
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
||||
dashboardSelector.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
// Update total cost
|
||||
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
||||
if (totalCostElement) {
|
||||
totalCostElement.textContent = "€" + data.total_cost;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
// Function to update dashboard charts
|
||||
function updateDashboardCharts(data) {
|
||||
// Check if Plotly is available
|
||||
if (!window.Plotly) {
|
||||
console.error("Plotly library not loaded!");
|
||||
document.querySelectorAll(".chart-container").forEach((container) => {
|
||||
container.innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update sessions over time chart
|
||||
const timeSeriesData = data.time_series_data;
|
||||
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||
try {
|
||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"sessions-time-chart",
|
||||
[
|
||||
{
|
||||
x: timeSeriesX,
|
||||
y: timeSeriesY,
|
||||
type: "scatter",
|
||||
mode: "lines+markers",
|
||||
line: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
width: 2,
|
||||
},
|
||||
marker: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
...window.plotlyDefaultLayout,
|
||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||
xaxis: {
|
||||
...window.plotlyDefaultLayout.xaxis,
|
||||
title: "Date",
|
||||
},
|
||||
yaxis: {
|
||||
...window.plotlyDefaultLayout.yaxis,
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error rendering time series chart:", error);
|
||||
document.getElementById("sessions-time-chart").innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
|
||||
}
|
||||
} else {
|
||||
document.getElementById("sessions-time-chart").innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
||||
}
|
||||
|
||||
// Update sentiment chart
|
||||
const sentimentData = data.sentiment_data;
|
||||
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||
const sentimentValues = sentimentData.map((item) => item.count);
|
||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||
return "rgb(201, 203, 207)";
|
||||
});
|
||||
|
||||
Plotly.react(
|
||||
"sentiment-chart",
|
||||
[
|
||||
{
|
||||
values: sentimentValues,
|
||||
labels: sentimentLabels,
|
||||
type: "pie",
|
||||
marker: {
|
||||
colors: sentimentColors,
|
||||
},
|
||||
hole: 0.4,
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
...window.plotlyDefaultLayout,
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Update country chart
|
||||
const countryData = data.country_data;
|
||||
if (countryData && countryData.length > 0 && window.Plotly) {
|
||||
const countryLabels = countryData.map((item) => item.country);
|
||||
const countryValues = countryData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"country-chart",
|
||||
[
|
||||
{
|
||||
x: countryValues,
|
||||
y: countryLabels,
|
||||
type: "bar",
|
||||
orientation: "h",
|
||||
marker: {
|
||||
color: "rgb(54, 162, 235)",
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
...window.plotlyDefaultLayout,
|
||||
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||
xaxis: {
|
||||
...window.plotlyDefaultLayout.xaxis,
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Update category chart
|
||||
const categoryData = data.category_data;
|
||||
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
||||
const categoryLabels = categoryData.map((item) => item.category);
|
||||
const categoryValues = categoryData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"category-chart",
|
||||
[
|
||||
{
|
||||
labels: categoryLabels,
|
||||
values: categoryValues,
|
||||
type: "pie",
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
...window.plotlyDefaultLayout,
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard selector
|
||||
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
||||
dashboardSelector.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,241 +7,241 @@
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
// Initialize tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize popovers
|
||||
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
|
||||
// Toggle sidebar on mobile
|
||||
const sidebarToggle = document.querySelector("#sidebarToggle");
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener("click", function () {
|
||||
document.querySelector(".sidebar").classList.toggle("show");
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize popovers
|
||||
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
// Auto-dismiss alerts after 5 seconds
|
||||
setTimeout(function () {
|
||||
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
||||
alerts.forEach(function (alert) {
|
||||
if (alert && bootstrap.Alert.getInstance(alert)) {
|
||||
bootstrap.Alert.getInstance(alert).close();
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Toggle sidebar on mobile
|
||||
const sidebarToggle = document.querySelector("#sidebarToggle");
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener("click", function () {
|
||||
document.querySelector(".sidebar").classList.toggle("show");
|
||||
});
|
||||
}
|
||||
// Form validation
|
||||
const forms = document.querySelectorAll(".needs-validation");
|
||||
forms.forEach(function (form) {
|
||||
form.addEventListener(
|
||||
"submit",
|
||||
function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add("was-validated");
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// Auto-dismiss alerts after 5 seconds
|
||||
setTimeout(function () {
|
||||
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
||||
alerts.forEach(function (alert) {
|
||||
if (alert && bootstrap.Alert.getInstance(alert)) {
|
||||
bootstrap.Alert.getInstance(alert).close();
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Form validation
|
||||
const forms = document.querySelectorAll(".needs-validation");
|
||||
forms.forEach(function (form) {
|
||||
form.addEventListener(
|
||||
"submit",
|
||||
function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add("was-validated");
|
||||
},
|
||||
false,
|
||||
);
|
||||
// Confirm dialogs
|
||||
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
||||
confirmButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Confirm dialogs
|
||||
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
||||
confirmButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
// Back button
|
||||
const backButtons = document.querySelectorAll(".btn-back");
|
||||
backButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
window.history.back();
|
||||
});
|
||||
});
|
||||
|
||||
// Back button
|
||||
const backButtons = document.querySelectorAll(".btn-back");
|
||||
backButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
window.history.back();
|
||||
});
|
||||
// File input customization
|
||||
const fileInputs = document.querySelectorAll(".custom-file-input");
|
||||
fileInputs.forEach(function (input) {
|
||||
input.addEventListener("change", function (e) {
|
||||
const fileName = this.files[0]?.name || "Choose file";
|
||||
const nextSibling = this.nextElementSibling;
|
||||
if (nextSibling) {
|
||||
nextSibling.innerText = fileName;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// File input customization
|
||||
const fileInputs = document.querySelectorAll(".custom-file-input");
|
||||
fileInputs.forEach(function (input) {
|
||||
input.addEventListener("change", function (e) {
|
||||
const fileName = this.files[0]?.name || "Choose file";
|
||||
const nextSibling = this.nextElementSibling;
|
||||
if (nextSibling) {
|
||||
nextSibling.innerText = fileName;
|
||||
}
|
||||
});
|
||||
// Search form submit on enter
|
||||
const searchInputs = document.querySelectorAll(".search-input");
|
||||
searchInputs.forEach(function (input) {
|
||||
input.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.closest("form").submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Search form submit on enter
|
||||
const searchInputs = document.querySelectorAll(".search-input");
|
||||
searchInputs.forEach(function (input) {
|
||||
input.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.closest("form").submit();
|
||||
}
|
||||
});
|
||||
// Toggle password visibility
|
||||
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
||||
togglePasswordButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
const target = document.querySelector(this.dataset.target);
|
||||
if (target) {
|
||||
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
||||
target.setAttribute("type", type);
|
||||
this.querySelector("i").classList.toggle("fa-eye");
|
||||
this.querySelector("i").classList.toggle("fa-eye-slash");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle password visibility
|
||||
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
||||
togglePasswordButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
const target = document.querySelector(this.dataset.target);
|
||||
if (target) {
|
||||
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
||||
target.setAttribute("type", type);
|
||||
this.querySelector("i").classList.toggle("fa-eye");
|
||||
this.querySelector("i").classList.toggle("fa-eye-slash");
|
||||
}
|
||||
});
|
||||
// Dropdown menu positioning
|
||||
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
||||
dropdowns.forEach(function (dropdown) {
|
||||
dropdown.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
// Dropdown menu positioning
|
||||
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
||||
dropdowns.forEach(function (dropdown) {
|
||||
dropdown.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
// Responsive table handling
|
||||
const tables = document.querySelectorAll(".table-responsive");
|
||||
if (window.innerWidth < 768) {
|
||||
tables.forEach(function (table) {
|
||||
table.classList.add("table-responsive-force");
|
||||
});
|
||||
}
|
||||
|
||||
// Responsive table handling
|
||||
const tables = document.querySelectorAll(".table-responsive");
|
||||
// Handle special links (printable views, exports)
|
||||
const printLinks = document.querySelectorAll(".print-link");
|
||||
printLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
window.print();
|
||||
});
|
||||
});
|
||||
|
||||
const exportLinks = document.querySelectorAll("[data-export]");
|
||||
exportLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
// Handle export functionality if needed
|
||||
console.log("Export requested:", this.dataset.export);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle sidebar collapse on small screens
|
||||
function handleSidebarOnResize() {
|
||||
if (window.innerWidth < 768) {
|
||||
tables.forEach(function (table) {
|
||||
table.classList.add("table-responsive-force");
|
||||
});
|
||||
document.querySelector(".sidebar")?.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", handleSidebarOnResize); // Theme toggling functionality
|
||||
function setTheme(theme, isUserPreference = false) {
|
||||
console.log("Setting theme to:", theme, "User preference:", isUserPreference);
|
||||
|
||||
// Update the HTML attribute that controls theme
|
||||
document.documentElement.setAttribute("data-bs-theme", theme);
|
||||
|
||||
// Save the theme preference to localStorage
|
||||
localStorage.setItem("theme", theme);
|
||||
|
||||
// If this was a user choice (from the toggle button), record that fact
|
||||
if (isUserPreference) {
|
||||
localStorage.setItem("userPreferredTheme", "true");
|
||||
}
|
||||
|
||||
// Handle special links (printable views, exports)
|
||||
const printLinks = document.querySelectorAll(".print-link");
|
||||
printLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
window.print();
|
||||
});
|
||||
});
|
||||
|
||||
const exportLinks = document.querySelectorAll("[data-export]");
|
||||
exportLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
// Handle export functionality if needed
|
||||
console.log("Export requested:", this.dataset.export);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle sidebar collapse on small screens
|
||||
function handleSidebarOnResize() {
|
||||
if (window.innerWidth < 768) {
|
||||
document.querySelector(".sidebar")?.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", handleSidebarOnResize); // Theme toggling functionality
|
||||
function setTheme(theme, isUserPreference = false) {
|
||||
console.log("Setting theme to:", theme, "User preference:", isUserPreference);
|
||||
|
||||
// Update the HTML attribute that controls theme
|
||||
document.documentElement.setAttribute("data-bs-theme", theme);
|
||||
|
||||
// Save the theme preference to localStorage
|
||||
localStorage.setItem("theme", theme);
|
||||
|
||||
// If this was a user choice (from the toggle button), record that fact
|
||||
if (isUserPreference) {
|
||||
localStorage.setItem("userPreferredTheme", "true");
|
||||
}
|
||||
|
||||
// Update toggle button icon
|
||||
const themeToggle = document.getElementById("theme-toggle");
|
||||
if (themeToggle) {
|
||||
const icon = themeToggle.querySelector("i");
|
||||
if (theme === "dark") {
|
||||
icon.classList.remove("fa-moon");
|
||||
icon.classList.add("fa-sun");
|
||||
themeToggle.setAttribute("title", "Switch to light mode");
|
||||
themeToggle.setAttribute("aria-label", "Switch to light mode");
|
||||
} else {
|
||||
icon.classList.remove("fa-sun");
|
||||
icon.classList.add("fa-moon");
|
||||
themeToggle.setAttribute("title", "Switch to dark mode");
|
||||
themeToggle.setAttribute("aria-label", "Switch to dark mode");
|
||||
}
|
||||
}
|
||||
|
||||
// If we're on a page with charts, refresh them to match the theme
|
||||
if (typeof window.refreshAllCharts === "function") {
|
||||
console.log("Calling refresh charts from theme toggle");
|
||||
// Add a small delay to ensure DOM updates have completed
|
||||
setTimeout(window.refreshAllCharts, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user has a system preference for dark mode
|
||||
function getSystemPreference() {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
// Initialize theme based on saved preference or system setting
|
||||
function initializeTheme() {
|
||||
// Check if the user has explicitly set a preference
|
||||
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
const systemTheme = getSystemPreference();
|
||||
|
||||
console.log("Theme initialization:", {
|
||||
hasUserPreference,
|
||||
savedTheme,
|
||||
systemTheme,
|
||||
});
|
||||
|
||||
// Use saved theme if it exists and was set by user
|
||||
// Otherwise, use system preference
|
||||
if (hasUserPreference && savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
} else {
|
||||
// No user preference, use system preference
|
||||
setTheme(systemTheme);
|
||||
// Clear any saved theme to ensure it uses system preference
|
||||
localStorage.removeItem("userPreferredTheme");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme on page load
|
||||
initializeTheme();
|
||||
|
||||
// Listen for system preference changes
|
||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
||||
// Only update theme based on system if user hasn't set a preference
|
||||
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
|
||||
console.log("System preference changed. Following system?", !hasUserPreference);
|
||||
|
||||
if (!hasUserPreference) {
|
||||
setTheme(e.matches ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
|
||||
// Theme toggle button functionality
|
||||
// Update toggle button icon
|
||||
const themeToggle = document.getElementById("theme-toggle");
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener("click", function () {
|
||||
const currentTheme = document.documentElement.getAttribute("data-bs-theme") || "light";
|
||||
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
||||
console.log("Manual theme toggle from", currentTheme, "to", newTheme);
|
||||
setTheme(newTheme, true); // true indicates this is a user preference
|
||||
});
|
||||
const icon = themeToggle.querySelector("i");
|
||||
if (theme === "dark") {
|
||||
icon.classList.remove("fa-moon");
|
||||
icon.classList.add("fa-sun");
|
||||
themeToggle.setAttribute("title", "Switch to light mode");
|
||||
themeToggle.setAttribute("aria-label", "Switch to light mode");
|
||||
} else {
|
||||
icon.classList.remove("fa-sun");
|
||||
icon.classList.add("fa-moon");
|
||||
themeToggle.setAttribute("title", "Switch to dark mode");
|
||||
themeToggle.setAttribute("aria-label", "Switch to dark mode");
|
||||
}
|
||||
}
|
||||
|
||||
// If we're on a page with charts, refresh them to match the theme
|
||||
if (typeof window.refreshAllCharts === "function") {
|
||||
console.log("Calling refresh charts from theme toggle");
|
||||
// Add a small delay to ensure DOM updates have completed
|
||||
setTimeout(window.refreshAllCharts, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user has a system preference for dark mode
|
||||
function getSystemPreference() {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
// Initialize theme based on saved preference or system setting
|
||||
function initializeTheme() {
|
||||
// Check if the user has explicitly set a preference
|
||||
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
const systemTheme = getSystemPreference();
|
||||
|
||||
console.log("Theme initialization:", {
|
||||
hasUserPreference,
|
||||
savedTheme,
|
||||
systemTheme,
|
||||
});
|
||||
|
||||
// Use saved theme if it exists and was set by user
|
||||
// Otherwise, use system preference
|
||||
if (hasUserPreference && savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
} else {
|
||||
// No user preference, use system preference
|
||||
setTheme(systemTheme);
|
||||
// Clear any saved theme to ensure it uses system preference
|
||||
localStorage.removeItem("userPreferredTheme");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize theme on page load
|
||||
initializeTheme();
|
||||
|
||||
// Listen for system preference changes
|
||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
||||
// Only update theme based on system if user hasn't set a preference
|
||||
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
|
||||
console.log("System preference changed. Following system?", !hasUserPreference);
|
||||
|
||||
if (!hasUserPreference) {
|
||||
setTheme(e.matches ? "dark" : "light");
|
||||
}
|
||||
});
|
||||
|
||||
// Theme toggle button functionality
|
||||
const themeToggle = document.getElementById("theme-toggle");
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener("click", function () {
|
||||
const currentTheme = document.documentElement.getAttribute("data-bs-theme") || "light";
|
||||
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
||||
console.log("Manual theme toggle from", currentTheme, "to", newTheme);
|
||||
setTheme(newTheme, true); // true indicates this is a user preference
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user