Add configuration and scripts for linting, testing, and dependency management

- Introduced .pre-commit-config.yaml for pre-commit hooks using uv-pre-commit.
- Created lint.sh script to run Ruff and Black for linting and formatting.
- Added test.sh script to execute tests with coverage reporting.
- Configured .uv file for uv settings including lockfile management and dependency resolution.
- Updated Makefile with targets for virtual environment setup, dependency installation, linting, testing, formatting, and database migrations.
- Established requirements.txt with main and development dependencies for the project.
This commit is contained in:
2025-05-17 20:18:21 +02:00
parent d916ae2247
commit 6b19cbcb51
48 changed files with 4733 additions and 3362 deletions

View File

@ -18,7 +18,7 @@ User = get_user_model()
class Command(BaseCommand):
help = "Create sample data for testing"
def handle(self, *args, **kwargs):
def handle(self, **_options):
self.stdout.write("Creating sample data...")
# Create admin user if it doesn't exist
@ -45,7 +45,7 @@ class Command(BaseCommand):
self.stdout.write(f"Company already exists: {company.name}")
# Create users for each company
for i, company in enumerate(companies):
for _i, company in enumerate(companies):
# Company admin
username = f"admin_{company.name.lower().replace(' ', '_')}"
if not User.objects.filter(username=username).exists():

View File

@ -1,5 +1,7 @@
# dashboard/utils.py
import contextlib
import numpy as np
import pandas as pd
from django.db import models
@ -25,17 +27,13 @@ def process_csv_file(data_source):
# Handle datetime fields
start_time = None
end_time = None
if "start_time" in row and pd.notna(row["start_time"]):
try:
with contextlib.suppress(Exception):
start_time = make_aware(pd.to_datetime(row["start_time"]))
except Exception:
pass
if "end_time" in row and pd.notna(row["end_time"]):
try:
with contextlib.suppress(Exception):
end_time = make_aware(pd.to_datetime(row["end_time"]))
except Exception:
pass
# Convert boolean fields

View File

@ -7,7 +7,7 @@ from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-your-secret-key-here"
SECRET_KEY = "django-insecure-your-secret-key-here" # nosec: B105
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

View File

@ -4,277 +4,311 @@
/* Dashboard grid layout */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
/* Slightly larger minmax for widgets */
gap: 1.5rem;
/* Increased gap */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
/* Slightly larger minmax for widgets */
gap: 1.5rem;
/* Increased gap */
}
/* Dashboard widget cards */
.dashboard-widget {
display: flex;
/* Allow flex for content alignment */
flex-direction: column;
/* Stack header, body, footer vertically */
height: 100%;
/* Ensure widgets fill grid cell height */
display: flex;
/* Allow flex for content alignment */
flex-direction: column;
/* Stack header, body, footer vertically */
height: 100%;
/* 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;
/* Slightly larger widget titles */
font-weight: 600;
font-size: 1.1rem;
/* 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;
/* 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;
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;
}
.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;
/* Allow card body to take available space */
padding: 1.25rem;
/* Consistent padding */
flex-grow: 1;
/* Allow card body to take available space */
padding: 1.25rem;
/* 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;
/* Adjusted min-height */
width: 100%;
/* Ensure it takes full width of card body */
flex: 1;
min-height: 250px;
/* Adjusted min-height */
width: 100%;
/* Ensure it takes full width of card body */
}
/* Stat widgets / Stat Cards */
.stat-card {
text-align: center;
padding: 1.5rem;
/* Generous padding */
text-align: center;
padding: 1.5rem;
/* Generous padding */
}
.stat-card .stat-icon {
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;
/* Light blue background for icon */
color: #007bff;
/* Primary color for icon */
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;
/* Light blue background for icon */
color: #007bff;
/* Primary color for icon */
}
.stat-card .stat-value {
font-size: 2.25rem;
/* Larger stat value */
font-weight: 700;
margin-bottom: 0.25rem;
/* Reduced margin */
line-height: 1.1;
color: #212529;
/* Darker color for value */
font-size: 2.25rem;
/* Larger stat value */
font-weight: 700;
margin-bottom: 0.25rem;
/* Reduced margin */
line-height: 1.1;
color: #212529;
/* Darker color for value */
}
.stat-card .stat-label {
font-size: 0.9rem;
/* Slightly larger label */
color: #6c757d;
margin-bottom: 0;
font-size: 0.9rem;
/* Slightly larger label */
color: #6c757d;
margin-bottom: 0;
}
/* Dashboard theme variations */
.dashboard-theme-light .card {
background-color: #ffffff;
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;
/* Increased gap */
margin-bottom: 1.5rem;
/* Increased margin */
display: flex;
align-items: center;
gap: 0.75rem;
/* Increased gap */
margin-bottom: 1.5rem;
/* Increased margin */
}
.time-period-selector .btn-group {
flex-wrap: wrap;
flex-wrap: wrap;
}
.time-period-selector .btn {
padding: 0.375rem 0.75rem;
/* Bootstrap-like padding */
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
/* 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%);
/* Lighter gradient */
background-size: 200% 100%;
animation: loading 1.8s infinite ease-in-out;
/* Smoother animation */
border-radius: 0.5rem;
/* Consistent with cards */
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;
/* Smoother animation */
border-radius: 0.5rem;
/* 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;
/* Increased padding */
text-align: center;
color: #6c757d;
background-color: #f8f9fa;
/* Light background for empty state */
border-radius: 0.5rem;
border: 1px dashed #ced4da;
/* Dashed border */
padding: 2.5rem;
/* Increased padding */
text-align: center;
color: #6c757d;
background-color: #f8f9fa;
/* Light background for empty state */
border-radius: 0.5rem;
border: 1px dashed #ced4da;
/* Dashed border */
}
.empty-state .empty-state-icon {
font-size: 3.5rem;
/* Larger icon */
margin-bottom: 1.5rem;
opacity: 0.4;
font-size: 3.5rem;
/* Larger icon */
margin-bottom: 1.5rem;
opacity: 0.4;
}
.empty-state .empty-state-message {
font-size: 1.2rem;
/* Slightly larger message */
margin-bottom: 1.5rem;
font-weight: 500;
font-size: 1.2rem;
/* Slightly larger message */
margin-bottom: 1.5rem;
font-weight: 500;
}
.empty-state .btn {
margin-top: 1rem;
margin-top: 1rem;
}
/* Responsive adjustments */
@media (max-width: 767.98px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
@media (width <=767.98px) {
.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;
}
}
/* --- Stat Boxes 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; /* Push content to bottom */
align-items: flex-start;
box-sizing: border-box;
/* Remove min-height/height for natural stretch */
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;
/* Remove min-height/height for natural stretch */
}

View File

@ -4,325 +4,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;
/* Lighter, cleaner background */
color: #333;
/* Darker text for better contrast */
line-height: 1.6;
display: flex;
/* Added for sticky footer */
flex-direction: column;
/* Added for sticky footer */
min-height: 100vh;
/* Ensures body takes at least full viewport height */
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;
/* Darker text for better contrast */
line-height: 1.6;
display: flex;
/* Added for sticky footer */
flex-direction: column;
/* Added for sticky footer */
min-height: 100vh;
/* Ensures body takes at least full viewport height */
}
/* Navbar adjustments (if needed, Bootstrap usually handles this well) */
.navbar {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
/* Subtle shadow for depth */
box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
/* 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;
/* Lighter border */
border-radius: 0.5rem;
/* Slightly more rounded corners */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
/* Softer, more modern shadow */
transition:
transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out;
margin-bottom: 1.5rem;
/* Consistent margin */
border: 1px solid #e0e5e9;
/* Lighter border */
border-radius: 0.5rem;
/* 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;
/* Consistent margin */
}
.card-hover:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-3px);
box-shadow: 0 6px 16px rgb(0 0 0 / 10%);
}
.card-header {
background-color: #ffffff;
/* Clean white header */
border-bottom: 1px solid #e0e5e9;
font-weight: 500;
/* Slightly bolder header text */
padding: 0.75rem 1.25rem;
background-color: #fff;
/* Clean white header */
border-bottom: 1px solid #e0e5e9;
font-weight: 500;
/* Slightly bolder header text */
padding: 0.75rem 1.25rem;
}
.card-title {
font-size: 1.15rem;
/* Adjusted card title size */
font-weight: 600;
font-size: 1.15rem;
/* Adjusted card title size */
font-weight: 600;
}
/* Sidebar enhancements */
.sidebar {
background-color: #ffffff;
/* White sidebar for a cleaner look */
border-right: 1px solid #e0e5e9;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.03);
transition: all 0.3s;
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;
}
.sidebar-sticky {
padding-top: 1rem;
padding-top: 1rem;
}
.sidebar .nav-link {
color: #4a5568;
/* Softer link color */
padding: 0.65rem 1.25rem;
/* Adjusted padding */
border-radius: 0.375rem;
/* Bootstrap-like rounded corners for links */
margin: 0.1rem 0.5rem;
/* Margin around links */
font-weight: 500;
color: #4a5568;
/* Softer link color */
padding: 0.65rem 1.25rem;
/* Adjusted padding */
border-radius: 0.375rem;
/* Bootstrap-like rounded corners for links */
margin: 0.1rem 0.5rem;
/* Margin around links */
font-weight: 500;
}
.sidebar .nav-link:hover {
color: #007bff;
/* Primary color on hover */
background-color: #e9f2ff;
/* Light blue background on hover */
color: #007bff;
/* Primary color on hover */
background-color: #e9f2ff;
/* Light blue background on hover */
}
.sidebar .nav-link.active {
color: #007bff;
background-color: #d6e4ff;
/* Slightly darker blue for active */
font-weight: 600;
color: #007bff;
background-color: #d6e4ff;
/* Slightly darker blue for active */
font-weight: 600;
}
.sidebar .nav-link i.me-2 {
width: 20px;
/* Ensure icons align well */
text-align: center;
margin-right: 0.75rem !important;
/* Consistent icon spacing */
width: 20px;
/* Ensure icons align well */
text-align: center;
margin-right: 0.75rem !important;
/* Consistent icon spacing */
}
.sidebar .nav-header {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #718096;
/* Softer header color */
padding: 0.5rem 1.25rem;
margin-top: 1rem;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #718096;
/* 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: rgba(255, 255, 255, 0.7);
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;
/* Bolder table headers */
color: #4a5568;
background-color: #f8f9fc;
/* Light background for headers */
font-weight: 600;
/* Bolder table headers */
color: #4a5568;
background-color: #f8f9fc;
/* Light background for headers */
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0, 0, 0, 0.02);
/* Very subtle striping */
background-color: rgb(0 0 0 / 2%);
/* Very subtle striping */
}
.table-hover tbody tr:hover {
background-color: #e9f2ff;
/* Consistent hover with sidebar */
background-color: #e9f2ff;
/* Consistent hover with sidebar */
}
/* Form improvements */
.form-control,
.form-select {
border-color: #ced4da;
border-radius: 0.375rem;
/* Consistent border radius */
padding: 0.5rem 0.75rem;
/* Adjusted padding */
border-color: #ced4da;
border-radius: 0.375rem;
/* Consistent border radius */
padding: 0.5rem 0.75rem;
/* Adjusted padding */
}
.form-control:focus,
.form-select:focus {
border-color: #86b7fe;
/* Bootstrap focus color */
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
/* Bootstrap focus shadow */
border-color: #86b7fe;
/* Bootstrap focus color */
box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%);
/* Bootstrap focus shadow */
}
/* Button styling */
.btn {
border-radius: 0.375rem;
/* 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;
border-radius: 0.375rem;
/* 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;
}
.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: #ffffff;
/* White footer */
border-top: 1px solid #e0e5e9;
padding: 1.5rem 0;
color: #6c757d;
font-size: 0.9rem;
margin-top: auto;
/* Added for sticky footer */
background-color: #fff;
/* White footer */
border-top: 1px solid #e0e5e9;
padding: 1.5rem 0;
color: #6c757d;
font-size: 0.9rem;
margin-top: auto;
/* Added for sticky footer */
}
/* Responsive adjustments */
@media (max-width: 767.98px) {
.main-content {
margin-left: 0;
}
@media (width <=767.98px) {
.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;
}
}

View File

@ -6,268 +6,269 @@
*/
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
],
};
// 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);
}
// 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 loading indicator element
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
// Get the loading indicator element
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
// Get the main content container
const mainContent = document.querySelector(config.mainContentSelector);
if (!mainContent) {
console.warn("Main content container not found. AJAX navigation disabled.");
return;
}
// 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;
}
// 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 show the loading indicator
function showLoading() {
loadingIndicator.style.display = "block";
}
// Function to hide the loading indicator
function hideLoading() {
loadingIndicator.style.display = "none";
}
// 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 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");
// 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 all attributes
Array.from(script.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value);
});
// Copy inline script content
newScript.textContent = script.textContent;
// Copy inline script content
newScript.textContent = script.textContent;
// Replace old script with new one
script.parentNode.replaceChild(newScript, script);
}
}
// Replace old script with new one
script.parentNode.replaceChild(newScript, script);
}
}
// Function to handle form submissions
function handleFormSubmission(form, e) {
e.preventDefault();
// Function to handle form submissions
function handleFormSubmission(form, e) {
e.preventDefault();
// Show loading indicator
showLoading();
// Show loading indicator
showLoading();
// Get form data
const formData = new FormData(form);
const method = form.method.toLowerCase();
const url = form.action || window.location.href;
// 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",
},
};
// 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;
// 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;
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();
// 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();
});
}
}
// 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();
}
// 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 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);
});
}
}
// 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";
}
});
// 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";
}
});
}
// 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();
// 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);
}
});
}
// 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);
}
});
}
});

View File

@ -6,101 +6,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);
}
// 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");
// 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);
});
}
// 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();
// 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);
}
});
}
// 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);
}
});
}
});

View File

@ -7,272 +7,272 @@
*/
document.addEventListener("DOMContentLoaded", function () {
// 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,
});
}
});
}
// 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,
});
}
});
}
// Handle window resize
window.addEventListener("resize", function () {
if (window.Plotly) {
resizeCharts();
}
});
// Handle window resize
window.addEventListener("resize", function () {
if (window.Plotly) {
resizeCharts();
}
});
// 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");
// 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");
// Fetch updated data via AJAX
if (dashboardId) {
fetchDashboardData(dashboardId, timeRange);
e.preventDefault();
}
});
});
}
// Fetch updated data via AJAX
if (dashboardId) {
fetchDashboardData(dashboardId, timeRange);
e.preventDefault();
}
});
});
}
// 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);
// 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);
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);
// 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();
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 = `
// 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 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 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 total cost
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
if (totalCostElement) {
totalCostElement.textContent = "€" + data.total_cost;
}
}
// 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;
}
// 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);
// 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,
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 40 },
xaxis: {
title: "Date",
},
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>';
}
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,
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 40 },
xaxis: {
title: "Date",
},
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)";
});
// 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",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
}
);
}
Plotly.react(
"sentiment-chart",
[
{
values: sentimentValues,
labels: sentimentLabels,
type: "pie",
marker: {
colors: sentimentColors,
},
hole: 0.4,
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
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);
// 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)",
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: {
title: "Number of Sessions",
},
}
);
}
Plotly.react(
"country-chart",
[
{
x: countryValues,
y: countryLabels,
type: "bar",
orientation: "h",
marker: {
color: "rgb(54, 162, 235)",
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 100 },
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);
// 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",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
}
);
}
}
Plotly.react(
"category-chart",
[
{
labels: categoryLabels,
values: categoryValues,
type: "pie",
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
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");
// 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();
}
});
});
// Fetch updated data via AJAX
if (dashboardId) {
fetchDashboardData(dashboardId);
e.preventDefault();
}
});
});
});

View File

@ -6,147 +6,147 @@
*/
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);
});
// 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");
});
}
// Toggle sidebar on mobile
const sidebarToggle = document.querySelector("#sidebarToggle");
if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () {
document.querySelector(".sidebar").classList.toggle("show");
});
}
// 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);
// 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
);
});
// 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");
if (window.innerWidth < 768) {
tables.forEach(function (table) {
table.classList.add("table-responsive-force");
});
}
// 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();
});
});
// 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);
});
});
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");
}
}
// Handle sidebar collapse on small screens
function handleSidebarOnResize() {
if (window.innerWidth < 768) {
document.querySelector(".sidebar")?.classList.remove("show");
}
}
window.addEventListener("resize", handleSidebarOnResize);
window.addEventListener("resize", handleSidebarOnResize);
});

View File

@ -5,25 +5,27 @@
{% block title %}Login | Chat Analytics{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title mb-0">Login</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">Don't have an account? <a href="{% url 'register' %}">Register</a></p>
</div>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title mb-0">Login</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">
Don't have an account? <a href="{% url 'register' %}">Register</a>
</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -5,33 +5,33 @@
{% block title %}Change Password | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Change Password</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Profile
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Change Password</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Profile
</a>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Change Your Password</h5>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Change Password</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Change Your Password</h5>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Change Password</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -4,38 +4,41 @@
{% block title %}Password Changed | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Password Changed</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Profile
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Password Changed</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'profile' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Profile
</a>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">Password Changed Successfully</h5>
</div>
<div class="card-body text-center">
<div class="mb-4">
<i class="fas fa-check-circle fa-4x text-success mb-3"></i>
<h4>Your password has been changed successfully!</h4>
<p>Your new password is now active. You can use it the next time you log in.</p>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0">Password Changed Successfully</h5>
</div>
<div class="card-body text-center">
<div class="mb-4">
<i class="fas fa-check-circle fa-4x text-success mb-3"></i>
<h4>Your password has been changed successfully!</h4>
<p>
Your new password is now active. You can use it the next time you log
in.
</p>
</div>
<div class="mt-4">
<a href="{% url 'profile' %}" class="btn btn-primary">Return to Profile</a>
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary ms-2"
>Go to Dashboard</a
>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4">
<a href="{% url 'profile' %}" class="btn btn-primary">Return to Profile</a>
<a href="{% url 'dashboard' %}" class="btn btn-outline-secondary ms-2"
>Go to Dashboard</a
>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -4,186 +4,210 @@
{% block title %}My Profile | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">My Profile</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">My Profile</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Account Information</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-4 fw-bold">Username:</div>
<div class="col-md-8">{{ user.username }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Email:</div>
<div class="col-md-8">{{ user.email }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Company:</div>
<div class="col-md-8">
{% if user.company %}
{{ user.company.name }}
{% else %}
<span class="text-muted">Not assigned to a company</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Role:</div>
<div class="col-md-8">
{% if user.is_staff %}
<span class="badge bg-danger">Admin</span>
{% elif user.is_company_admin %}
<span class="badge bg-primary">Company Admin</span>
{% else %}
<span class="badge bg-secondary">User</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Last Login:</div>
<div class="col-md-8">{{ user.last_login|date:"F d, Y H:i" }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Date Joined:</div>
<div class="col-md-8">{{ user.date_joined|date:"F d, Y H:i" }}</div>
</div>
</div>
<div class="card-footer">
<a href="{% url 'password_change' %}" class="btn btn-primary">Change Password</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Account Information</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-4 fw-bold">Username:</div>
<div class="col-md-8">{{ user.username }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Email:</div>
<div class="col-md-8">{{ user.email }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Company:</div>
<div class="col-md-8">
{% if user.company %}
{{ user.company.name }}
{% else %}
<span class="text-muted">Not assigned to a company</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Role:</div>
<div class="col-md-8">
{% if user.is_staff %}
<span class="badge bg-danger">Admin</span>
{% elif user.is_company_admin %}
<span class="badge bg-primary">Company Admin</span>
{% else %}
<span class="badge bg-secondary">User</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Last Login:</div>
<div class="col-md-8">{{ user.last_login|date:"F d, Y H:i" }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Date Joined:</div>
<div class="col-md-8">{{ user.date_joined|date:"F d, Y H:i" }}</div>
</div>
</div>
<div class="card-footer">
<a href="{% url 'password_change' %}" class="btn btn-primary"
>Change Password</a
>
</div>
</div>
</div>
{% if user.company %}
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Company Information</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-4 fw-bold">Company Name:</div>
<div class="col-md-8">{{ user.company.name }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Description:</div>
<div class="col-md-8">
{{ user.company.description|default:"No description available." }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Created:</div>
<div class="col-md-8">{{ user.company.created_at|date:"F d, Y" }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Total Employees:</div>
<div class="col-md-8">{{ user.company.employees.count }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Data Sources:</div>
<div class="col-md-8">{{ user.company.data_sources.count }}</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if user.company %}
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Company Information</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-4 fw-bold">Company Name:</div>
<div class="col-md-8">{{ user.company.name }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Description:</div>
<div class="col-md-8">
{{ user.company.description|default:"No description available." }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Created:</div>
<div class="col-md-8">{{ user.company.created_at|date:"F d, Y" }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Total Employees:</div>
<div class="col-md-8">{{ user.company.employees.count }}</div>
</div>
<div class="row mb-3">
<div class="col-md-4 fw-bold">Data Sources:</div>
<div class="col-md-8">{{ user.company.data_sources.count }}</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if user.is_company_admin or user.is_staff %}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Admin Actions</h5>
</div>
<div class="card-body">
<div class="row">
{% if user.is_staff %}
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Manage Users</h5>
<p class="card-text">Manage users and assign them to companies.</p>
<a
href="{% url 'admin:accounts_customuser_changelist' %}"
class="btn btn-primary"
>Manage Users</a
>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Manage Companies</h5>
<p class="card-text">Create and edit companies in the system.</p>
<a
href="{% url 'admin:accounts_company_changelist' %}"
class="btn btn-primary"
>Manage Companies</a
>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Admin Dashboard</h5>
<p class="card-text">Go to the full admin dashboard.</p>
<a href="{% url 'admin:index' %}" class="btn btn-primary">Admin Dashboard</a>
</div>
</div>
</div>
{% elif user.is_company_admin %}
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Manage Dashboards</h5>
<p class="card-text">Create and edit dashboards for your company.</p>
<a href="{% url 'create_dashboard' %}" class="btn btn-primary"
>Manage Dashboards</a
>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Upload Data</h5>
<p class="card-text">Upload and manage data sources for analysis.</p>
<a href="{% url 'upload_data' %}" class="btn btn-primary">Upload Data</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Search Sessions</h5>
<p class="card-text">Search and analyze chat sessions.</p>
<a href="{% url 'search_chat_sessions' %}" class="btn btn-primary"
>Search Sessions</a
>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if user.is_company_admin or user.is_staff %}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Admin Actions</h5>
</div>
<div class="card-body">
<div class="row">
{% if user.is_staff %}
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Manage Users</h5>
<p class="card-text">
Manage users and assign them to companies.
</p>
<a
href="{% url 'admin:accounts_customuser_changelist' %}"
class="btn btn-primary"
>Manage Users</a
>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Manage Companies</h5>
<p class="card-text">
Create and edit companies in the system.
</p>
<a
href="{% url 'admin:accounts_company_changelist' %}"
class="btn btn-primary"
>Manage Companies</a
>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Admin Dashboard</h5>
<p class="card-text">Go to the full admin dashboard.</p>
<a
href="{% url 'admin:index' %}"
class="btn btn-primary"
>Admin Dashboard</a
>
</div>
</div>
</div>
{% elif user.is_company_admin %}
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Manage Dashboards</h5>
<p class="card-text">
Create and edit dashboards for your company.
</p>
<a
href="{% url 'create_dashboard' %}"
class="btn btn-primary"
>Manage Dashboards</a
>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Upload Data</h5>
<p class="card-text">
Upload and manage data sources for analysis.
</p>
<a
href="{% url 'upload_data' %}"
class="btn btn-primary"
>Upload Data</a
>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body text-center">
<h5 class="card-title">Search Sessions</h5>
<p class="card-text">
Search and analyze chat sessions.
</p>
<a
href="{% url 'search_chat_sessions' %}"
class="btn btn-primary"
>Search Sessions</a
>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -5,25 +5,27 @@
{% block title %}Register | Chat Analytics{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title mb-0">Register</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Register</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">Already have an account? <a href="{% url 'login' %}">Login</a></p>
</div>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card mt-4">
<div class="card-header">
<h4 class="card-title mb-0">Register</h4>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Register</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">
Already have an account? <a href="{% url 'login' %}">Login</a>
</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -2,314 +2,339 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Chat Analytics Dashboard{% endblock %}</title>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Chat Analytics Dashboard{% endblock %}</title>
<!-- Bootstrap CSS -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<!-- Bootstrap CSS -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<!-- Font Awesome -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@latest/css/all.min.css"
/>
<!-- Font Awesome -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@latest/css/all.min.css"
/>
<!-- Plotly.js -->
<script
src="https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js"
charset="utf-8"
></script>
<!-- Plotly.js -->
<script
src="https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js"
charset="utf-8"
></script>
<!-- Custom CSS -->
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
<link rel="stylesheet" href="{% static 'css/dashboard.css' %}" />
<!-- Custom CSS -->
<link rel="stylesheet" href="{% static 'css/style.css' %}" />
<link rel="stylesheet" href="{% static 'css/dashboard.css' %}" />
{% block extra_css %}{% endblock %}
</head>
{% block extra_css %}{% endblock %}
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-md navbar-dark bg-dark absolute-top">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'dashboard' %}">Chat Analytics</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarCollapse"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}"
>Dashboard</a
>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}"
>Upload Data</a
>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}"
>Search</a
>
</li>
</ul>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-md navbar-dark bg-dark absolute-top">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'dashboard' %}">Chat Analytics</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarCollapse"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}"
>Dashboard</a
>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}"
>Upload Data</a
>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}"
>Search</a
>
</li>
</ul>
<div class="d-flex">
{% if user.is_authenticated %}
<div class="dropdown">
<button
class="btn btn-outline-light dropdown-toggle"
type="button"
id="userDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{% if user.company %}
<span class="badge bg-info me-1">{{ user.company.name }}</span>
{% endif %}
{{ user.username }}
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li>
<a class="dropdown-item ajax-nav-link" href="{% url 'profile' %}">Profile</a>
</li>
{% if user.is_staff %}
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
{% endif %}
<li>
<hr class="dropdown-divider" />
</li>
<li><a class="dropdown-item" href="{% url 'logout' %}">Logout</a></li>
</ul>
</div>
{% else %}
<a href="{% url 'login' %}" class="btn btn-outline-light me-2">Login</a>
<a href="{% url 'register' %}" class="btn btn-light">Register</a>
{% endif %}
</div>
</div>
</div>
</nav>
<div class="d-flex">
{% if user.is_authenticated %}
<div class="dropdown">
<button
class="btn btn-outline-light dropdown-toggle"
type="button"
id="userDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
{% if user.company %}
<span class="badge bg-info me-1"
>{{ user.company.name }}</span
>
{% endif %}
{{ user.username }}
</button>
<ul
class="dropdown-menu dropdown-menu-end"
aria-labelledby="userDropdown"
>
<li>
<a
class="dropdown-item ajax-nav-link"
href="{% url 'profile' %}"
>Profile</a
>
</li>
{% if user.is_staff %}
<li>
<a class="dropdown-item" href="{% url 'admin:index' %}"
>Admin</a
>
</li>
{% endif %}
<li>
<hr class="dropdown-divider" />
</li>
<li>
<a class="dropdown-item" href="{% url 'logout' %}"
>Logout</a
>
</li>
</ul>
</div>
{% else %}
<a href="{% url 'login' %}" class="btn btn-outline-light me-2">Login</a>
<a href="{% url 'register' %}" class="btn btn-light">Register</a>
{% endif %}
</div>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<nav
id="sidebarMenu"
class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse sticky-top h-100 p-0"
>
<div class="sidebar-sticky pt-3">
{% block sidebar %}
<ul class="nav flex-column">
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}"
>
<i class="fas fa-tachometer-alt me-2"></i>
Dashboard
</a>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}"
>
<i class="fas fa-upload me-2"></i>
Upload Data
</a>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}"
>
<i class="fas fa-search me-2"></i>
Search
</a>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}"
href="{% url 'data_view' %}"
>
<i class="fas fa-table me-2"></i>
Data View
</a>
</li>
{% if user.is_authenticated and user.company %}
{% if dashboards %}
<li class="nav-header"><strong>Dashboards</strong></li>
{% for dashboard in dashboards %}
<li class="nav-item">
<a
class="nav-link {% if selected_dashboard.id == dashboard.id %}active{% endif %}"
href="{% url 'dashboard' %}?dashboard_id={{ dashboard.id }}"
>
<i class="fas fa-chart-line me-2"></i>
{{ dashboard.name }}
</a>
</li>
{% endfor %}
<li class="nav-item">
<a class="nav-link" href="{% url 'create_dashboard' %}">
<i class="fas fa-plus-circle me-2"></i>
New Dashboard
</a>
</li>
{% endif %}
{% if data_sources %}
<li class="nav-header"><strong>Data Sources</strong></li>
{% for data_source in data_sources %}
<li class="nav-item">
<a class="nav-link" href="{% url 'data_source_detail' data_source.id %}">
<i class="fas fa-database me-2"></i>
{{ data_source.name }}
</a>
</li>
{% endfor %}
{% endif %}
{% endif %}
</ul>
{% endblock %}
</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<nav
id="sidebarMenu"
class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse sticky-top h-100 p-0"
>
<div class="sidebar-sticky pt-3">
{% block sidebar %}
<ul class="nav flex-column">
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}"
href="{% url 'dashboard' %}"
>
<i class="fas fa-tachometer-alt me-2"></i>
Dashboard
</a>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'upload_data' %}active{% endif %}"
href="{% url 'upload_data' %}"
>
<i class="fas fa-upload me-2"></i>
Upload Data
</a>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'search_chat_sessions' %}active{% endif %}"
href="{% url 'search_chat_sessions' %}"
>
<i class="fas fa-search me-2"></i>
Search
</a>
</li>
<li class="nav-item">
<a
class="nav-link ajax-nav-link {% if request.resolver_match.url_name == 'data_view' %}active{% endif %}"
href="{% url 'data_view' %}"
>
<i class="fas fa-table me-2"></i>
Data View
</a>
</li>
{% if user.is_authenticated and user.company %}
{% if dashboards %}
<li class="nav-header"><strong>Dashboards</strong></li>
{% for dashboard in dashboards %}
<li class="nav-item">
<a
class="nav-link {% if selected_dashboard.id == dashboard.id %}active{% endif %}"
href="{% url 'dashboard' %}?dashboard_id={{ dashboard.id }}"
>
<i class="fas fa-chart-line me-2"></i>
{{ dashboard.name }}
</a>
</li>
{% endfor %}
<li class="nav-item">
<a class="nav-link" href="{% url 'create_dashboard' %}">
<i class="fas fa-plus-circle me-2"></i>
New Dashboard
</a>
</li>
{% endif %}
{% if data_sources %}
<li class="nav-header"><strong>Data Sources</strong></li>
{% for data_source in data_sources %}
<li class="nav-item">
<a
class="nav-link"
href="{% url 'data_source_detail' data_source.id %}"
>
<i class="fas fa-database me-2"></i>
{{ data_source.name }}
</a>
</li>
{% endfor %}
{% endif %}
{% endif %}
</ul>
{% endblock %}
</div>
</nav>
<!-- Main content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
{# {% if messages %} #}
{# <div class="messages mt-3"> #}
{# {% for message in messages %} #}
{# <div class="alert {% if message.tags == 'error' %}alert-danger{% elif message.tags == 'success' %}alert-success{% elif message.tags == 'warning' %}alert-warning{% else %}alert-info{% endif %} alert-dismissible fade show" role="alert"> #}
{# {{ message }} #}
{# <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> #}
{# </div> #}
{# {% endfor %} #}
{# </div> #}
{# {% endif %} #}
<!-- Main content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
{# {% if messages %} #}
{# <div class="messages mt-3"> #}
{# {% for message in messages %} #}
{# <div class="alert {% if message.tags == 'error' %}alert-danger{% elif message.tags == 'success' %}alert-success{% elif message.tags == 'warning' %}alert-warning{% else %}alert-info{% endif %} alert-dismissible fade show" role="alert"> #}
{# {{ message }} #}
{# <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> #}
{# </div> #}
{# {% endfor %} #}
{# </div> #}
{# {% endif %} #}
<div id="main-content">
{% block content %}
{% endblock %}
</div>
</main>
</div>
</div>
<div id="main-content">
{% block content %}
{% endblock %}
</div>
</main>
</div>
</div>
<footer>
<div class="container">
<p>&copy; {% now "Y" %} KJANAT All rights reserved. | Chat Analytics Dashboard.</p>
</div>
</footer>
<footer>
<div class="container">
<p>&copy; {% now "Y" %} KJANAT All rights reserved. | Chat Analytics Dashboard.</p>
</div>
</footer>
<!-- Bootstrap JS -->
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"
></script>
<!-- jQuery (for Ajax) -->
<script
src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"
crossorigin="anonymous"
></script>
<!-- Bootstrap JS -->
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"
></script>
<!-- jQuery (for Ajax) -->
<script
src="https://cdn.jsdelivr.net/npm/jquery@latest/dist/jquery.min.js"
crossorigin="anonymous"
></script>
<!-- Custom JavaScript -->
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/ajax-pagination.js' %}"></script>
<script src="{% static 'js/ajax-navigation.js' %}"></script>
<!-- Custom JavaScript -->
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/ajax-pagination.js' %}"></script>
<script src="{% static 'js/ajax-navigation.js' %}"></script>
<!-- Enable AJAX Navigation -->
<script>
// Enable AJAX navigation for the entire application
var ENABLE_AJAX_NAVIGATION = true;
</script>
<!-- Enable AJAX Navigation -->
<script>
// Enable AJAX navigation for the entire application
var ENABLE_AJAX_NAVIGATION = true;
</script>
<!-- Check if Plotly loaded successfully -->
<script>
if (typeof Plotly === "undefined") {
console.error("Plotly library failed to load. Will attempt to load fallback.");
// Try to load Plotly from alternative source
const script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js";
script.async = true;
script.crossOrigin = "anonymous";
document.head.appendChild(script);
}
</script>
<!-- Check if Plotly loaded successfully -->
<script>
if (typeof Plotly === "undefined") {
console.error("Plotly library failed to load. Will attempt to load fallback.");
// Try to load Plotly from alternative source
const script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/npm/plotly.js@latest/dist/plotly.min.js";
script.async = true;
script.crossOrigin = "anonymous";
document.head.appendChild(script);
}
</script>
{% block extra_js %}
{{ block.super }}
{% if messages %}
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100;">
<!-- Toasts will be appended here -->
</div>
{% block extra_js %}
{{ block.super }}
{% if messages %}
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1100;">
<!-- Toasts will be appended here -->
</div>
{% for message in messages %}
<!-- Pre-render message data that will be used by JavaScript -->
<script type="application/json" id="message-data-{{ forloop.counter }}">
{
"message": "{{ message|escapejs }}",
"tags": "{{ message.tags|default:'' }}"
}
</script>
{% endfor %}
{% for message in messages %}
<!-- Pre-render message data that will be used by JavaScript -->
<script type="application/json" id="message-data-{{ forloop.counter }}">
{
"message": "{{ message|escapejs }}",
"tags": "{{ message.tags|default:'' }}"
}
</script>
{% endfor %}
<script>
document.addEventListener("DOMContentLoaded", function () {
const toastContainer = document.querySelector(".toast-container");
if (!toastContainer) return;
<script>
document.addEventListener("DOMContentLoaded", function () {
const toastContainer = document.querySelector(".toast-container");
if (!toastContainer) return;
// Find all message data elements
const messageDataElements = document.querySelectorAll('script[id^="message-data-"]');
messageDataElements.forEach(function (dataElement) {
try {
const messageData = JSON.parse(dataElement.textContent);
createToast(messageData.message, messageData.tags);
} catch (e) {
console.error("Error parsing message data:", e);
}
});
// Find all message data elements
const messageDataElements = document.querySelectorAll(
'script[id^="message-data-"]',
);
messageDataElements.forEach(function (dataElement) {
try {
const messageData = JSON.parse(dataElement.textContent);
createToast(messageData.message, messageData.tags);
} catch (e) {
console.error("Error parsing message data:", e);
}
});
function createToast(messageText, messageTags) {
let toastClass = "";
let autohide = true;
let delay = 5000;
function createToast(messageText, messageTags) {
let toastClass = "";
let autohide = true;
let delay = 5000;
if (messageTags.includes("debug")) {
toastClass = "bg-secondary text-white";
} else if (messageTags.includes("info")) {
toastClass = "bg-info text-dark";
} else if (messageTags.includes("success")) {
toastClass = "bg-success text-white";
} else if (messageTags.includes("warning")) {
toastClass = "bg-warning text-dark";
autohide = false;
} else if (messageTags.includes("error")) {
toastClass = "bg-danger text-white";
autohide = false;
} else {
toastClass = "bg-light text-dark";
}
if (messageTags.includes("debug")) {
toastClass = "bg-secondary text-white";
} else if (messageTags.includes("info")) {
toastClass = "bg-info text-dark";
} else if (messageTags.includes("success")) {
toastClass = "bg-success text-white";
} else if (messageTags.includes("warning")) {
toastClass = "bg-warning text-dark";
autohide = false;
} else if (messageTags.includes("error")) {
toastClass = "bg-danger text-white";
autohide = false;
} else {
toastClass = "bg-light text-dark";
}
const toastId =
"toast-" + Date.now() + "-" + Math.random().toString(36).substring(2, 11);
const toastHtml = `
const toastId =
"toast-" +
Date.now() +
"-" +
Math.random().toString(36).substring(2, 11);
const toastHtml = `
<div id="${toastId}" class="toast ${toastClass}" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header">
<strong class="me-auto">Notification</strong>
@ -321,25 +346,25 @@
</div>
</div>`;
toastContainer.insertAdjacentHTML("beforeend", toastHtml);
toastContainer.insertAdjacentHTML("beforeend", toastHtml);
const toastElement = document.getElementById(toastId);
if (toastElement) {
const toast = new bootstrap.Toast(toastElement, {
autohide: autohide,
delay: autohide ? delay : undefined,
});
const toastElement = document.getElementById(toastId);
if (toastElement) {
const toast = new bootstrap.Toast(toastElement, {
autohide: autohide,
delay: autohide ? delay : undefined,
});
toastElement.addEventListener("hidden.bs.toast", function () {
toastElement.remove();
});
toastElement.addEventListener("hidden.bs.toast", function () {
toastElement.remove();
});
toast.show();
}
}
});
</script>
{% endif %}
{% endblock %}
</body>
toast.show();
}
}
});
</script>
{% endif %}
{% endblock %}
</body>
</html>

View File

@ -3,128 +3,143 @@
{% block title %}Chat Session {{ session.session_id }} | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Chat Session: {{ session.session_id }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a
href="{% url 'data_source_detail' session.data_source.id %}"
class="btn btn-sm btn-outline-secondary"
>
<i class="fas fa-arrow-left"></i> Back to Data Source
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Chat Session: {{ session.session_id }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a
href="{% url 'data_source_detail' session.data_source.id %}"
class="btn btn-sm btn-outline-secondary"
>
<i class="fas fa-arrow-left"></i> Back to Data Source
</a>
</div>
</div>
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Session Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Session ID:</strong> {{ session.session_id }}</p>
<p><strong>Start Time:</strong> {{ session.start_time|date:"F d, Y H:i" }}</p>
<p><strong>End Time:</strong> {{ session.end_time|date:"F d, Y H:i" }}</p>
<p><strong>IP Address:</strong> {{ session.ip_address|default:"N/A" }}</p>
<p><strong>Country:</strong> {{ session.country|default:"N/A" }}</p>
<p><strong>Language:</strong> {{ session.language|default:"N/A" }}</p>
</div>
<div class="col-md-6">
<p><strong>Messages Sent:</strong> {{ session.messages_sent }}</p>
<p>
<strong>Average Response Time:</strong>
{{ session.avg_response_time|floatformat:2 }}s
</p>
<p><strong>Tokens:</strong> {{ session.tokens }}</p>
<p><strong>Token Cost:</strong> €{{ session.tokens_eur|floatformat:2 }}</p>
<p><strong>Category:</strong> {{ session.category|default:"N/A" }}</p>
<p>
<strong>Sentiment:</strong>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</p>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<p class="mb-2"><strong>Initial Message:</strong></p>
<div class="card bg-light">
<div class="card-body">
<p class="mb-0">{{ session.initial_msg|default:"N/A" }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Additional Info</h5>
</div>
<div class="card-body">
<p>
<strong>Escalated:</strong> {% if session.escalated %}
<span class="badge bg-danger">Yes</span>
{% else %}
<span class="badge bg-success">No</span>
{% endif %}
</p>
<p>
<strong>Forwarded to HR:</strong> {% if session.forwarded_hr %}
<span class="badge bg-danger">Yes</span>
{% else %}
<span class="badge bg-success">No</span>
{% endif %}
</p>
<p><strong>User Rating:</strong> {{ session.user_rating|default:"N/A" }}</p>
<hr />
<p>
<strong>Data Source:</strong>
<a href="{% url 'data_source_detail' session.data_source.id %}"
>{{ session.data_source.name }}</a
>
</p>
<p><strong>Company:</strong> {{ session.data_source.company.name }}</p>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Session Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Session ID:</strong> {{ session.session_id }}</p>
<p>
<strong>Start Time:</strong>
{{ session.start_time|date:"F d, Y H:i" }}
</p>
<p>
<strong>End Time:</strong> {{ session.end_time|date:"F d, Y H:i" }}
</p>
<p>
<strong>IP Address:</strong> {{ session.ip_address|default:"N/A" }}
</p>
<p><strong>Country:</strong> {{ session.country|default:"N/A" }}</p>
<p><strong>Language:</strong> {{ session.language|default:"N/A" }}</p>
</div>
<div class="col-md-6">
<p><strong>Messages Sent:</strong> {{ session.messages_sent }}</p>
<p>
<strong>Average Response Time:</strong>
{{ session.avg_response_time|floatformat:2 }}s
</p>
<p><strong>Tokens:</strong> {{ session.tokens }}</p>
<p>
<strong>Token Cost:</strong>{{ session.tokens_eur|floatformat:2 }}
</p>
<p><strong>Category:</strong> {{ session.category|default:"N/A" }}</p>
<p>
<strong>Sentiment:</strong>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success"
>{{ session.sentiment }}</span
>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning"
>{{ session.sentiment }}</span
>
{% else %}
<span class="badge bg-secondary"
>{{ session.sentiment }}</span
>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</p>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<p class="mb-2"><strong>Initial Message:</strong></p>
<div class="card bg-light">
<div class="card-body">
<p class="mb-0">{{ session.initial_msg|default:"N/A" }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Additional Info</h5>
</div>
<div class="card-body">
<p>
<strong>Escalated:</strong> {% if session.escalated %}
<span class="badge bg-danger">Yes</span>
{% else %}
<span class="badge bg-success">No</span>
{% endif %}
</p>
<p>
<strong>Forwarded to HR:</strong> {% if session.forwarded_hr %}
<span class="badge bg-danger">Yes</span>
{% else %}
<span class="badge bg-success">No</span>
{% endif %}
</p>
<p><strong>User Rating:</strong> {{ session.user_rating|default:"N/A" }}</p>
<hr />
<p>
<strong>Data Source:</strong>
<a href="{% url 'data_source_detail' session.data_source.id %}"
>{{ session.data_source.name }}</a
>
</p>
<p><strong>Company:</strong> {{ session.data_source.company.name }}</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Full Transcript</h5>
</div>
<div class="card-body">
{% if session.full_transcript %}
<div class="chat-transcript" style="max-height: 500px; overflow-y: auto;">
<pre style="white-space: pre-wrap; font-family: inherit;">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Full Transcript</h5>
</div>
<div class="card-body">
{% if session.full_transcript %}
<div class="chat-transcript" style="max-height: 500px; overflow-y: auto;">
<pre style="white-space: pre-wrap; font-family: inherit;">
{{ session.full_transcript }}</pre
>
</div>
{% else %}
<p class="text-center text-muted">No transcript available.</p>
{% endif %}
</div>
</div>
</div>
</div>
>
</div>
{% else %}
<p class="text-center text-muted">No transcript available.</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -5,155 +5,163 @@
{% block title %}Dashboard | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">{{ selected_dashboard.name }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a
href="{% url 'edit_dashboard' selected_dashboard.id %}"
class="btn btn-sm btn-outline-secondary"
>
<i class="fas fa-edit"></i> Edit
</a>
<a
href="{% url 'delete_dashboard' selected_dashboard.id %}"
class="btn btn-sm btn-outline-danger"
>
<i class="fas fa-trash"></i> Delete
</a>
<a
href="{% url 'export_chats_csv' %}?dashboard_id={{ selected_dashboard.id }}"
class="btn btn-sm btn-outline-success"
>
<i class="fas fa-file-csv"></i> Export CSV
</a>
</div>
<div class="dropdown">
<button
class="btn btn-sm btn-outline-primary dropdown-toggle"
type="button"
id="timeRangeDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="fas fa-calendar"></i> Time Range
</button>
<ul class="dropdown-menu" aria-labelledby="timeRangeDropdown">
<li>
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=7"
>Last 7 days</a
>
</li>
<li>
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=30"
>Last 30 days</a
>
</li>
<li>
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=90"
>Last 90 days</a
>
</li>
<li>
<a class="dropdown-item" href="?dashboard_id={{ selected_dashboard.id }}&time_range=all"
>All time</a
>
</li>
</ul>
</div>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">{{ selected_dashboard.name }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a
href="{% url 'edit_dashboard' selected_dashboard.id %}"
class="btn btn-sm btn-outline-secondary"
>
<i class="fas fa-edit"></i> Edit
</a>
<a
href="{% url 'delete_dashboard' selected_dashboard.id %}"
class="btn btn-sm btn-outline-danger"
>
<i class="fas fa-trash"></i> Delete
</a>
<a
href="{% url 'export_chats_csv' %}?dashboard_id={{ selected_dashboard.id }}"
class="btn btn-sm btn-outline-success"
>
<i class="fas fa-file-csv"></i> Export CSV
</a>
</div>
<div class="dropdown">
<button
class="btn btn-sm btn-outline-primary dropdown-toggle"
type="button"
id="timeRangeDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="fas fa-calendar"></i> Time Range
</button>
<ul class="dropdown-menu" aria-labelledby="timeRangeDropdown">
<li>
<a
class="dropdown-item"
href="?dashboard_id={{ selected_dashboard.id }}&time_range=7"
>Last 7 days</a
>
</li>
<li>
<a
class="dropdown-item"
href="?dashboard_id={{ selected_dashboard.id }}&time_range=30"
>Last 30 days</a
>
</li>
<li>
<a
class="dropdown-item"
href="?dashboard_id={{ selected_dashboard.id }}&time_range=90"
>Last 90 days</a
>
</li>
<li>
<a
class="dropdown-item"
href="?dashboard_id={{ selected_dashboard.id }}&time_range=all"
>All time</a
>
</li>
</ul>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">
<div class="card stats-card bg-primary text-white">
<div class="card-body">
<h6 class="card-title">Total Sessions</h6>
<h3>{{ dashboard_data.total_sessions }}</h3>
<p>Chat conversations</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-success text-white">
<div class="card-body">
<h6 class="card-title">Avg Response Time</h6>
<h3>{{ dashboard_data.avg_response_time }}s</h3>
<p>Average response</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-info text-white">
<div class="card-body">
<h6 class="card-title">Total Tokens</h6>
<h3>{{ dashboard_data.total_tokens }}</h3>
<p>Total usage</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-warning text-white">
<div class="card-body">
<h6 class="card-title">Total Cost</h6>
<h3>€{{ dashboard_data.total_cost }}</h3>
<p>Token cost</p>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">
<div class="card stats-card bg-primary text-white">
<div class="card-body">
<h6 class="card-title">Total Sessions</h6>
<h3>{{ dashboard_data.total_sessions }}</h3>
<p>Chat conversations</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-success text-white">
<div class="card-body">
<h6 class="card-title">Avg Response Time</h6>
<h3>{{ dashboard_data.avg_response_time }}s</h3>
<p>Average response</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-info text-white">
<div class="card-body">
<h6 class="card-title">Total Tokens</h6>
<h3>{{ dashboard_data.total_tokens }}</h3>
<p>Total usage</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-warning text-white">
<div class="card-body">
<h6 class="card-title">Total Cost</h6>
<h3>€{{ dashboard_data.total_cost }}</h3>
<p>Token cost</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Sessions Over Time</h5>
</div>
<div class="card-body">
<div id="sessions-time-chart" class="chart-container"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Sentiment Analysis</h5>
</div>
<div class="card-body">
<div id="sentiment-chart" class="chart-container"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Sessions Over Time</h5>
</div>
<div class="card-body">
<div id="sessions-time-chart" class="chart-container"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Sentiment Analysis</h5>
</div>
<div class="card-body">
<div id="sentiment-chart" class="chart-container"></div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Top Countries</h5>
</div>
<div class="card-body">
<div id="country-chart" class="chart-container"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Categories</h5>
</div>
<div class="card-body">
<div id="category-chart" class="chart-container"></div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Top Countries</h5>
</div>
<div class="card-body">
<div id="country-chart" class="chart-container"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Categories</h5>
</div>
<div class="card-body">
<div id="category-chart" class="chart-container"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- prettier-ignore-start -->
<!-- prettier-ignore-start -->
<!-- Store the JSON data in script tags to avoid parsing issues -->
<script type="application/json" id="time-series-data">{{ time_series_data_json|safe }}</script>
<script type="application/json" id="sentiment-data">{{ sentiment_data_json|safe }}</script>
@ -161,154 +169,161 @@
<script type="application/json" id="category-data">{{ category_data_json|safe }}</script>
<!-- prettier-ignore-end -->
<script>
document.addEventListener("DOMContentLoaded", function () {
try {
// Parse the dashboard data components from script tags
const timeSeriesData = JSON.parse(document.getElementById("time-series-data").textContent);
const sentimentData = JSON.parse(document.getElementById("sentiment-data").textContent);
const countryData = JSON.parse(document.getElementById("country-data").textContent);
const categoryData = JSON.parse(document.getElementById("category-data").textContent);
<script>
document.addEventListener("DOMContentLoaded", function () {
try {
// Parse the dashboard data components from script tags
const timeSeriesData = JSON.parse(
document.getElementById("time-series-data").textContent,
);
const sentimentData = JSON.parse(
document.getElementById("sentiment-data").textContent,
);
const countryData = JSON.parse(document.getElementById("country-data").textContent);
const categoryData = JSON.parse(
document.getElementById("category-data").textContent,
);
console.log("Time series data loaded:", timeSeriesData);
console.log("Sentiment data loaded:", sentimentData);
console.log("Country data loaded:", countryData);
console.log("Category data loaded:", categoryData);
console.log("Time series data loaded:", timeSeriesData);
console.log("Sentiment data loaded:", sentimentData);
console.log("Country data loaded:", countryData);
console.log("Category data loaded:", categoryData);
// Sessions over time chart
if (timeSeriesData && timeSeriesData.length > 0) {
const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count);
// Sessions over time chart
if (timeSeriesData && timeSeriesData.length > 0) {
const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count);
Plotly.newPlot(
"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,
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 40 },
xaxis: {
title: "Date",
},
yaxis: {
title: "Number of Sessions",
},
}
);
} else {
document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
}
Plotly.newPlot(
"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,
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 40 },
xaxis: {
title: "Date",
},
yaxis: {
title: "Number of Sessions",
},
},
);
} else {
document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
}
// Sentiment analysis chart
if (sentimentData && sentimentData.length > 0) {
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)";
});
// Sentiment analysis chart
if (sentimentData && sentimentData.length > 0) {
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.newPlot(
"sentiment-chart",
[
{
values: sentimentValues,
labels: sentimentLabels,
type: "pie",
marker: {
colors: sentimentColors,
},
hole: 0.4,
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
}
);
} else {
document.getElementById("sentiment-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
}
Plotly.newPlot(
"sentiment-chart",
[
{
values: sentimentValues,
labels: sentimentLabels,
type: "pie",
marker: {
colors: sentimentColors,
},
hole: 0.4,
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
},
);
} else {
document.getElementById("sentiment-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No sentiment data available</p></div>';
}
// Country chart
if (countryData && countryData.length > 0) {
const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count);
// Country chart
if (countryData && countryData.length > 0) {
const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count);
Plotly.newPlot(
"country-chart",
[
{
x: countryValues,
y: countryLabels,
type: "bar",
orientation: "h",
marker: {
color: "rgb(54, 162, 235)",
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: {
title: "Number of Sessions",
},
}
);
} else {
document.getElementById("country-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
}
Plotly.newPlot(
"country-chart",
[
{
x: countryValues,
y: countryLabels,
type: "bar",
orientation: "h",
marker: {
color: "rgb(54, 162, 235)",
},
},
],
{
margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: {
title: "Number of Sessions",
},
},
);
} else {
document.getElementById("country-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No country data available</p></div>';
}
// Category chart
if (categoryData && categoryData.length > 0) {
const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count);
// Category chart
if (categoryData && categoryData.length > 0) {
const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count);
Plotly.newPlot(
"category-chart",
[
{
labels: categoryLabels,
values: categoryValues,
type: "pie",
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
}
);
} else {
document.getElementById("category-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
}
} catch (error) {
console.error("Error rendering charts:", error);
document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Error loading chart data. Please refresh the page.</p></div>';
});
}
});
</script>
Plotly.newPlot(
"category-chart",
[
{
labels: categoryLabels,
values: categoryValues,
type: "pie",
textinfo: "label+percent",
insidetextorientation: "radial",
},
],
{
margin: { t: 10, r: 10, b: 10, l: 10 },
},
);
} else {
document.getElementById("category-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No category data available</p></div>';
}
} catch (error) {
console.error("Error rendering charts:", error);
document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Error loading chart data. Please refresh the page.</p></div>';
});
}
});
</script>
{% endblock %}

View File

@ -3,41 +3,42 @@
{% block title %}Delete Dashboard | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Delete Dashboard</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Delete Dashboard</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h5 class="card-title mb-0">Confirm Deletion</h5>
</div>
<div class="card-body">
<p class="lead">
Are you sure you want to delete the dashboard "<strong>{{ dashboard.name }}</strong>"?
</p>
<p>
This action cannot be undone. The dashboard will be permanently deleted, but the
underlying data sources will remain intact.
</p>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h5 class="card-title mb-0">Confirm Deletion</h5>
</div>
<div class="card-body">
<p class="lead">
Are you sure you want to delete the dashboard
"<strong>{{ dashboard.name }}</strong>"?
</p>
<p>
This action cannot be undone. The dashboard will be permanently deleted, but
the underlying data sources will remain intact.
</p>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between mt-4">
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-danger">Delete Dashboard</button>
</div>
</form>
</div>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between mt-4">
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-danger">Delete Dashboard</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -2,42 +2,42 @@
{% load crispy_forms_tags %}
{% block title %}
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
| Chat Analytics
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
| Chat Analytics
{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
</h5>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
{% if is_create %}Create Dashboard{% else %}Update Dashboard{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
{% if is_create %}Create Dashboard{% else %}Edit Dashboard{% endif %}
</h5>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
{% if is_create %}Create Dashboard{% else %}Update Dashboard{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -4,47 +4,50 @@
{% block title %}Delete Data Source | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Delete Data Source</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a
href="{% url 'data_source_detail' data_source.id %}"
class="btn btn-sm btn-outline-secondary"
>
<i class="fas fa-arrow-left"></i> Back to Data Source
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Delete Data Source</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a
href="{% url 'data_source_detail' data_source.id %}"
class="btn btn-sm btn-outline-secondary"
>
<i class="fas fa-arrow-left"></i> Back to Data Source
</a>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h5 class="card-title mb-0">Confirm Deletion</h5>
</div>
<div class="card-body">
<p class="lead">
Are you sure you want to delete the data source
"<strong>{{ data_source.name }}</strong>"?
</p>
<p>
This action cannot be undone. The data source and all associated chat sessions
({{ data_source.chat_sessions.count }} sessions) will be permanently deleted.
</p>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h5 class="card-title mb-0">Confirm Deletion</h5>
</div>
<div class="card-body">
<p class="lead">
Are you sure you want to delete the data source
"<strong>{{ data_source.name }}</strong>"?
</p>
<p>
This action cannot be undone. The data source and all associated chat
sessions ({{ data_source.chat_sessions.count }} sessions) will be
permanently deleted.
</p>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between mt-4">
<a href="{% url 'data_source_detail' data_source.id %}" class="btn btn-secondary"
>Cancel</a
>
<button type="submit" class="btn btn-danger">Delete Data Source</button>
</div>
</form>
</div>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between mt-4">
<a
href="{% url 'data_source_detail' data_source.id %}"
class="btn btn-secondary"
>Cancel</a
>
<button type="submit" class="btn btn-danger">Delete Data Source</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -5,224 +5,251 @@
{% block title %}{{ data_source.name }} | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">{{ data_source.name }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'upload_data' %}" class="btn btn-sm btn-outline-secondary me-2">
<i class="fas fa-arrow-left"></i> Back to Data Sources
</a>
<a
href="{% url 'export_chats_csv' %}?data_source_id={{ data_source.id }}"
class="btn btn-sm btn-outline-success me-2"
>
<i class="fas fa-file-csv"></i> Export CSV
</a>
<a href="{% url 'delete_data_source' data_source.id %}" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash"></i> Delete
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">{{ data_source.name }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'upload_data' %}" class="btn btn-sm btn-outline-secondary me-2">
<i class="fas fa-arrow-left"></i> Back to Data Sources
</a>
<a
href="{% url 'export_chats_csv' %}?data_source_id={{ data_source.id }}"
class="btn btn-sm btn-outline-success me-2"
>
<i class="fas fa-file-csv"></i> Export CSV
</a>
<a
href="{% url 'delete_data_source' data_source.id %}"
class="btn btn-sm btn-outline-danger"
>
<i class="fas fa-trash"></i> Delete
</a>
</div>
</div>
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Data Source Details</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Name:</strong> {{ data_source.name }}</p>
<p><strong>Uploaded At:</strong> {{ data_source.uploaded_at|date:"F d, Y H:i" }}</p>
<p><strong>File:</strong> {{ data_source.file.name|split:"/"|last }}</p>
</div>
<div class="col-md-6">
<p><strong>Company:</strong> {{ data_source.company.name }}</p>
<p><strong>Total Sessions:</strong> {{ page_obj.paginator.count }}</p>
<p><strong>Description:</strong> {{ data_source.description }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Filter Sessions</h5>
</div>
<div class="card-body">
<form method="get" action="{% url 'search_chat_sessions' %}">
<div class="input-group mb-3">
<input
type="text"
name="q"
class="form-control"
placeholder="Search sessions..."
aria-label="Search sessions"
/>
<input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
<button class="btn btn-outline-primary" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Data Source Details</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Name:</strong> {{ data_source.name }}</p>
<p>
<strong>Uploaded At:</strong>
{{ data_source.uploaded_at|date:"F d, Y H:i" }}
</p>
<p><strong>File:</strong> {{ data_source.file.name|split:"/"|last }}</p>
</div>
<div class="col-md-6">
<p><strong>Company:</strong> {{ data_source.company.name }}</p>
<p><strong>Total Sessions:</strong> {{ page_obj.paginator.count }}</p>
<p><strong>Description:</strong> {{ data_source.description }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Filter Sessions</h5>
</div>
<div class="card-body">
<form method="get" action="{% url 'search_chat_sessions' %}">
<div class="input-group mb-3">
<input
type="text"
name="q"
class="form-control"
placeholder="Search sessions..."
aria-label="Search sessions"
/>
<input
type="hidden"
name="data_source_id"
value="{{ data_source.id }}"
/>
<button class="btn btn-outline-primary" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Chat Sessions ({{ page_obj.paginator.count }})</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Country</th>
<th>Language</th>
<th>Sentiment</th>
<th>Messages</th>
<th>Tokens</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>{{ session.country }}</td>
<td>{{ session.language }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.messages_sent }}</td>
<td>{{ session.tokens }}</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Chat Sessions ({{ page_obj.paginator.count }})</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Country</th>
<th>Language</th>
<th>Sentiment</th>
<th>Messages</th>
<th>Tokens</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>{{ session.country }}</td>
<td>{{ session.language }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success"
>{{ session.sentiment }}</span
>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger"
>{{ session.sentiment }}</span
>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning"
>{{ session.sentiment }}</span
>
{% else %}
<span class="badge bg-secondary"
>{{ session.sentiment }}</span
>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.messages_sent }}</td>
<td>{{ session.tokens }}</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button
class="btn btn-sm btn-outline-secondary"
disabled
>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">
No chat sessions found.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a class="page-link" href="?page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link"
href="?page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
</div>
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link"
href="?page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link"
href="?page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -5,270 +5,316 @@
{% block title %}Data View | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Data View</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
{% if selected_data_source %}
<a
href="{% url 'data_source_detail' selected_data_source.id %}"
class="btn btn-sm btn-outline-secondary ajax-nav-link"
>
<i class="fas fa-database"></i> View Source
</a>
{% endif %}
</div>
<div class="dropdown">
<button
class="btn btn-sm btn-outline-primary dropdown-toggle"
type="button"
id="dataViewDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="fas fa-filter"></i> Filter
</button>
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
<li><a class="dropdown-item ajax-nav-link" href="?view=all">All Sessions</a></li>
<li><a class="dropdown-item ajax-nav-link" href="?view=recent">Recent Sessions</a></li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=positive">Positive Sentiment</a>
</li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=negative">Negative Sentiment</a>
</li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=escalated">Escalated Sessions</a>
</li>
</ul>
</div>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Data View</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a
href="{% url 'dashboard' %}"
class="btn btn-sm btn-outline-secondary ajax-nav-link"
>
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
{% if selected_data_source %}
<a
href="{% url 'data_source_detail' selected_data_source.id %}"
class="btn btn-sm btn-outline-secondary ajax-nav-link"
>
<i class="fas fa-database"></i> View Source
</a>
{% endif %}
</div>
<div class="dropdown">
<button
class="btn btn-sm btn-outline-primary dropdown-toggle"
type="button"
id="dataViewDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="fas fa-filter"></i> Filter
</button>
<ul class="dropdown-menu" aria-labelledby="dataViewDropdown">
<li>
<a class="dropdown-item ajax-nav-link" href="?view=all">All Sessions</a>
</li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=recent"
>Recent Sessions</a
>
</li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=positive"
>Positive Sentiment</a
>
</li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=negative"
>Negative Sentiment</a
>
</li>
<li>
<a class="dropdown-item ajax-nav-link" href="?view=escalated"
>Escalated Sessions</a
>
</li>
</ul>
</div>
</div>
</div>
<!-- Data Source Selection -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Data Source Selection</h5>
</div>
<div class="card-body">
<form method="get" class="row g-3 align-items-center filter-form">
<div class="col-md-6">
<select name="data_source_id" class="form-select" aria-label="Select Data Source">
<option value="">All Data Sources</option>
{% for ds in data_sources %}
<option
value="{{ ds.id }}"
{% if selected_data_source.id == ds.id %}selected{% endif %}
>
{{ ds.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<select name="view" class="form-select" aria-label="Select View">
<option value="all" {% if view == 'all' %}selected{% endif %}>All Sessions</option>
<option value="recent" {% if view == 'recent' %}selected{% endif %}>
Recent Sessions
</option>
<option value="positive" {% if view == 'positive' %}selected{% endif %}>
Positive Sentiment
</option>
<option value="negative" {% if view == 'negative' %}selected{% endif %}>
Negative Sentiment
</option>
<option value="escalated" {% if view == 'escalated' %}selected{% endif %}>
Escalated Sessions
</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Apply</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Data Source Selection -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Data Source Selection</h5>
</div>
<div class="card-body">
<form method="get" class="row g-3 align-items-center filter-form">
<div class="col-md-6">
<select
name="data_source_id"
class="form-select"
aria-label="Select Data Source"
>
<option value="">All Data Sources</option>
{% for ds in data_sources %}
<option
value="{{ ds.id }}"
{% if selected_data_source.id == ds.id %}selected{% endif %}
>
{{ ds.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<select name="view" class="form-select" aria-label="Select View">
<option value="all" {% if view == 'all' %}selected{% endif %}>
All Sessions
</option>
<option value="recent" {% if view == 'recent' %}selected{% endif %}>
Recent Sessions
</option>
<option
value="positive"
{% if view == 'positive' %}selected{% endif %}
>
Positive Sentiment
</option>
<option
value="negative"
{% if view == 'negative' %}selected{% endif %}
>
Negative Sentiment
</option>
<option
value="escalated"
{% if view == 'escalated' %}selected{% endif %}
>
Escalated Sessions
</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Apply</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Export to CSV -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Export Data</h5>
</div>
<div class="card-body">
<form id="export-form" method="get" action="{% url 'export_chats_csv' %}" class="row g-3">
<!-- Pass current filters to export -->
<input type="hidden" name="data_source_id" value="{{ selected_data_source.id }}" />
<input type="hidden" name="view" value="{{ view }}" />
<!-- Export to CSV -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Export Data</h5>
</div>
<div class="card-body">
<form
id="export-form"
method="get"
action="{% url 'export_chats_csv' %}"
class="row g-3"
>
<!-- Pass current filters to export -->
<input
type="hidden"
name="data_source_id"
value="{{ selected_data_source.id }}"
/>
<input type="hidden" name="view" value="{{ view }}" />
<div class="col-md-3">
<label for="start_date" class="form-label">Start Date</label>
<input type="date" name="start_date" id="start_date" class="form-control" />
</div>
<div class="col-md-3">
<label for="end_date" class="form-label">End Date</label>
<input type="date" name="end_date" id="end_date" class="form-control" />
</div>
<div class="col-md-3">
<label for="country" class="form-label">Country</label>
<input
type="text"
name="country"
id="country"
class="form-control"
placeholder="Country"
/>
</div>
<div class="col-md-3">
<label for="sentiment" class="form-label">Sentiment</label>
<select name="sentiment" id="sentiment" class="form-select">
<option value="">All</option>
<option value="positive">Positive</option>
<option value="negative">Negative</option>
<option value="neutral">Neutral</option>
</select>
</div>
<div class="col-md-3">
<label for="escalated" class="form-label">Escalated</label>
<select name="escalated" id="escalated" class="form-select">
<option value="">All</option>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-file-csv me-1"></i> Export to CSV
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<label for="start_date" class="form-label">Start Date</label>
<input
type="date"
name="start_date"
id="start_date"
class="form-control"
/>
</div>
<div class="col-md-3">
<label for="end_date" class="form-label">End Date</label>
<input type="date" name="end_date" id="end_date" class="form-control" />
</div>
<div class="col-md-3">
<label for="country" class="form-label">Country</label>
<input
type="text"
name="country"
id="country"
class="form-control"
placeholder="Country"
/>
</div>
<div class="col-md-3">
<label for="sentiment" class="form-label">Sentiment</label>
<select name="sentiment" id="sentiment" class="form-select">
<option value="">All</option>
<option value="positive">Positive</option>
<option value="negative">Negative</option>
<option value="neutral">Neutral</option>
</select>
</div>
<div class="col-md-3">
<label for="escalated" class="form-label">Escalated</label>
<select name="escalated" id="escalated" class="form-select">
<option value="">All</option>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-file-csv me-1"></i> Export to CSV
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Data Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
Chat Sessions
{% if selected_data_source %}
for {{ selected_data_source.name }}
{% endif %}
{% if view != 'all' %}
({{ view|title }})
{% endif %}
</h5>
<span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span>
</div>
<div class="card-body">
<!-- Loading spinner shown during AJAX requests -->
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading data...</p>
</div>
<!-- Data Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
Chat Sessions
{% if selected_data_source %}
for {{ selected_data_source.name }}
{% endif %}
{% if view != 'all' %}
({{ view|title }})
{% endif %}
</h5>
<span class="badge bg-primary">{{ page_obj.paginator.count }} sessions</span>
</div>
<div class="card-body">
<!-- Loading spinner shown during AJAX requests -->
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading data...</p>
</div>
<!-- Data table container that will be updated via AJAX -->
<div id="ajax-content-container">{% include "dashboard/partials/data_table.html" %}</div>
</div>
</div>
</div>
</div>
<!-- Data table container that will be updated via AJAX -->
<div id="ajax-content-container">
{% include "dashboard/partials/data_table.html" %}
</div>
</div>
</div>
</div>
</div>
<!-- Data Summary -->
{% if page_obj %}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Summary</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="card stats-card bg-light">
<div class="card-body">
<h6 class="card-title">Total Sessions</h6>
<h3>{{ page_obj.paginator.count }}</h3>
<p>Chat conversations</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-light">
<div class="card-body">
<h6 class="card-title">Avg Response Time</h6>
<h3>{{ avg_response_time|floatformat:2 }}s</h3>
<p>Average response</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-light">
<div class="card-body">
<h6 class="card-title">Avg Messages</h6>
<h3>{{ avg_messages|floatformat:1 }}</h3>
<p>Per conversation</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-light">
<div class="card-body">
<h6 class="card-title">Escalation Rate</h6>
<h3>{{ escalation_rate|floatformat:1 }}%</h3>
<p>Escalated sessions</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Data Summary -->
{% if page_obj %}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Summary</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="card stats-card bg-light">
<div class="card-body">
<h6 class="card-title">Total Sessions</h6>
<h3>{{ page_obj.paginator.count }}</h3>
<p>Chat conversations</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-light">
<div class="card-body">
<h6 class="card-title">Avg Response Time</h6>
<h3>{{ avg_response_time|floatformat:2 }}s</h3>
<p>Average response</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-light">
<div class="card-body">
<h6 class="card-title">Avg Messages</h6>
<h3>{{ avg_messages|floatformat:1 }}</h3>
<p>Per conversation</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card bg-light">
<div class="card-body">
<h6 class="card-title">Escalation Rate</h6>
<h3>{{ escalation_rate|floatformat:1 }}%</h3>
<p>Escalated sessions</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
// Function to update the summary section with new data
function updateSummary(data) {
if (document.querySelector(".stats-card h3:nth-of-type(1)")) {
document.querySelector(".stats-card h3:nth-of-type(1)").textContent =
data.page_obj.paginator.count;
}
if (document.querySelector(".stats-card h3:nth-of-type(2)")) {
document.querySelector(".stats-card h3:nth-of-type(2)").textContent =
data.avg_response_time !== null && data.avg_response_time !== undefined
? data.avg_response_time.toFixed(2) + "s"
: "0.00s";
}
if (document.querySelector(".stats-card h3:nth-of-type(3)")) {
document.querySelector(".stats-card h3:nth-of-type(3)").textContent =
data.avg_messages !== null && data.avg_messages !== undefined
? data.avg_messages.toFixed(1)
: "0.0";
}
if (document.querySelector(".stats-card h3:nth-of-type(4)")) {
document.querySelector(".stats-card h3:nth-of-type(4)").textContent =
data.escalation_rate !== null && data.escalation_rate !== undefined
? data.escalation_rate.toFixed(1) + "%"
: "0.0%";
}
}
</script>
<script>
// Function to update the summary section with new data
function updateSummary(data) {
if (document.querySelector(".stats-card h3:nth-of-type(1)")) {
document.querySelector(".stats-card h3:nth-of-type(1)").textContent =
data.page_obj.paginator.count;
}
if (document.querySelector(".stats-card h3:nth-of-type(2)")) {
document.querySelector(".stats-card h3:nth-of-type(2)").textContent =
data.avg_response_time !== null && data.avg_response_time !== undefined
? data.avg_response_time.toFixed(2) + "s"
: "0.00s";
}
if (document.querySelector(".stats-card h3:nth-of-type(3)")) {
document.querySelector(".stats-card h3:nth-of-type(3)").textContent =
data.avg_messages !== null && data.avg_messages !== undefined
? data.avg_messages.toFixed(1)
: "0.0";
}
if (document.querySelector(".stats-card h3:nth-of-type(4)")) {
document.querySelector(".stats-card h3:nth-of-type(4)").textContent =
data.escalation_rate !== null && data.escalation_rate !== undefined
? data.escalation_rate.toFixed(1) + "%"
: "0.0%";
}
}
</script>
{% endblock %}

View File

@ -4,37 +4,41 @@
{% block title %}No Company | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">No Company Association</h1>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">No Company Association</h1>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">Account Not Associated with a Company</h5>
</div>
<div class="card-body text-center">
<div class="mb-4">
<i class="fas fa-building fa-4x text-warning mb-3"></i>
<h4>You are not currently associated with any company</h4>
<p class="lead">You need to be associated with a company to access the dashboard.</p>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">Account Not Associated with a Company</h5>
</div>
<div class="card-body text-center">
<div class="mb-4">
<i class="fas fa-building fa-4x text-warning mb-3"></i>
<h4>You are not currently associated with any company</h4>
<p class="lead">
You need to be associated with a company to access the dashboard.
</p>
</div>
<p>
Please contact an administrator to have your account assigned to a company. Once your
account is associated with a company, you'll be able to access the dashboard and its
features.
</p>
<p>
Please contact an administrator to have your account assigned to a company.
Once your account is associated with a company, you'll be able to access the
dashboard and its features.
</p>
<div class="mt-4">
<a href="{% url 'profile' %}" class="btn btn-primary">View Your Profile</a>
<a href="{% url 'logout' %}" class="btn btn-outline-secondary ms-2">Logout</a>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4">
<a href="{% url 'profile' %}" class="btn btn-primary">View Your Profile</a>
<a href="{% url 'logout' %}" class="btn btn-outline-secondary ms-2"
>Logout</a
>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,160 +1,160 @@
<!-- templates/dashboard/partials/data_table.html -->
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Country</th>
<th>Language</th>
<th>Messages</th>
<th>Sentiment</th>
<th>Response Time</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>{{ session.country|default:"N/A" }}</td>
<td>{{ session.language|default:"N/A" }}</td>
<td>{{ session.messages_sent }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary ajax-nav-link"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Country</th>
<th>Language</th>
<th>Messages</th>
<th>Sentiment</th>
<th>Response Time</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>{{ session.country|default:"N/A" }}</td>
<td>{{ session.language|default:"N/A" }}</td>
<td>{{ session.messages_sent }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.avg_response_time|floatformat:2 }}s</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary ajax-nav-link"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="1"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.previous_page_number }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="1"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.previous_page_number }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.next_page_number }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.paginator.num_pages }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.next_page_number }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.paginator.num_pages }}"
href="?{% if selected_data_source %}data_source_id={{ selected_data_source.id }}&{% endif %}{% if view %}view={{ view }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}

View File

@ -1,164 +1,168 @@
<!-- templates/dashboard/partials/search_results_table.html -->
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Data Source</th>
<th>Country</th>
<th>Language</th>
<th>Sentiment</th>
<th>Messages</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>
<a href="{% url 'data_source_detail' session.data_source.id %}" class="ajax-nav-link"
>{{ session.data_source.name|truncatechars:15 }}</a
>
</td>
<td>{{ session.country }}</td>
<td>{{ session.language }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.messages_sent }}</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">No chat sessions found matching your criteria.</td>
</tr>
{% endfor %}
</tbody>
</table>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Session ID</th>
<th>Start Time</th>
<th>Data Source</th>
<th>Country</th>
<th>Language</th>
<th>Sentiment</th>
<th>Messages</th>
<th>Category</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in page_obj %}
<tr>
<td>{{ session.session_id|truncatechars:10 }}</td>
<td>{{ session.start_time|date:"M d, Y H:i" }}</td>
<td>
<a
href="{% url 'data_source_detail' session.data_source.id %}"
class="ajax-nav-link"
>{{ session.data_source.name|truncatechars:15 }}</a
>
</td>
<td>{{ session.country }}</td>
<td>{{ session.language }}</td>
<td>
{% if session.sentiment %}
{% if 'positive' in session.sentiment|lower %}
<span class="badge bg-success">{{ session.sentiment }}</span>
{% elif 'negative' in session.sentiment|lower %}
<span class="badge bg-danger">{{ session.sentiment }}</span>
{% elif 'neutral' in session.sentiment|lower %}
<span class="badge bg-warning">{{ session.sentiment }}</span>
{% else %}
<span class="badge bg-secondary">{{ session.sentiment }}</span>
{% endif %}
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>{{ session.messages_sent }}</td>
<td>{{ session.category|default:"N/A" }}</td>
<td>
{% if session.session_id %}
<a
href="{% url 'chat_session_detail' session.session_id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
{% else %}
<button class="btn btn-sm btn-outline-secondary" disabled>
<i class="fas fa-eye-slash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center">
No chat sessions found matching your criteria.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="1"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.previous_page_number }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
<nav aria-label="Page navigation" class="mt-4" id="pagination-container">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="1"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page=1"
aria-label="First"
>
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.previous_page_number }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.previous_page_number }}"
aria-label="Previous"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ num }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ num }}"
>{{ num }}</a
>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.next_page_number }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.paginator.num_pages }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% if page_obj.has_next %}
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.next_page_number }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.next_page_number }}"
aria-label="Next"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a
class="page-link pagination-link"
data-page="{{ page_obj.paginator.num_pages }}"
href="?{% if query %}q={{ query }}&{% endif %}{% if data_source %}data_source_id={{ data_source.id }}&{% endif %}page={{ page_obj.paginator.num_pages }}"
aria-label="Last"
>
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}

View File

@ -4,82 +4,90 @@
{% block title %}Search Results | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Search Results</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Search Results</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary ajax-nav-link">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Search Chat Sessions</h5>
</div>
<div class="card-body">
<form method="get" action="{% url 'search_chat_sessions' %}" class="search-form">
<div class="input-group">
<input
type="text"
name="q"
class="form-control"
placeholder="Search sessions..."
value="{{ query }}"
aria-label="Search sessions"
/>
{% if data_source %}
<input type="hidden" name="data_source_id" value="{{ data_source.id }}" />
{% endif %}
<button class="btn btn-outline-primary" type="submit">
<i class="fas fa-search"></i> Search
</button>
</div>
<div class="mt-2 text-muted">
<small
>Search by session ID, country, language, sentiment, category, or message
content.</small
>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Search Chat Sessions</h5>
</div>
<div class="card-body">
<form
method="get"
action="{% url 'search_chat_sessions' %}"
class="search-form"
>
<div class="input-group">
<input
type="text"
name="q"
class="form-control"
placeholder="Search sessions..."
value="{{ query }}"
aria-label="Search sessions"
/>
{% if data_source %}
<input
type="hidden"
name="data_source_id"
value="{{ data_source.id }}"
/>
{% endif %}
<button class="btn btn-outline-primary" type="submit">
<i class="fas fa-search"></i> Search
</button>
</div>
<div class="mt-2 text-muted">
<small
>Search by session ID, country, language, sentiment, category, or
message content.</small
>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
Results {% if query %}for "{{ query }}"{% endif %}
{% if data_source %}in {{ data_source.name }}{% endif %}
({{ page_obj.paginator.count }})
</h5>
</div>
<div class="card-body">
<!-- Loading spinner shown during AJAX requests -->
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading data...</p>
</div>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
Results {% if query %}for "{{ query }}"{% endif %}
{% if data_source %}in {{ data_source.name }}{% endif %}
({{ page_obj.paginator.count }})
</h5>
</div>
<div class="card-body">
<!-- Loading spinner shown during AJAX requests -->
<div id="ajax-loading-spinner" class="text-center py-4 d-none">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading data...</p>
</div>
<!-- Search results container that will be updated via AJAX -->
<div id="ajax-content-container">
{% include "dashboard/partials/search_results_table.html" %}
</div>
</div>
</div>
</div>
</div>
<!-- Search results container that will be updated via AJAX -->
<div id="ajax-content-container">
{% include "dashboard/partials/search_results_table.html" %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- No need for extra JavaScript here, using common ajax-pagination.js -->
<!-- No need for extra JavaScript here, using common ajax-pagination.js -->
{% endblock %}

View File

@ -6,192 +6,192 @@
{% block title %}Upload Data | Chat Analytics{% endblock %}
{% block content %}
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Upload Data</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<h1 class="h2">Upload Data</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<a href="{% url 'dashboard' %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Upload CSV File</h5>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %} {{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Upload</button>
</div>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Upload CSV File</h5>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %} {{ form|crispy }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Upload</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">CSV File Format</h5>
</div>
<div class="card-body">
<p>The CSV file should contain the following columns:</p>
<table class="table table-sm">
<thead>
<tr>
<th>Column</th>
<th>Description</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<tr>
<td>session_id</td>
<td>Unique identifier for the chat session</td>
<td>String</td>
</tr>
<tr>
<td>start_time</td>
<td>When the session started</td>
<td>Datetime</td>
</tr>
<tr>
<td>end_time</td>
<td>When the session ended</td>
<td>Datetime</td>
</tr>
<tr>
<td>ip_address</td>
<td>IP address of the user</td>
<td>String</td>
</tr>
<tr>
<td>country</td>
<td>Country of the user</td>
<td>String</td>
</tr>
<tr>
<td>language</td>
<td>Language used in the conversation</td>
<td>String</td>
</tr>
<tr>
<td>messages_sent</td>
<td>Number of messages in the conversation</td>
<td>Integer</td>
</tr>
<tr>
<td>sentiment</td>
<td>Sentiment analysis of the conversation</td>
<td>String</td>
</tr>
<tr>
<td>escalated</td>
<td>Whether the conversation was escalated</td>
<td>Boolean</td>
</tr>
<tr>
<td>forwarded_hr</td>
<td>Whether the conversation was forwarded to HR</td>
<td>Boolean</td>
</tr>
<tr>
<td>full_transcript</td>
<td>Full transcript of the conversation</td>
<td>Text</td>
</tr>
<tr>
<td>avg_response_time</td>
<td>Average response time in seconds</td>
<td>Float</td>
</tr>
<tr>
<td>tokens</td>
<td>Total number of tokens used</td>
<td>Integer</td>
</tr>
<tr>
<td>tokens_eur</td>
<td>Cost of tokens in EUR</td>
<td>Float</td>
</tr>
<tr>
<td>category</td>
<td>Category of the conversation</td>
<td>String</td>
</tr>
<tr>
<td>initial_msg</td>
<td>First message from the user</td>
<td>Text</td>
</tr>
<tr>
<td>user_rating</td>
<td>User rating of the conversation</td>
<td>String</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">CSV File Format</h5>
</div>
<div class="card-body">
<p>The CSV file should contain the following columns:</p>
<table class="table table-sm">
<thead>
<tr>
<th>Column</th>
<th>Description</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<tr>
<td>session_id</td>
<td>Unique identifier for the chat session</td>
<td>String</td>
</tr>
<tr>
<td>start_time</td>
<td>When the session started</td>
<td>Datetime</td>
</tr>
<tr>
<td>end_time</td>
<td>When the session ended</td>
<td>Datetime</td>
</tr>
<tr>
<td>ip_address</td>
<td>IP address of the user</td>
<td>String</td>
</tr>
<tr>
<td>country</td>
<td>Country of the user</td>
<td>String</td>
</tr>
<tr>
<td>language</td>
<td>Language used in the conversation</td>
<td>String</td>
</tr>
<tr>
<td>messages_sent</td>
<td>Number of messages in the conversation</td>
<td>Integer</td>
</tr>
<tr>
<td>sentiment</td>
<td>Sentiment analysis of the conversation</td>
<td>String</td>
</tr>
<tr>
<td>escalated</td>
<td>Whether the conversation was escalated</td>
<td>Boolean</td>
</tr>
<tr>
<td>forwarded_hr</td>
<td>Whether the conversation was forwarded to HR</td>
<td>Boolean</td>
</tr>
<tr>
<td>full_transcript</td>
<td>Full transcript of the conversation</td>
<td>Text</td>
</tr>
<tr>
<td>avg_response_time</td>
<td>Average response time in seconds</td>
<td>Float</td>
</tr>
<tr>
<td>tokens</td>
<td>Total number of tokens used</td>
<td>Integer</td>
</tr>
<tr>
<td>tokens_eur</td>
<td>Cost of tokens in EUR</td>
<td>Float</td>
</tr>
<tr>
<td>category</td>
<td>Category of the conversation</td>
<td>String</td>
</tr>
<tr>
<td>initial_msg</td>
<td>First message from the user</td>
<td>Text</td>
</tr>
<tr>
<td>user_rating</td>
<td>User rating of the conversation</td>
<td>String</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
{% if data_sources %}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Uploaded Data Sources</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Uploaded</th>
<th>File</th>
<th>Sessions</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for data_source in data_sources %}
<tr>
<td>{{ data_source.name }}</td>
<td>{{ data_source.description|truncatechars:50 }}</td>
<td>{{ data_source.uploaded_at|date:"M d, Y H:i" }}</td>
<td>{{ data_source.file.name|split:"/"|last }}</td>
<td>{{ data_source.chat_sessions.count }}</td>
<td>
<a
href="{% url 'data_source_detail' data_source.id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
<a
href="{% url 'delete_data_source' data_source.id %}"
class="btn btn-sm btn-outline-danger"
>
<i class="fas fa-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% if data_sources %}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">Uploaded Data Sources</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Uploaded</th>
<th>File</th>
<th>Sessions</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for data_source in data_sources %}
<tr>
<td>{{ data_source.name }}</td>
<td>{{ data_source.description|truncatechars:50 }}</td>
<td>{{ data_source.uploaded_at|date:"M d, Y H:i" }}</td>
<td>{{ data_source.file.name|split:"/"|last }}</td>
<td>{{ data_source.chat_sessions.count }}</td>
<td>
<a
href="{% url 'data_source_detail' data_source.id %}"
class="btn btn-sm btn-outline-primary"
>
<i class="fas fa-eye"></i>
</a>
<a
href="{% url 'delete_data_source' data_source.id %}"
class="btn btn-sm btn-outline-danger"
>
<i class="fas fa-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}