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,8 @@ indent_size = 4
# HTML and Django/Jinja2 template files
[*.{html,htm}]
indent_size = 2
indent_style = tab
indent_size = 4
# Allow prettier to format Django/Jinja templates properly
# The following comment options can be used in individual files if needed:
# <!-- prettier-ignore -->
@ -26,6 +27,7 @@ indent_size = 2
# CSS, JavaScript, and JSON files
[*.{css,scss,js,json}]
indent_style = tab
indent_size = 4
# Markdown files

9
.gitignore vendored
View File

@ -407,3 +407,12 @@ pyrightconfig.json
*Zone.Identifier
examples/
**/migrations/[0-9]**.py
# UV specific
.uv/
.uv-configs/
# Pyright and IDE specific
.vscode/
.idea/
.pyright/

92
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,92 @@
default_install_hook_types:
- pre-commit
- post-checkout
- post-merge
- post-rewrite
repos:
# uv hooks for dependency management
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.7.5
hooks:
- id: uv-export
# Standard pre-commit hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-toml
- id: check-added-large-files
args: ["--maxkb=500"]
- id: detect-private-key
- id: check-merge-conflict
- id: check-case-conflict
- id: debug-statements
- id: mixed-line-ending
args: ["--fix=lf"]
# # HTML/Django template linting
# - repo: https://github.com/rtts/djhtml
# rev: 3.0.7
# hooks:
# - id: djhtml
# entry: djhtml --tabwidth 4 --
# - id: djcss
# - id: djjs
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [javascript, jsx, ts, tsx, css, scss, html, json, yaml, markdown]
additional_dependencies:
- prettier
- prettier-plugin-jinja-template
# Ruff for linting and formatting
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.10
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
# Django-specific hooks
- repo: local
hooks:
- id: django-check
name: Django Check
entry: uv run python dashboard_project/manage.py check
language: system
pass_filenames: false
types: [python]
always_run: true
- id: django-check-migrations
name: Django Check Migrations
entry: uv run python dashboard_project/manage.py makemigrations --check --dry-run
language: system
pass_filenames: false
types: [python]
# Security checks
- repo: https://github.com/pycqa/bandit
rev: 1.8.3
hooks:
- id: bandit
args: ["-c", "pyproject.toml", "-r", "dashboard_project"]
additional_dependencies: ["bandit[toml]"]
# # Type checking
# - repo: https://github.com/pre-commit/mirrors-mypy
# rev: v1.15.0
# hooks:
# - id: mypy
# additional_dependencies:
# - django-stubs>=5.0.2
# - types-python-dateutil
# - types-requests
# - types-PyYAML

View File

@ -11,9 +11,7 @@
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"useTabs": true,
"overrides": [
{
"files": ["*.html"],

11
.scripts/lint.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
# Run linting, formatting and type checking
echo "Running Ruff linter..."
uv run -m ruff check dashboard_project
echo "Running Ruff formatter..."
uv run -m ruff format dashboard_project
echo "Running Black formatter..."
uv run -m black dashboard_project

6
.scripts/test.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
# Run tests with coverage
echo "Running tests with coverage..."
uv run -m coverage run -m pytest
uv run -m coverage report -m

18
.uv Normal file
View File

@ -0,0 +1,18 @@
[uv]
# Keep the uv.lock file up to date
keep-lockfile = true
# Cache compiled bytecode for dependencies
compile-bytecode = true
# Use a local cache directory
local-cache = true
# Verbosity of output
verbosity = "minimal"
# Define which part of the environment to check
environment-checks = ["python", "dependencies"]
# How to resolve dependencies not specified with exact versions
dependency-resolution = "strict"

View File

@ -43,5 +43,5 @@
"notebook.source.organizeImports": "explicit"
},
"notebook.formatOnSave.enabled": true,
"prettier.requireConfig": true,
"prettier.requireConfig": true
}

View File

@ -10,15 +10,25 @@ ENV DJANGO_SETTINGS_MODULE=dashboard_project.settings
# Set work directory
WORKDIR /app
# Install UV for Python package management
RUN pip install uv
# Copy project files
COPY pyproject.toml .
COPY uv.lock .
COPY . .
# Install dependencies
COPY requirements.txt .
RUN uv pip install -e .
# Copy project
COPY . .
# Change to the Django project directory
WORKDIR /app/dashboard_project
# Collect static files
RUN python manage.py collectstatic --noinput
# Change back to the app directory
WORKDIR /app
# Run gunicorn
CMD ["gunicorn", "dashboard_project.wsgi:application", "--bind", "0.0.0.0:8000"]

78
Makefile Normal file
View File

@ -0,0 +1,78 @@
.PHONY: venv install install-dev lint test format clean run migrate makemigrations superuser setup-node format-js
# Create a virtual environment
venv:
uv venv -p 3.13
# Install production dependencies
install:
uv pip install -e .
# Install development dependencies
install-dev:
uv pip install -e ".[dev]"
# Run linting
lint:
uv run -m ruff check dashboard_project
# Run tests
test:
uv run -m pytest
# Format Python code
format:
uv run -m ruff format dashboard_project
uv run -m black dashboard_project
# Format JavaScript/CSS/HTML files with Prettier
format-js:
npx run format
# Setup Node.js dependencies
setup-node:
npm install
# Clean Python cache files
clean:
find . -type d -name "__pycache__" -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
find . -type f -name "*.pyo" -delete
find . -type f -name "*.pyd" -delete
find . -type d -name "*.egg-info" -exec rm -rf {} +
find . -type d -name "*.egg" -exec rm -rf {} +
find . -type d -name ".pytest_cache" -exec rm -rf {} +
find . -type d -name ".coverage" -exec rm -rf {} +
find . -type d -name "htmlcov" -exec rm -rf {} +
find . -type d -name ".ruff_cache" -exec rm -rf {} +
rm -rf build/
rm -rf dist/
# Run the development server
run:
cd dashboard_project && uv run python manage.py runserver 8001
# Apply migrations
migrate:
cd dashboard_project && uv run python manage.py migrate
# Create migrations
makemigrations:
cd dashboard_project && uv run python manage.py makemigrations
# Create a superuser
superuser:
cd dashboard_project && uv run python manage.py createsuperuser
# Update uv lock file
lock:
uv pip freeze > requirements.lock
# Setup pre-commit hooks
setup-pre-commit:
uv pip install pre-commit
pre-commit install
# Run pre-commit on all files
lint-all:
pre-commit run --all-files

View File

@ -1,37 +1,61 @@
# Prettier for Django/Jinja Templates
# Prettier for Django Templates
This project uses Prettier with the `prettier-plugin-jinja-template` plugin to format HTML templates with Django/Jinja syntax.
This project uses Prettier with the `prettier-plugin-django-annotations` plugin to format HTML templates with Django template syntax.
## Setup
To use Prettier with your Django templates, you'll need to install Prettier and the Jinja template plugin:
The project is already configured with Prettier integration in pre-commit hooks. The configuration includes:
1. `.prettierrc` - Configuration file with Django HTML support
2. `.prettierignore` - Files to exclude from formatting
3. Pre-commit hook for automatic formatting on commits
### Manual Installation
To use Prettier locally (outside of pre-commit hooks), you'll need to install the dependencies:
```bash
# Using npm
npm install --save-dev prettier prettier-plugin-jinja-template
npm install
# Or using yarn
yarn add --dev prettier prettier-plugin-jinja-template
# Or install just the required packages
npm install --save-dev prettier prettier-plugin-django-annotations
```
## Usage
Once installed, you can format your Django templates using:
### With Pre-commit
Prettier will automatically run as part of the pre-commit hooks when you commit changes.
To manually run the pre-commit hooks on all files:
```bash
pre-commit run prettier --all-files
```
### Using npm Scripts
The package.json includes npm scripts for formatting:
```bash
# Format all static files
npm run format
# Check formatting without modifying files
npm run format:check
```
### Command Line
You can also run Prettier directly:
```bash
# Format a specific file
npx prettier --write path/to/template.html
# Format all HTML files
npx prettier --write "**/*.html"
```
### Without install
If you don't want to install the plugin, you can use the following command:
```bash
npx prettier --plugin=prettier-plugin-jinja-template --parser=jinja-template --write **/*.html
npx prettier --write "dashboard_project/templates/**/*.html"
```
## VSCode Integration
@ -40,12 +64,12 @@ For VSCode users, install the Prettier extension and add these settings to your
```json
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"prettier.requireConfig": true
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"prettier.requireConfig": true
}
```
@ -62,3 +86,12 @@ If you need to prevent Prettier from formatting a section of your template:
This works too.
</div>
```
## Django Template Support
The `prettier-plugin-django-annotations` plugin provides special handling for Django templates, including:
- Proper formatting of Django template tags (`{% %}`)
- Support for Django template comments (`{# #}`)
- Preservation of Django template variable output (`{{ }}`)
- Special handling for Django template syntax inside HTML attributes

View File

@ -15,9 +15,9 @@ A Django application that creates an analytics dashboard for chat session data.
## Requirements
- Python 3.13+
- Django 5.0+
- PostgreSQL (optional, SQLite is fine for development)
- Other dependencies listed in [`pyproject.toml`](pyproject.toml)
- Django 5.2+
- UV package manager (recommended)
- Other dependencies listed in [`pyproject.toml`](./pyproject.toml)
## Setup
@ -27,42 +27,103 @@ A Django application that creates an analytics dashboard for chat session data.
```sh
git clone <repository-url>
cd dashboard_project
cd LiveGraphsDjango
```
2. Create a virtual environment and activate it:
2. Install uv if you don't have it yet:
```sh
# Install using pip
pip install uv
# Or with curl (Unix/macOS)
curl -sSf https://install.pypa.io/get-uv.py | python3 -
# Or on Windows with PowerShell
irm https://install.pypa.io/get-uv.ps1 | iex
```
3. Create a virtual environment and activate it:
```sh
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
```
3. Install dependencies:
4. Install dependencies:
```sh
uv pip install -r requirements.txt
# Install all dependencies including dev dependencies
uv pip install -e ".[dev]"
# Or just runtime dependencies
uv pip install -e .
```
4. Run migrations:
5. Run migrations:
```sh
uv run python manage.py makemigrations
uv run python manage.py migrate
cd dashboard_project
python manage.py makemigrations
python manage.py migrate
```
5. Create a superuser:
6. Create a superuser:
```sh
uv run python manage.py createsuperuser
python manage.py createsuperuser
```
6. Run the development server:
7. Run the development server:
```sh
uv run python manage.py runserver
python manage.py runserver
```
7. Access the application at <http://127.0.0.1:8000/>
8. Access the application at <http://127.0.0.1:8000/>
### Development Workflow with UV
UV offers several advantages over traditional pip, including faster dependency resolution and installation:
1. Running linting and formatting:
```sh
# Using the convenience script
./.scripts/lint.sh
# Or directly
uv run -m ruff check dashboard_project
uv run -m ruff format dashboard_project
uv run -m black dashboard_project
```
2. Running tests:
```sh
# Using the convenience script
./.scripts/test.sh
# Or directly
uv run -m pytest
```
3. Adding new dependencies:
```sh
# Add to project
uv pip install package_name
# Then update pyproject.toml manually
# And update the lockfile
uv pip freeze > requirements.lock
```
4. Updating the lockfile:
```sh
uv pip compile pyproject.toml -o uv.lock
```
### Using Docker

42
TODO.md
View File

@ -2,30 +2,30 @@
- When I zoom into the dasboard page, the graphs don't scale/adjust to fit the window until I completely refresh the page, can we solve that?
- Add export functionality to the dashboard:
- File formats:
- CSV
- Excel
- JSON
- XML
- HTML
- PDF
- Make the export button a dropdown with the following options:
- Export as CSV
- Export as Excel
- Export as JSON
- Export as XML
- Export as HTML
- Export as PDF
- Make the export data section folded by default and only show the export button.
- Adjust the downloaded file name to include the company name, date and time of the export.
- File formats:
- CSV
- Excel
- JSON
- XML
- HTML
- PDF
- Make the export button a dropdown with the following options:
- Export as CSV
- Export as Excel
- Export as JSON
- Export as XML
- Export as HTML
- Export as PDF
- Make the export data section folded by default and only show the export button.
- Adjust the downloaded file name to include the company name, date and time of the export.
- Add a button to download the CSV file for the selected company.
- Make it possible to modify the column names in the CSV file through the admin interface.
- Add possibility to add a company logo in the admin interface.
- Add periodic download from https://proto.notso.ai/jumbo/chats possibility for the jumbo company.
- Authentication: Basic Auth
- URL: https://proto.notso.ai/jumbo/chats
- Username: jumboadmin
- Password: jumboadmin
- Add periodic download from <https://proto.notso.ai/XY/chats> possibility for the XY company.
- Authentication: Basic Auth
- URL: <https://proto.notso.ai/XY/chats>
- Username: xxxx
- Password: xxxx
- Reduce amount of rows in the table to fit the screen.
- Add dark mode/theming to the dashboard.
- Add Notso AI branding to the dashboard.

View File

@ -1,5 +0,0 @@
"""
LiveGraphsDjango - Dashboard for analyzing chat session data.
"""
__version__ = "0.1.0"

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 %}

View File

@ -16,6 +16,7 @@ services:
- DEBUG=0
- SECRET_KEY=your_secret_key_here
- ALLOWED_HOSTS=localhost,127.0.0.1
- DJANGO_SETTINGS_MODULE=dashboard_project.settings
depends_on:
- db
@ -27,6 +28,8 @@ services:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=dashboard_db
ports:
- "5432:5432"
nginx:
image: nginx:latest

39
package-lock.json generated
View File

@ -1,39 +0,0 @@
{
"name": "LiveGraphsDjango",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"prettier": "^3.5.3",
"prettier-plugin-jinja-template": "^2.1.0"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-jinja-template": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-jinja-template/-/prettier-plugin-jinja-template-2.1.0.tgz",
"integrity": "sha512-mzoCp2Oy9BDSug80fw3B3J4n4KQj1hRvoQOL1akqcDKBb5nvYxrik9zUEDs4AEJ6nK7QDTGoH0y9rx7AlnQ78Q==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"prettier": "^3.0.0"
}
}
}
}

View File

@ -1,6 +1,17 @@
{
"devDependencies": {
"prettier": "^3.5.3",
"prettier-plugin-jinja-template": "^2.1.0"
}
"name": "livegraphsdjango",
"version": "0.1.0",
"description": "Live Graphs Django Dashboard",
"private": true,
"scripts": {
"lint": "eslint dashboard_project/static/js/",
"format": "prettier --write \"dashboard_project/static/**/*.{js,css,html}\"",
"format:check": "prettier --check \"dashboard_project/static/**/*.{js,css,html}\"",
"stylelint": "stylelint \"dashboard_project/static/css/**/*.css\" --fix",
"stylelint:check": "stylelint \"dashboard_project/static/css/**/*.css\""
},
"devDependencies": {
"prettier": "^3.5.3",
"prettier-plugin-jinja-template": "^2.1.0"
}
}

View File

@ -1,9 +1,21 @@
[project]
name = "livegraphsdjango"
version = "0.1.0"
description = "Add your description here"
description = "Live Graphs Django Dashboard"
readme = "README.md"
requires-python = ">=3.13"
authors = [{ name = "LiveGraphs Team" }]
license = { text = "MIT" }
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.13",
"Framework :: Django",
"Framework :: Django :: 5.2",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"crispy-bootstrap5>=2025.4",
"django>=5.2.1",
@ -17,16 +29,29 @@ dependencies = [
"whitenoise>=6.9.0",
]
[project.scripts]
# Django management commands
"manage" = "dashboard_project.manage:main"
"runserver" = "dashboard_project.manage:main"
"migrate" = "dashboard_project.manage:main"
"makemigrations" = "dashboard_project.manage:main"
"collectstatic" = "dashboard_project.manage:main"
"createsuperuser" = "dashboard_project.manage:main"
"shell" = "dashboard_project.manage:main"
"test" = "dashboard_project.manage:main"
[dependency-groups]
dev = [
"bandit>=1.8.3",
"black>=25.1.0",
"coverage>=7.8.0",
"django-debug-toolbar>=5.2.0",
"django-stubs>=5.2.0",
"mypy>=1.15.0",
"pre-commit>=4.2.0",
"pytest>=8.3.5",
"pytest-django>=4.11.1",
"ruff>=0.11.10",
]
[build-system]
requires = ["setuptools>=69.0.0", "wheel>=0.42.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["dashboard_project"]
[tool.setuptools.package-data]
"dashboard_project" = ["static/**/*", "templates/**/*", "media/**/*"]
[tool.ruff]
# Exclude a variety of commonly ignored directories.
@ -67,7 +92,7 @@ indent-width = 4
target-version = "py313"
[tool.ruff.lint]
select = ["E", "F", "I"]
select = ["E", "F", "I", "B", "C4", "ARG", "SIM", "PERF"]
ignore = ["E501"]
fixable = ["ALL"]
unfixable = []
@ -76,3 +101,55 @@ unfixable = []
quote-style = "double"
indent-style = "space"
line-ending = "lf"
[tool.bandit]
exclude_dirs = ["tests", "venv", ".venv", ".git", "__pycache__", "migrations", "**/create_sample_data.py"]
skips = ["B101"]
targets = ["dashboard_project"]
[tool.mypy]
python_version = "3.13"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
disallow_incomplete_defs = false
plugins = ["mypy_django_plugin.main"]
[[tool.mypy.overrides]]
module = ["django.*", "rest_framework.*"]
ignore_missing_imports = true
[tool.django-stubs]
django_settings_module = "dashboard_project.settings"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "dashboard_project.settings"
python_files = "test_*.py"
testpaths = ["dashboard_project"]
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning",
]
[tool.coverage.run]
source = ["dashboard_project"]
omit = [
"dashboard_project/manage.py",
"dashboard_project/*/migrations/*",
"dashboard_project/*/tests/*",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"pass",
"raise ImportError",
]
[project.urls]
"Documentation" = "https://github.com/kjanat/livegraphsdjango#readme"
"Source" = "https://github.com/kjanat/livegraphsdjango"
"Bug Tracker" = "https://github.com/kjanat/livegraphsdjango/issues"

320
requirements.txt Normal file
View File

@ -0,0 +1,320 @@
# This file was autogenerated by uv via the following command:
# uv export --frozen --output-file=requirements.txt
-e .
asgiref==3.8.1 \
--hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
--hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
# via
# django
# django-allauth
# django-stubs
bandit==1.8.3 \
--hash=sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8 \
--hash=sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a
black==25.1.0 \
--hash=sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171 \
--hash=sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666 \
--hash=sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f \
--hash=sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717 \
--hash=sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18 \
--hash=sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3
cfgv==3.4.0 \
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
# via pre-commit
click==8.2.0 \
--hash=sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c \
--hash=sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d
# via black
colorama==0.4.6 ; sys_platform == 'win32' \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via
# bandit
# click
# pytest
coverage==7.8.0 \
--hash=sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3 \
--hash=sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25 \
--hash=sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257 \
--hash=sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada \
--hash=sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64 \
--hash=sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067 \
--hash=sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733 \
--hash=sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008 \
--hash=sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd \
--hash=sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00 \
--hash=sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501 \
--hash=sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42 \
--hash=sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487 \
--hash=sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4 \
--hash=sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73 \
--hash=sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a \
--hash=sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1 \
--hash=sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7 \
--hash=sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d \
--hash=sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502 \
--hash=sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323 \
--hash=sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883
crispy-bootstrap5==2025.4 \
--hash=sha256:51efa19c7d40e339774a6fe23407e83b95b7634cad6de70fd1f1093131bea1d9 \
--hash=sha256:d675ea7e245048905077dfe16bf1fa1ee16842f52fe88164ccc8a5e2d11119b3
# via livegraphsdjango
distlib==0.3.9 \
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
# via virtualenv
django==5.2.1 \
--hash=sha256:57fe1f1b59462caed092c80b3dd324fd92161b620d59a9ba9181c34746c97284 \
--hash=sha256:a9b680e84f9a0e71da83e399f1e922e1ab37b2173ced046b541c72e1589a5961
# via
# crispy-bootstrap5
# django-allauth
# django-crispy-forms
# django-debug-toolbar
# django-stubs
# django-stubs-ext
# livegraphsdjango
django-allauth==65.8.0 \
--hash=sha256:9da589d99d412740629333a01865a90c95c97e0fae0cde789aa45a8fda90e83b
# via livegraphsdjango
django-crispy-forms==2.4 \
--hash=sha256:5a4b99876cfb1bdd3e47727731b6d4197c51c0da502befbfbec6a93010b02030 \
--hash=sha256:915e1ffdeb2987d78b33fabfeff8e5203c8776aa910a3a659a2c514ca125f3bd
# via
# crispy-bootstrap5
# livegraphsdjango
django-debug-toolbar==5.2.0 \
--hash=sha256:15627f4c2836a9099d795e271e38e8cf5204ccd79d5dbcd748f8a6c284dcd195 \
--hash=sha256:9e7f0145e1a1b7d78fcc3b53798686170a5b472d9cf085d88121ff823e900821
django-stubs==5.2.0 \
--hash=sha256:07e25c2d3cbff5be540227ff37719cc89f215dfaaaa5eb038a75b01bbfbb2722 \
--hash=sha256:cd52da033489afc1357d6245f49e3cc57bf49015877253fb8efc6722ea3d2d2b
django-stubs-ext==5.2.0 \
--hash=sha256:00c4ae307b538f5643af761a914c3f8e4e3f25f4e7c6d7098f1906c0d8f2aac9 \
--hash=sha256:b27ae0aab970af4894ba4e9b3fcd3e03421dc8731516669659ee56122d148b23
# via django-stubs
filelock==3.18.0 \
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
# via virtualenv
gunicorn==23.0.0 \
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
# via livegraphsdjango
identify==2.6.10 \
--hash=sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8 \
--hash=sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25
# via pre-commit
iniconfig==2.1.0 \
--hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \
--hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
# via pytest
markdown-it-py==3.0.0 \
--hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
--hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
# via rich
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
# via markdown-it-py
mypy==1.15.0 \
--hash=sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43 \
--hash=sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e \
--hash=sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d \
--hash=sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445 \
--hash=sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5 \
--hash=sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf \
--hash=sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357 \
--hash=sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036
mypy-extensions==1.1.0 \
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
--hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558
# via
# black
# mypy
narwhals==1.39.1 \
--hash=sha256:68d0f29c760f1a9419ada537f35f21ff202b0be1419e6d22135a0352c6d96deb \
--hash=sha256:cf15389e6f8c5321e8cd0ca8b5bace3b1aea5f5622fa59dfd64821998741d836
# via plotly
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
# via pre-commit
numpy==2.2.5 \
--hash=sha256:02f226baeefa68f7d579e213d0f3493496397d8f1cff5e2b222af274c86a552a \
--hash=sha256:059b51b658f4414fff78c6d7b1b4e18283ab5fa56d270ff212d5ba0c561846f4 \
--hash=sha256:1a161c2c79ab30fe4501d5a2bbfe8b162490757cf90b7f05be8b80bc02f7bb8e \
--hash=sha256:261a1ef047751bb02f29dfe337230b5882b54521ca121fc7f62668133cb119c9 \
--hash=sha256:2ba321813a00e508d5421104464510cc962a6f791aa2fca1c97b1e65027da80d \
--hash=sha256:352d330048c055ea6db701130abc48a21bec690a8d38f8284e00fab256dc1376 \
--hash=sha256:3d14b17b9be5f9c9301f43d2e2a4886a33b53f4e6fdf9ca2f4cc60aeeee76372 \
--hash=sha256:4520caa3807c1ceb005d125a75e715567806fed67e315cea619d5ec6e75a4191 \
--hash=sha256:47f9ed103af0bc63182609044b0490747e03bd20a67e391192dde119bf43d52f \
--hash=sha256:54088a5a147ab71a8e7fdfd8c3601972751ded0739c6b696ad9cb0343e21ab73 \
--hash=sha256:55f09e00d4dccd76b179c0f18a44f041e5332fd0e022886ba1c0bbf3ea4a18d0 \
--hash=sha256:8b4c0773b6ada798f51f0f8e30c054d32304ccc6e9c5d93d46cb26f3d385ab19 \
--hash=sha256:8dfa94b6a4374e7851bbb6f35e6ded2120b752b063e6acdd3157e4d2bb922eba \
--hash=sha256:97c8425d4e26437e65e1d189d22dff4a079b747ff9c2788057bfb8114ce1e133 \
--hash=sha256:a4cbdef3ddf777423060c6f81b5694bad2dc9675f110c4b2a60dc0181543fac7 \
--hash=sha256:a9c0d994680cd991b1cb772e8b297340085466a6fe964bc9d4e80f5e2f43c291 \
--hash=sha256:c26843fd58f65da9491165072da2cccc372530681de481ef670dcc8e27cfb066 \
--hash=sha256:c8b82a55ef86a2d8e81b63da85e55f5537d2157165be1cb2ce7cfa57b6aef38b \
--hash=sha256:d403c84991b5ad291d3809bace5e85f4bbf44a04bdc9a88ed2bb1807b3360bb8 \
--hash=sha256:d8882a829fd779f0f43998e931c466802a77ca1ee0fe25a3abe50278616b1471 \
--hash=sha256:e8b025c351b9f0e8b5436cf28a07fa4ac0204d67b38f01433ac7f9b870fa38c6
# via
# livegraphsdjango
# pandas
packaging==25.0 \
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
--hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
# via
# black
# gunicorn
# plotly
# pytest
pandas==2.2.3 \
--hash=sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d \
--hash=sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4 \
--hash=sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0 \
--hash=sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28 \
--hash=sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18 \
--hash=sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468 \
--hash=sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667 \
--hash=sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d \
--hash=sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb \
--hash=sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659 \
--hash=sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a \
--hash=sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2 \
--hash=sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015 \
--hash=sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24
# via livegraphsdjango
pathspec==0.12.1 \
--hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \
--hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712
# via black
pbr==6.1.1 \
--hash=sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76 \
--hash=sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b
# via stevedore
platformdirs==4.3.8 \
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
# via
# black
# virtualenv
plotly==6.1.0 \
--hash=sha256:a29d3ed523c9d7960095693af1ee52689830df0f9c6bae3e5e92c20c4f5684c3 \
--hash=sha256:f13f497ccc2d97f06f771a30b27fab0cbd220f2975865f4ecbc75057135521de
# via livegraphsdjango
pluggy==1.6.0 \
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
# via pytest
pre-commit==4.2.0 \
--hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \
--hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd
pygments==2.19.1 \
--hash=sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f \
--hash=sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c
# via rich
pytest==8.3.5 \
--hash=sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820 \
--hash=sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845
# via pytest-django
pytest-django==4.11.1 \
--hash=sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10 \
--hash=sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991
python-dateutil==2.9.0.post0 \
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
# via pandas
python-dotenv==1.1.0 \
--hash=sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5 \
--hash=sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d
# via livegraphsdjango
pytz==2025.2 \
--hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \
--hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00
# via pandas
pyyaml==6.0.2 \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba
# via
# bandit
# pre-commit
rich==14.0.0 \
--hash=sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0 \
--hash=sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725
# via bandit
ruff==0.11.10 \
--hash=sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca \
--hash=sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f \
--hash=sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad \
--hash=sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b \
--hash=sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125 \
--hash=sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641 \
--hash=sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224 \
--hash=sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947 \
--hash=sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4 \
--hash=sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58 \
--hash=sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5 \
--hash=sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed \
--hash=sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6 \
--hash=sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2 \
--hash=sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19 \
--hash=sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523 \
--hash=sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1 \
--hash=sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2
setuptools==80.7.1 \
--hash=sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009 \
--hash=sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552
# via pbr
six==1.17.0 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
# via python-dateutil
sqlparse==0.5.3 \
--hash=sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272 \
--hash=sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca
# via
# django
# django-debug-toolbar
stevedore==5.4.1 \
--hash=sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b \
--hash=sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe
# via bandit
types-pyyaml==6.0.12.20250516 \
--hash=sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530 \
--hash=sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba
# via django-stubs
typing-extensions==4.13.2 \
--hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \
--hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef
# via
# django-stubs
# django-stubs-ext
# mypy
tzdata==2025.2 \
--hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \
--hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9
# via
# django
# pandas
virtualenv==20.31.2 \
--hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \
--hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af
# via pre-commit
whitenoise==6.9.0 \
--hash=sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609 \
--hash=sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df
# via livegraphsdjango

View File

@ -1,19 +0,0 @@
from setuptools import setup
setup(
name="livegraphsdjango",
version="0.1.0",
packages=["dashboard_project"],
entry_points={
"console_scripts": [
"manage=dashboard_project.manage:main",
"runserver=dashboard_project.__main__:main",
"migrate=dashboard_project.__main__:main",
"makemigrations=dashboard_project.__main__:main",
"collectstatic=dashboard_project.__main__:main",
"createsuperuser=dashboard_project.__main__:main",
"shell=dashboard_project.__main__:main",
"test=dashboard_project.__main__:main",
],
},
)

459
uv.lock generated
View File

@ -11,6 +11,100 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
]
[[package]]
name = "bandit"
version = "1.8.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "pyyaml" },
{ name = "rich" },
{ name = "stevedore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/a5/144a45f8e67df9d66c3bc3f7e69a39537db8bff1189ab7cff4e9459215da/bandit-1.8.3.tar.gz", hash = "sha256:f5847beb654d309422985c36644649924e0ea4425c76dec2e89110b87506193a", size = 4232005, upload-time = "2025-02-17T05:24:57.031Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/85/db74b9233e0aa27ec96891045c5e920a64dd5cbccd50f8e64e9460f48d35/bandit-1.8.3-py3-none-any.whl", hash = "sha256:28f04dc0d258e1dd0f99dee8eefa13d1cb5e3fde1a5ab0c523971f97b289bcd8", size = 129078, upload-time = "2025-02-17T05:24:54.068Z" },
]
[[package]]
name = "black"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" },
{ url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" },
{ url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" },
{ url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" },
{ url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" },
]
[[package]]
name = "cfgv"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
]
[[package]]
name = "click"
version = "8.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coverage"
version = "7.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" },
{ url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" },
{ url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" },
{ url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" },
{ url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" },
{ url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" },
{ url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" },
{ url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" },
{ url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" },
{ url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" },
{ url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" },
{ url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" },
{ url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" },
{ url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" },
{ url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" },
{ url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" },
{ url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" },
{ url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" },
{ url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" },
]
[[package]]
name = "crispy-bootstrap5"
version = "2025.4"
@ -24,6 +118,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/9a/4f1166cc82c9f777cf9a5bc2a75171d63301ac317c5de8f59bd44bfe2b7a/crispy_bootstrap5-2025.4-py3-none-any.whl", hash = "sha256:51efa19c7d40e339774a6fe23407e83b95b7634cad6de70fd1f1093131bea1d9", size = 24772, upload-time = "2025-04-02T12:33:14.904Z" },
]
[[package]]
name = "distlib"
version = "0.3.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" },
]
[[package]]
name = "django"
version = "5.2.1"
@ -60,6 +163,57 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/ec/a25f81e56a674e63cf6c3dd8e36b1b3fecc238fecd6098504adc0cc61402/django_crispy_forms-2.4-py3-none-any.whl", hash = "sha256:5a4b99876cfb1bdd3e47727731b6d4197c51c0da502befbfbec6a93010b02030", size = 31446, upload-time = "2025-04-13T07:24:58.516Z" },
]
[[package]]
name = "django-debug-toolbar"
version = "5.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "sqlparse" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2a/9f/97ba2648f66fa208fc7f19d6895586d08bc5f0ab930a1f41032e60f31a41/django_debug_toolbar-5.2.0.tar.gz", hash = "sha256:9e7f0145e1a1b7d78fcc3b53798686170a5b472d9cf085d88121ff823e900821", size = 297901, upload-time = "2025-04-29T05:23:57.533Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/c2/ed3cb815002664349e9e50799b8c00ef15941f4cad797247cadbdeebab02/django_debug_toolbar-5.2.0-py3-none-any.whl", hash = "sha256:15627f4c2836a9099d795e271e38e8cf5204ccd79d5dbcd748f8a6c284dcd195", size = 262834, upload-time = "2025-04-29T05:23:55.472Z" },
]
[[package]]
name = "django-stubs"
version = "5.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
{ name = "django-stubs-ext" },
{ name = "types-pyyaml" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/87/79/7e7ad8b4bac545c8e608fa8db0bd061977f93035a112be78a7f3ffc6ff66/django_stubs-5.2.0.tar.gz", hash = "sha256:07e25c2d3cbff5be540227ff37719cc89f215dfaaaa5eb038a75b01bbfbb2722", size = 276297, upload-time = "2025-04-26T10:49:04.974Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/01/5913ba5514337f3896c7bcbff6075808184dd303cd0fc3ecc289ec7e0c96/django_stubs-5.2.0-py3-none-any.whl", hash = "sha256:cd52da033489afc1357d6245f49e3cc57bf49015877253fb8efc6722ea3d2d2b", size = 481836, upload-time = "2025-04-26T10:49:02.97Z" },
]
[[package]]
name = "django-stubs-ext"
version = "5.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7c/7a/84338605817960942c1ea9d852685923ccccd0d91ba0d49532605973491f/django_stubs_ext-5.2.0.tar.gz", hash = "sha256:00c4ae307b538f5643af761a914c3f8e4e3f25f4e7c6d7098f1906c0d8f2aac9", size = 9618, upload-time = "2025-04-26T10:48:38.05Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/65/9f5ca467d84a67c0c547f10b0ece9fd9c26c5efc818a01bf6a3d306c2a0c/django_stubs_ext-5.2.0-py3-none-any.whl", hash = "sha256:b27ae0aab970af4894ba4e9b3fcd3e03421dc8731516669659ee56122d148b23", size = 9066, upload-time = "2025-04-26T10:48:36.032Z" },
]
[[package]]
name = "filelock"
version = "3.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
@ -72,10 +226,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
]
[[package]]
name = "identify"
version = "2.6.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201, upload-time = "2025-04-19T15:10:38.32Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload-time = "2025-04-19T15:10:36.701Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "livegraphsdjango"
version = "0.1.0"
source = { virtual = "." }
source = { editable = "." }
dependencies = [
{ name = "crispy-bootstrap5" },
{ name = "django" },
@ -89,6 +261,20 @@ dependencies = [
{ name = "whitenoise" },
]
[package.dev-dependencies]
dev = [
{ name = "bandit" },
{ name = "black" },
{ name = "coverage" },
{ name = "django-debug-toolbar" },
{ name = "django-stubs" },
{ name = "mypy" },
{ name = "pre-commit" },
{ name = "pytest" },
{ name = "pytest-django" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "crispy-bootstrap5", specifier = ">=2025.4" },
@ -103,6 +289,69 @@ requires-dist = [
{ name = "whitenoise", specifier = ">=6.9.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "bandit", specifier = ">=1.8.3" },
{ name = "black", specifier = ">=25.1.0" },
{ name = "coverage", specifier = ">=7.8.0" },
{ name = "django-debug-toolbar", specifier = ">=5.2.0" },
{ name = "django-stubs", specifier = ">=5.2.0" },
{ name = "mypy", specifier = ">=1.15.0" },
{ name = "pre-commit", specifier = ">=4.2.0" },
{ name = "pytest", specifier = ">=8.3.5" },
{ name = "pytest-django", specifier = ">=4.11.1" },
{ name = "ruff", specifier = ">=0.11.10" },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mypy"
version = "1.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" },
{ url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" },
{ url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" },
{ url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" },
{ url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" },
{ url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" },
{ url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "narwhals"
version = "1.39.1"
@ -112,6 +361,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/c4/b83520ecc27840a4d58bd585ae0ec0f8a4f2b0c5a965b66749254a54de0e/narwhals-1.39.1-py3-none-any.whl", hash = "sha256:68d0f29c760f1a9419ada537f35f21ff202b0be1419e6d22135a0352c6d96deb", size = 355009, upload-time = "2025-05-15T17:45:07.954Z" },
]
[[package]]
name = "nodeenv"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
[[package]]
name = "numpy"
version = "2.2.5"
@ -176,6 +434,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "pbr"
version = "6.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702, upload-time = "2025-02-04T14:28:06.514Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997, upload-time = "2025-02-04T14:28:03.168Z" },
]
[[package]]
name = "platformdirs"
version = "4.3.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
]
[[package]]
name = "plotly"
version = "6.1.0"
@ -189,6 +477,67 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/11/83ae52318353f9da4a88cc23e7f9dbc3d449b3f0fd6158fba15eb3c3b816/plotly-6.1.0-py3-none-any.whl", hash = "sha256:a29d3ed523c9d7960095693af1ee52689830df0f9c6bae3e5e92c20c4f5684c3", size = 16118476, upload-time = "2025-05-15T16:04:30.81Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pre-commit"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
{ name = "identify" },
{ name = "nodeenv" },
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
]
[[package]]
name = "pytest"
version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
]
[[package]]
name = "pytest-django"
version = "4.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@ -219,6 +568,70 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
]
[[package]]
name = "rich"
version = "14.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
]
[[package]]
name = "ruff"
version = "0.11.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/4c/4a3c5a97faaae6b428b336dcca81d03ad04779f8072c267ad2bd860126bf/ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6", size = 4165632, upload-time = "2025-05-15T14:08:56.76Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/9f/596c628f8824a2ce4cd12b0f0b4c0629a62dfffc5d0f742c19a1d71be108/ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58", size = 10316243, upload-time = "2025-05-15T14:08:12.884Z" },
{ url = "https://files.pythonhosted.org/packages/3c/38/c1e0b77ab58b426f8c332c1d1d3432d9fc9a9ea622806e208220cb133c9e/ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed", size = 11083636, upload-time = "2025-05-15T14:08:16.551Z" },
{ url = "https://files.pythonhosted.org/packages/23/41/b75e15961d6047d7fe1b13886e56e8413be8467a4e1be0a07f3b303cd65a/ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca", size = 10441624, upload-time = "2025-05-15T14:08:19.032Z" },
{ url = "https://files.pythonhosted.org/packages/b6/2c/e396b6703f131406db1811ea3d746f29d91b41bbd43ad572fea30da1435d/ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2", size = 10624358, upload-time = "2025-05-15T14:08:21.542Z" },
{ url = "https://files.pythonhosted.org/packages/bd/8c/ee6cca8bdaf0f9a3704796022851a33cd37d1340bceaf4f6e991eb164e2e/ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5", size = 10176850, upload-time = "2025-05-15T14:08:23.682Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ce/4e27e131a434321b3b7c66512c3ee7505b446eb1c8a80777c023f7e876e6/ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641", size = 11759787, upload-time = "2025-05-15T14:08:25.733Z" },
{ url = "https://files.pythonhosted.org/packages/58/de/1e2e77fc72adc7cf5b5123fd04a59ed329651d3eab9825674a9e640b100b/ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947", size = 12430479, upload-time = "2025-05-15T14:08:28.013Z" },
{ url = "https://files.pythonhosted.org/packages/07/ed/af0f2340f33b70d50121628ef175523cc4c37619e98d98748c85764c8d88/ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4", size = 11919760, upload-time = "2025-05-15T14:08:30.956Z" },
{ url = "https://files.pythonhosted.org/packages/24/09/d7b3d3226d535cb89234390f418d10e00a157b6c4a06dfbe723e9322cb7d/ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f", size = 14041747, upload-time = "2025-05-15T14:08:33.297Z" },
{ url = "https://files.pythonhosted.org/packages/62/b3/a63b4e91850e3f47f78795e6630ee9266cb6963de8f0191600289c2bb8f4/ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b", size = 11550657, upload-time = "2025-05-15T14:08:35.639Z" },
{ url = "https://files.pythonhosted.org/packages/46/63/a4f95c241d79402ccdbdb1d823d156c89fbb36ebfc4289dce092e6c0aa8f/ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2", size = 10489671, upload-time = "2025-05-15T14:08:38.437Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9b/c2238bfebf1e473495659c523d50b1685258b6345d5ab0b418ca3f010cd7/ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523", size = 10160135, upload-time = "2025-05-15T14:08:41.247Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ef/ba7251dd15206688dbfba7d413c0312e94df3b31b08f5d695580b755a899/ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125", size = 11170179, upload-time = "2025-05-15T14:08:43.762Z" },
{ url = "https://files.pythonhosted.org/packages/73/9f/5c336717293203ba275dbfa2ea16e49b29a9fd9a0ea8b6febfc17e133577/ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad", size = 11626021, upload-time = "2025-05-15T14:08:46.451Z" },
{ url = "https://files.pythonhosted.org/packages/d9/2b/162fa86d2639076667c9aa59196c020dc6d7023ac8f342416c2f5ec4bda0/ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19", size = 10494958, upload-time = "2025-05-15T14:08:49.601Z" },
{ url = "https://files.pythonhosted.org/packages/24/f3/66643d8f32f50a4b0d09a4832b7d919145ee2b944d43e604fbd7c144d175/ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224", size = 11650285, upload-time = "2025-05-15T14:08:52.392Z" },
{ url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278, upload-time = "2025-05-15T14:08:54.56Z" },
]
[[package]]
name = "setuptools"
version = "80.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/8b/dc1773e8e5d07fd27c1632c45c1de856ac3dbf09c0147f782ca6d990cf15/setuptools-80.7.1.tar.gz", hash = "sha256:f6ffc5f0142b1bd8d0ca94ee91b30c0ca862ffd50826da1ea85258a06fd94552", size = 1319188, upload-time = "2025-05-15T02:41:00.955Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/18/0e835c3a557dc5faffc8f91092f62fc337c1dab1066715842e7a4b318ec4/setuptools-80.7.1-py3-none-any.whl", hash = "sha256:ca5cc1069b85dc23070a6628e6bcecb3292acac802399c7f8edc0100619f9009", size = 1200776, upload-time = "2025-05-15T02:40:58.887Z" },
]
[[package]]
name = "six"
version = "1.17.0"
@ -237,6 +650,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
]
[[package]]
name = "stevedore"
version = "5.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pbr" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/3f/13cacea96900bbd31bb05c6b74135f85d15564fc583802be56976c940470/stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b", size = 513858, upload-time = "2025-02-20T14:03:57.285Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/45/8c4ebc0c460e6ec38e62ab245ad3c7fc10b210116cea7c16d61602aa9558/stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe", size = 49533, upload-time = "2025-02-20T14:03:55.849Z" },
]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20250516"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378, upload-time = "2025-05-16T03:08:04.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" },
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
]
[[package]]
name = "tzdata"
version = "2025.2"
@ -246,6 +689,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
[[package]]
name = "virtualenv"
version = "20.31.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
]
[[package]]
name = "whitenoise"
version = "6.9.0"