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

9
.gitignore vendored
View File

@ -407,3 +407,12 @@ pyrightconfig.json
*Zone.Identifier *Zone.Identifier
examples/ examples/
**/migrations/[0-9]**.py **/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, "requirePragma": false,
"semi": true, "semi": true,
"singleQuote": false, "singleQuote": false,
"tabWidth": 2, "useTabs": true,
"trailingComma": "es5",
"useTabs": false,
"overrides": [ "overrides": [
{ {
"files": ["*.html"], "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.source.organizeImports": "explicit"
}, },
"notebook.formatOnSave.enabled": true, "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 # Set work directory
WORKDIR /app WORKDIR /app
# Install UV for Python package management
RUN pip install uv
# Copy project files
COPY pyproject.toml .
COPY uv.lock .
COPY . .
# Install dependencies # Install dependencies
COPY requirements.txt .
RUN uv pip install -e . RUN uv pip install -e .
# Copy project # Change to the Django project directory
COPY . . WORKDIR /app/dashboard_project
# Collect static files # Collect static files
RUN python manage.py collectstatic --noinput RUN python manage.py collectstatic --noinput
# Change back to the app directory
WORKDIR /app
# Run gunicorn # Run gunicorn
CMD ["gunicorn", "dashboard_project.wsgi:application", "--bind", "0.0.0.0:8000"] 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 ## 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 ```bash
# Using npm # Using npm
npm install --save-dev prettier prettier-plugin-jinja-template npm install
# Or using yarn # Or install just the required packages
yarn add --dev prettier prettier-plugin-jinja-template npm install --save-dev prettier prettier-plugin-django-annotations
``` ```
## Usage ## 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 ```bash
# Format a specific file # Format a specific file
npx prettier --write path/to/template.html npx prettier --write path/to/template.html
# Format all HTML files # Format all HTML files
npx prettier --write "**/*.html" npx prettier --write "dashboard_project/templates/**/*.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
``` ```
## VSCode Integration ## VSCode Integration
@ -40,12 +64,12 @@ For VSCode users, install the Prettier extension and add these settings to your
```json ```json
{ {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"[html]": { "[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
}, },
"prettier.requireConfig": true "prettier.requireConfig": true
} }
``` ```
@ -62,3 +86,12 @@ If you need to prevent Prettier from formatting a section of your template:
This works too. This works too.
</div> </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 ## Requirements
- Python 3.13+ - Python 3.13+
- Django 5.0+ - Django 5.2+
- PostgreSQL (optional, SQLite is fine for development) - UV package manager (recommended)
- Other dependencies listed in [`pyproject.toml`](pyproject.toml) - Other dependencies listed in [`pyproject.toml`](./pyproject.toml)
## Setup ## Setup
@ -27,42 +27,103 @@ A Django application that creates an analytics dashboard for chat session data.
```sh ```sh
git clone <repository-url> 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 ```sh
uv venv uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate source .venv/bin/activate # On Windows: .venv\Scripts\activate
``` ```
3. Install dependencies: 4. Install dependencies:
```sh ```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 ```sh
uv run python manage.py makemigrations cd dashboard_project
uv run python manage.py migrate python manage.py makemigrations
python manage.py migrate
``` ```
5. Create a superuser: 6. Create a superuser:
```sh ```sh
uv run python manage.py createsuperuser python manage.py createsuperuser
``` ```
6. Run the development server: 7. Run the development server:
```sh ```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 ### 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? - 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: - Add export functionality to the dashboard:
- File formats: - File formats:
- CSV - CSV
- Excel - Excel
- JSON - JSON
- XML - XML
- HTML - HTML
- PDF - PDF
- Make the export button a dropdown with the following options: - Make the export button a dropdown with the following options:
- Export as CSV - Export as CSV
- Export as Excel - Export as Excel
- Export as JSON - Export as JSON
- Export as XML - Export as XML
- Export as HTML - Export as HTML
- Export as PDF - Export as PDF
- Make the export data section folded by default and only show the export button. - 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. - 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. - 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. - 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 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. - Add periodic download from <https://proto.notso.ai/XY/chats> possibility for the XY company.
- Authentication: Basic Auth - Authentication: Basic Auth
- URL: https://proto.notso.ai/jumbo/chats - URL: <https://proto.notso.ai/XY/chats>
- Username: jumboadmin - Username: xxxx
- Password: jumboadmin - Password: xxxx
- Reduce amount of rows in the table to fit the screen. - Reduce amount of rows in the table to fit the screen.
- Add dark mode/theming to the dashboard. - Add dark mode/theming to the dashboard.
- Add Notso AI branding 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): class Command(BaseCommand):
help = "Create sample data for testing" help = "Create sample data for testing"
def handle(self, *args, **kwargs): def handle(self, **_options):
self.stdout.write("Creating sample data...") self.stdout.write("Creating sample data...")
# Create admin user if it doesn't exist # Create admin user if it doesn't exist
@ -45,7 +45,7 @@ class Command(BaseCommand):
self.stdout.write(f"Company already exists: {company.name}") self.stdout.write(f"Company already exists: {company.name}")
# Create users for each company # Create users for each company
for i, company in enumerate(companies): for _i, company in enumerate(companies):
# Company admin # Company admin
username = f"admin_{company.name.lower().replace(' ', '_')}" username = f"admin_{company.name.lower().replace(' ', '_')}"
if not User.objects.filter(username=username).exists(): if not User.objects.filter(username=username).exists():

View File

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

View File

@ -7,7 +7,7 @@ from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret! # 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True

View File

@ -4,277 +4,311 @@
/* Dashboard grid layout */ /* Dashboard grid layout */
.dashboard-grid { .dashboard-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
/* Slightly larger minmax for widgets */
gap: 1.5rem; /* Slightly larger minmax for widgets */
/* Increased gap */ gap: 1.5rem;
/* Increased gap */
} }
/* Dashboard widget cards */ /* Dashboard widget cards */
.dashboard-widget { .dashboard-widget {
display: flex; display: flex;
/* Allow flex for content alignment */
flex-direction: column; /* Allow flex for content alignment */
/* Stack header, body, footer vertically */ flex-direction: column;
height: 100%;
/* Ensure widgets fill grid cell height */ /* Stack header, body, footer vertically */
height: 100%;
/* Ensure widgets fill grid cell height */
} }
.dashboard-widget .card-header { .dashboard-widget .card-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.dashboard-widget .card-header .widget-title { .dashboard-widget .card-header .widget-title {
font-size: 1.1rem; font-size: 1.1rem;
/* Slightly larger widget titles */
font-weight: 600; /* Slightly larger widget titles */
font-weight: 600;
} }
.dashboard-widget .card-header .widget-actions { .dashboard-widget .card-header .widget-actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
} }
.dashboard-widget .card-header .widget-actions .btn { .dashboard-widget .card-header .widget-actions .btn {
width: 32px; width: 32px;
/* Slightly larger action buttons */
height: 32px; /* Slightly larger action buttons */
padding: 0; height: 32px;
display: flex; padding: 0;
align-items: center; display: flex;
justify-content: center; align-items: center;
font-size: 0.85rem; justify-content: center;
background-color: transparent; font-size: 0.85rem;
border: 1px solid transparent; background-color: transparent;
color: #6c757d; border: 1px solid transparent;
color: #6c757d;
} }
.dashboard-widget .card-header .widget-actions .btn:hover { .dashboard-widget .card-header .widget-actions .btn:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
border-color: #e0e0e0; border-color: #e0e0e0;
color: #333; color: #333;
} }
.dashboard-widget .card-body { .dashboard-widget .card-body {
flex-grow: 1; flex-grow: 1;
/* Allow card body to take available space */
padding: 1.25rem; /* Allow card body to take available space */
/* Consistent padding */ padding: 1.25rem;
/* Consistent padding */
} }
/* Chart widgets */ /* Chart widgets */
.chart-widget .card-body { .chart-widget .card-body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.chart-widget .chart-container { .chart-widget .chart-container {
flex: 1; flex: 1;
min-height: 250px; min-height: 250px;
/* Adjusted min-height */
width: 100%; /* Adjusted min-height */
/* Ensure it takes full width of card body */ width: 100%;
/* Ensure it takes full width of card body */
} }
/* Stat widgets / Stat Cards */ /* Stat widgets / Stat Cards */
.stat-card { .stat-card {
text-align: center; text-align: center;
padding: 1.5rem; padding: 1.5rem;
/* Generous padding */
/* Generous padding */
} }
.stat-card .stat-icon { .stat-card .stat-icon {
font-size: 2.25rem; font-size: 2.25rem;
/* Larger icon */
margin-bottom: 1rem; /* Larger icon */
display: inline-block; margin-bottom: 1rem;
width: 4.5rem; display: inline-block;
height: 4.5rem; width: 4.5rem;
line-height: 4.5rem; height: 4.5rem;
text-align: center; line-height: 4.5rem;
border-radius: 50%; text-align: center;
background-color: #e9f2ff; border-radius: 50%;
/* Light blue background for icon */ background-color: #e9f2ff;
color: #007bff;
/* Primary color for icon */ /* Light blue background for icon */
color: #007bff;
/* Primary color for icon */
} }
.stat-card .stat-value { .stat-card .stat-value {
font-size: 2.25rem; font-size: 2.25rem;
/* Larger stat value */
font-weight: 700; /* Larger stat value */
margin-bottom: 0.25rem; font-weight: 700;
/* Reduced margin */ margin-bottom: 0.25rem;
line-height: 1.1;
color: #212529; /* Reduced margin */
/* Darker color for value */ line-height: 1.1;
color: #212529;
/* Darker color for value */
} }
.stat-card .stat-label { .stat-card .stat-label {
font-size: 0.9rem; font-size: 0.9rem;
/* Slightly larger label */
color: #6c757d; /* Slightly larger label */
margin-bottom: 0; color: #6c757d;
margin-bottom: 0;
} }
/* Dashboard theme variations */ /* Dashboard theme variations */
.dashboard-theme-light .card { .dashboard-theme-light .card {
background-color: #ffffff; background-color: #fff;
} }
.dashboard-theme-dark { .dashboard-theme-dark {
background-color: #212529; background-color: #212529;
color: #f8f9fa; color: #f8f9fa;
} }
.dashboard-theme-dark .card { .dashboard-theme-dark .card {
background-color: #343a40; background-color: #343a40;
color: #f8f9fa; color: #f8f9fa;
border-color: #495057; border-color: #495057;
} }
.dashboard-theme-dark .card-header { .dashboard-theme-dark .card-header {
background-color: #495057; background-color: #495057;
border-bottom-color: #6c757d; border-bottom-color: #6c757d;
} }
.dashboard-theme-dark .stat-card .stat-label { .dashboard-theme-dark .stat-card .stat-label {
color: #adb5bd; color: #adb5bd;
} }
/* Time period selector */ /* Time period selector */
.time-period-selector { .time-period-selector {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
/* Increased gap */
margin-bottom: 1.5rem; /* Increased gap */
/* Increased margin */ margin-bottom: 1.5rem;
/* Increased margin */
} }
.time-period-selector .btn-group { .time-period-selector .btn-group {
flex-wrap: wrap; flex-wrap: wrap;
} }
.time-period-selector .btn { .time-period-selector .btn {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
/* Bootstrap-like padding */
font-size: 0.875rem; /* Bootstrap-like padding */
font-size: 0.875rem;
} }
/* Custom metric selector */ /* Custom metric selector */
.metric-selector { .metric-selector {
max-width: 100%; max-width: 100%;
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.metric-selector .nav-link { .metric-selector .nav-link {
white-space: nowrap; white-space: nowrap;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-weight: 500; font-weight: 500;
} }
.metric-selector .nav-link.active { .metric-selector .nav-link.active {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
/* Dashboard loading states */ /* Dashboard loading states */
.widget-placeholder { .widget-placeholder {
min-height: 300px; min-height: 300px;
background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%); background: linear-gradient(90deg, #e9ecef 25%, #f8f9fa 50%, #e9ecef 75%);
/* Lighter gradient */
background-size: 200% 100%; /* Lighter gradient */
animation: loading 1.8s infinite ease-in-out; background-size: 200% 100%;
/* Smoother animation */ animation: loading 1.8s infinite ease-in-out;
border-radius: 0.5rem;
/* Consistent with cards */ /* Smoother animation */
border-radius: 0.5rem;
/* Consistent with cards */
} }
@keyframes loading { @keyframes loading {
0% { 0% {
background-position: 200% 0; background-position: 200% 0;
} }
100% { 100% {
background-position: -200% 0; background-position: -200% 0;
} }
} }
/* Dashboard empty states */ /* Dashboard empty states */
.empty-state { .empty-state {
padding: 2.5rem; padding: 2.5rem;
/* Increased padding */
text-align: center; /* Increased padding */
color: #6c757d; text-align: center;
background-color: #f8f9fa; color: #6c757d;
/* Light background for empty state */ background-color: #f8f9fa;
border-radius: 0.5rem;
border: 1px dashed #ced4da; /* Light background for empty state */
/* Dashed border */ border-radius: 0.5rem;
border: 1px dashed #ced4da;
/* Dashed border */
} }
.empty-state .empty-state-icon { .empty-state .empty-state-icon {
font-size: 3.5rem; font-size: 3.5rem;
/* Larger icon */
margin-bottom: 1.5rem; /* Larger icon */
opacity: 0.4; margin-bottom: 1.5rem;
opacity: 0.4;
} }
.empty-state .empty-state-message { .empty-state .empty-state-message {
font-size: 1.2rem; font-size: 1.2rem;
/* Slightly larger message */
margin-bottom: 1.5rem; /* Slightly larger message */
font-weight: 500; margin-bottom: 1.5rem;
font-weight: 500;
} }
.empty-state .btn { .empty-state .btn {
margin-top: 1rem; margin-top: 1rem;
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 767.98px) { @media (width <=767.98px) {
.dashboard-grid { .dashboard-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.stat-card { .stat-card {
padding: 1rem; padding: 1rem;
} }
.stat-card .stat-icon { .stat-card .stat-icon {
font-size: 1.5rem; font-size: 1.5rem;
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
line-height: 3rem; line-height: 3rem;
} }
.stat-card .stat-value { .stat-card .stat-value {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
/* --- Stat Boxes Alignment Fix (Bottom Align, No Overlap) --- */ /* --- Stat Boxes Alignment Fix (Bottom Align, No Overlap) --- */
.stats-row { .stats-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1.5rem; gap: 1.5rem;
align-items: stretch; align-items: stretch;
} }
.stats-card { .stats-card {
flex: 1 1 0; flex: 1 1 0;
min-width: 200px; min-width: 200px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-end; /* Push content to bottom */ justify-content: flex-end;
align-items: flex-start;
box-sizing: border-box; /* Push content to bottom */
/* Remove min-height/height for natural stretch */ align-items: flex-start;
box-sizing: border-box;
/* Remove min-height/height for natural stretch */
} }

View File

@ -4,325 +4,361 @@
/* General Styles */ /* General Styles */
body { body {
font-family: font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
background-color: #f4f7f9; background-color: #f4f7f9;
/* Lighter, cleaner background */
color: #333; /* Lighter, cleaner background */
/* Darker text for better contrast */ color: #333;
line-height: 1.6;
display: flex; /* Darker text for better contrast */
/* Added for sticky footer */ line-height: 1.6;
flex-direction: column; display: flex;
/* Added for sticky footer */
min-height: 100vh; /* Added for sticky footer */
/* Ensures body takes at least full viewport height */ 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 adjustments (if needed, Bootstrap usually handles this well) */
.navbar { .navbar {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
/* Subtle shadow for depth */
/* Subtle shadow for depth */
} }
/* Helper Classes */ /* Helper Classes */
.text-truncate-2 { .text-truncate-2 {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
.min-w-150 { .min-w-150 {
min-width: 150px; min-width: 150px;
} }
/* Card styles */ /* Card styles */
.card { .card {
border: 1px solid #e0e5e9; border: 1px solid #e0e5e9;
/* Lighter border */
border-radius: 0.5rem; /* Lighter border */
/* Slightly more rounded corners */ border-radius: 0.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
/* Softer, more modern shadow */ /* Slightly more rounded corners */
transition: box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out; /* Softer, more modern shadow */
margin-bottom: 1.5rem; transition:
/* Consistent margin */ transform 0.2s ease-in-out,
box-shadow 0.2s ease-in-out;
margin-bottom: 1.5rem;
/* Consistent margin */
} }
.card-hover:hover { .card-hover:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 6px 16px rgb(0 0 0 / 10%);
} }
.card-header { .card-header {
background-color: #ffffff; background-color: #fff;
/* Clean white header */
border-bottom: 1px solid #e0e5e9; /* Clean white header */
font-weight: 500; border-bottom: 1px solid #e0e5e9;
/* Slightly bolder header text */ font-weight: 500;
padding: 0.75rem 1.25rem;
/* Slightly bolder header text */
padding: 0.75rem 1.25rem;
} }
.card-title { .card-title {
font-size: 1.15rem; font-size: 1.15rem;
/* Adjusted card title size */
font-weight: 600; /* Adjusted card title size */
font-weight: 600;
} }
/* Sidebar enhancements */ /* Sidebar enhancements */
.sidebar { .sidebar {
background-color: #ffffff; background-color: #fff;
/* White sidebar for a cleaner look */
border-right: 1px solid #e0e5e9; /* White sidebar for a cleaner look */
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.03); border-right: 1px solid #e0e5e9;
transition: all 0.3s; box-shadow: 2px 0 5px rgb(0 0 0 / 3%);
transition: all 0.3s;
} }
.sidebar-sticky { .sidebar-sticky {
padding-top: 1rem; padding-top: 1rem;
} }
.sidebar .nav-link { .sidebar .nav-link {
color: #4a5568; color: #4a5568;
/* Softer link color */
padding: 0.65rem 1.25rem; /* Softer link color */
/* Adjusted padding */ padding: 0.65rem 1.25rem;
border-radius: 0.375rem;
/* Bootstrap-like rounded corners for links */ /* Adjusted padding */
margin: 0.1rem 0.5rem; border-radius: 0.375rem;
/* Margin around links */
font-weight: 500; /* Bootstrap-like rounded corners for links */
margin: 0.1rem 0.5rem;
/* Margin around links */
font-weight: 500;
} }
.sidebar .nav-link:hover { .sidebar .nav-link:hover {
color: #007bff; color: #007bff;
/* Primary color on hover */
background-color: #e9f2ff; /* Primary color on hover */
/* Light blue background on hover */ background-color: #e9f2ff;
/* Light blue background on hover */
} }
.sidebar .nav-link.active { .sidebar .nav-link.active {
color: #007bff; color: #007bff;
background-color: #d6e4ff; background-color: #d6e4ff;
/* Slightly darker blue for active */
font-weight: 600; /* Slightly darker blue for active */
font-weight: 600;
} }
.sidebar .nav-link i.me-2 { .sidebar .nav-link i.me-2 {
width: 20px; width: 20px;
/* Ensure icons align well */
text-align: center; /* Ensure icons align well */
margin-right: 0.75rem !important; text-align: center;
/* Consistent icon spacing */ margin-right: 0.75rem !important;
/* Consistent icon spacing */
} }
.sidebar .nav-header { .sidebar .nav-header {
font-size: 0.8rem; font-size: 0.8rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
color: #718096; color: #718096;
/* Softer header color */
padding: 0.5rem 1.25rem; /* Softer header color */
margin-top: 1rem; padding: 0.5rem 1.25rem;
margin-top: 1rem;
} }
/* Dashboard stats cards */ /* Dashboard stats cards */
.stats-card { .stats-card {
border-radius: 0.5rem; border-radius: 0.5rem;
overflow: hidden; overflow: hidden;
} }
.stats-card h3 { .stats-card h3 {
font-size: 1.75rem; font-size: 1.75rem;
font-weight: 600; font-weight: 600;
} }
.stats-card p { .stats-card p {
font-size: 0.875rem; font-size: 0.875rem;
margin-bottom: 0; margin-bottom: 0;
opacity: 0.8; opacity: 0.8;
} }
/* Chart containers */ /* Chart containers */
.chart-container { .chart-container {
width: 100%; width: 100%;
height: 300px; height: 300px;
position: relative; position: relative;
} }
/* Loading overlay */ /* Loading overlay */
.loading-overlay { .loading-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(255, 255, 255, 0.7); background-color: rgb(255 255 255 / 70%);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 9999; z-index: 9999;
} }
/* Table enhancements */ /* Table enhancements */
.table { .table {
border-color: #e0e5e9; border-color: #e0e5e9;
} }
.table th { .table th {
font-weight: 600; font-weight: 600;
/* Bolder table headers */
color: #4a5568; /* Bolder table headers */
background-color: #f8f9fc; color: #4a5568;
/* Light background for headers */ background-color: #f8f9fc;
/* Light background for headers */
} }
.table-striped tbody tr:nth-of-type(odd) { .table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0, 0, 0, 0.02); background-color: rgb(0 0 0 / 2%);
/* Very subtle striping */
/* Very subtle striping */
} }
.table-hover tbody tr:hover { .table-hover tbody tr:hover {
background-color: #e9f2ff; background-color: #e9f2ff;
/* Consistent hover with sidebar */
/* Consistent hover with sidebar */
} }
/* Form improvements */ /* Form improvements */
.form-control, .form-control,
.form-select { .form-select {
border-color: #ced4da; border-color: #ced4da;
border-radius: 0.375rem; border-radius: 0.375rem;
/* Consistent border radius */
padding: 0.5rem 0.75rem; /* Consistent border radius */
/* Adjusted padding */ padding: 0.5rem 0.75rem;
/* Adjusted padding */
} }
.form-control:focus, .form-control:focus,
.form-select:focus { .form-select:focus {
border-color: #86b7fe; border-color: #86b7fe;
/* Bootstrap focus color */
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); /* Bootstrap focus color */
/* Bootstrap focus shadow */ box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%);
/* Bootstrap focus shadow */
} }
/* Button styling */ /* Button styling */
.btn { .btn {
border-radius: 0.375rem; border-radius: 0.375rem;
/* Consistent border radius */
padding: 0.5rem 1rem; /* Consistent border radius */
/* Standard button padding */ padding: 0.5rem 1rem;
font-weight: 500;
transition: /* Standard button padding */
background-color 0.15s ease-in-out, font-weight: 500;
border-color 0.15s ease-in-out, transition:
box-shadow 0.15s ease-in-out; background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;
} }
.btn-primary { .btn-primary {
background-color: #007bff; background-color: #007bff;
border-color: #007bff; border-color: #007bff;
} }
.btn-primary:hover { .btn-primary:hover {
background-color: #0069d9; background-color: #0069d9;
border-color: #0062cc; border-color: #0062cc;
} }
.btn-secondary { .btn-secondary {
background-color: #6c757d; background-color: #6c757d;
border-color: #6c757d; border-color: #6c757d;
} }
.btn-secondary:hover { .btn-secondary:hover {
background-color: #5a6268; background-color: #5a6268;
border-color: #545b62; border-color: #545b62;
} }
/* Alert styling */ /* Alert styling */
.alert { .alert {
border-radius: 0.375rem; border-radius: 0.375rem;
padding: 0.9rem 1.25rem; padding: 0.9rem 1.25rem;
} }
/* Chat transcript styling */ /* Chat transcript styling */
.chat-transcript { .chat-transcript {
background-color: #f8f9fa; background-color: #f8f9fa;
border: 1px solid #e9ecef; border: 1px solid #e9ecef;
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 1rem; padding: 1rem;
max-height: 500px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
font-size: 0.875rem; font-size: 0.875rem;
} }
.chat-transcript pre { .chat-transcript pre {
white-space: pre-wrap; white-space: pre-wrap;
font-family: inherit; font-family: inherit;
margin-bottom: 0; margin-bottom: 0;
} }
/* Footer styling */ /* Footer styling */
footer { footer {
background-color: #ffffff; background-color: #fff;
/* White footer */
border-top: 1px solid #e0e5e9; /* White footer */
padding: 1.5rem 0; border-top: 1px solid #e0e5e9;
color: #6c757d; padding: 1.5rem 0;
font-size: 0.9rem; color: #6c757d;
margin-top: auto; font-size: 0.9rem;
/* Added for sticky footer */ margin-top: auto;
/* Added for sticky footer */
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 767.98px) { @media (width <=767.98px) {
.main-content { .main-content {
margin-left: 0; margin-left: 0;
} }
.stats-card h3 { .stats-card h3 {
font-size: 1.5rem; font-size: 1.5rem;
} }
.chart-container { .chart-container {
height: 250px; height: 250px;
} }
.card-title { .card-title {
font-size: 1.25rem; font-size: 1.25rem;
} }
} }
/* Print styles */ /* Print styles */
@media print { @media print {
.sidebar, .sidebar,
.navbar, .navbar,
.btn, .btn,
footer { footer {
display: none !important; display: none !important;
} }
.main-content { .main-content {
margin-left: 0 !important; margin-left: 0 !important;
padding: 0 !important; padding: 0 !important;
} }
.card { .card {
break-inside: avoid; break-inside: avoid;
border: none !important; border: none !important;
box-shadow: none !important; box-shadow: none !important;
} }
.chart-container { .chart-container {
break-inside: avoid; break-inside: avoid;
height: auto !important; height: auto !important;
} }
} }

View File

@ -6,268 +6,269 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Only initialize if AJAX navigation is enabled // Only initialize if AJAX navigation is enabled
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) { if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
setupAjaxNavigation(); setupAjaxNavigation();
} }
// Function to set up AJAX navigation for the application // Function to set up AJAX navigation for the application
function setupAjaxNavigation() { function setupAjaxNavigation() {
// Configuration // Configuration
const config = { const config = {
mainContentSelector: "#main-content", // Selector for the main content area mainContentSelector: "#main-content", // Selector for the main content area
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
excludePatterns: [ excludePatterns: [
// URL patterns to exclude from AJAX navigation // URL patterns to exclude from AJAX navigation
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads /\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
/\/admin\//, // Admin pages /\/admin\//, // Admin pages
/\/accounts\/logout\//, // Logout page /\/accounts\/logout\//, // Logout page
/\/api\//, // API endpoints /\/api\//, // API endpoints
], ],
}; };
// Create and insert the loading indicator // Create and insert the loading indicator
if (!document.getElementById(config.loadingIndicatorId)) { if (!document.getElementById(config.loadingIndicatorId)) {
const loadingIndicator = document.createElement("div"); const loadingIndicator = document.createElement("div");
loadingIndicator.id = config.loadingIndicatorId; loadingIndicator.id = config.loadingIndicatorId;
loadingIndicator.className = "position-fixed top-0 start-0 end-0"; loadingIndicator.className = "position-fixed top-0 start-0 end-0";
loadingIndicator.innerHTML = 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>'; '<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.display = "none";
loadingIndicator.style.zIndex = "9999"; loadingIndicator.style.zIndex = "9999";
document.body.appendChild(loadingIndicator); document.body.appendChild(loadingIndicator);
} }
// Get the loading indicator element // Get the loading indicator element
const loadingIndicator = document.getElementById(config.loadingIndicatorId); const loadingIndicator = document.getElementById(config.loadingIndicatorId);
// Get the main content container // Get the main content container
const mainContent = document.querySelector(config.mainContentSelector); const mainContent = document.querySelector(config.mainContentSelector);
if (!mainContent) { if (!mainContent) {
console.warn("Main content container not found. AJAX navigation disabled."); console.warn("Main content container not found. AJAX navigation disabled.");
return; return;
} }
// Function to check if a URL should be excluded from AJAX navigation // Function to check if a URL should be excluded from AJAX navigation
function shouldExcludeUrl(url) { function shouldExcludeUrl(url) {
for (const pattern of config.excludePatterns) { for (const pattern of config.excludePatterns) {
if (pattern.test(url)) { if (pattern.test(url)) {
return true; return true;
} }
} }
return false; return false;
} }
// Function to show the loading indicator // Function to show the loading indicator
function showLoading() { function showLoading() {
loadingIndicator.style.display = "block"; loadingIndicator.style.display = "block";
} }
// Function to hide the loading indicator // Function to hide the loading indicator
function hideLoading() { function hideLoading() {
loadingIndicator.style.display = "none"; loadingIndicator.style.display = "none";
} }
// Function to handle AJAX page navigation // Function to handle AJAX page navigation
function handlePageNavigation(url, pushState = true) { function handlePageNavigation(url, pushState = true) {
if (shouldExcludeUrl(url)) { if (shouldExcludeUrl(url)) {
window.location.href = url; window.location.href = url;
return; return;
} }
showLoading(); showLoading();
const currentScrollPos = window.scrollY; const currentScrollPos = window.scrollY;
fetch(url, { fetch(url, {
headers: { headers: {
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
"X-AJAX-Navigation": "true", "X-AJAX-Navigation": "true",
Accept: "text/html", Accept: "text/html",
}, },
}) })
.then((response) => { .then((response) => {
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`); if (!response.ok)
return response.text(); throw new Error(`Network response was not ok: ${response.status}`);
}) return response.text();
.then((html) => { })
// Parse the HTML and extract #main-content .then((html) => {
const tempDiv = document.createElement("div"); // Parse the HTML and extract #main-content
tempDiv.innerHTML = html; const tempDiv = document.createElement("div");
const newContent = tempDiv.querySelector(config.mainContentSelector); tempDiv.innerHTML = html;
if (!newContent) throw new Error("Could not find main content in the response"); const newContent = tempDiv.querySelector(config.mainContentSelector);
mainContent.innerHTML = newContent.innerHTML; if (!newContent) throw new Error("Could not find main content in the response");
// Update the page title mainContent.innerHTML = newContent.innerHTML;
const titleMatch = html.match(/<title>(.*?)<\/title>/i); // Update the page title
if (titleMatch) document.title = titleMatch[1]; const titleMatch = html.match(/<title>(.*?)<\/title>/i);
// Re-initialize dynamic content if (titleMatch) document.title = titleMatch[1];
reloadScripts(mainContent); // Re-initialize dynamic content
attachEventListeners(); reloadScripts(mainContent);
initializePageScripts(); attachEventListeners();
if (pushState) { initializePageScripts();
history.pushState( if (pushState) {
{ url: url, title: document.title, scrollPos: currentScrollPos }, history.pushState(
document.title, { url: url, title: document.title, scrollPos: currentScrollPos },
url document.title,
); url,
window.scrollTo({ top: 0, behavior: "smooth" }); );
} else if (window.history.state && window.history.state.scrollPos) { window.scrollTo({ top: 0, behavior: "smooth" });
window.scrollTo({ top: window.history.state.scrollPos }); } else if (window.history.state && window.history.state.scrollPos) {
} window.scrollTo({ top: window.history.state.scrollPos });
hideLoading(); }
}) hideLoading();
.catch((error) => { })
console.error("Error during AJAX navigation:", error); .catch((error) => {
hideLoading(); console.error("Error during AJAX navigation:", error);
window.location.href = url; hideLoading();
}); window.location.href = url;
} });
}
// Function to reload and execute scripts in new content // Function to reload and execute scripts in new content
function reloadScripts(container) { function reloadScripts(container) {
const scripts = container.getElementsByTagName("script"); const scripts = container.getElementsByTagName("script");
for (let script of scripts) { for (let script of scripts) {
const newScript = document.createElement("script"); const newScript = document.createElement("script");
// Copy all attributes // Copy all attributes
Array.from(script.attributes).forEach((attr) => { Array.from(script.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value); newScript.setAttribute(attr.name, attr.value);
}); });
// Copy inline script content // Copy inline script content
newScript.textContent = script.textContent; newScript.textContent = script.textContent;
// Replace old script with new one // Replace old script with new one
script.parentNode.replaceChild(newScript, script); script.parentNode.replaceChild(newScript, script);
} }
} }
// Function to handle form submissions // Function to handle form submissions
function handleFormSubmission(form, e) { function handleFormSubmission(form, e) {
e.preventDefault(); e.preventDefault();
// Show loading indicator // Show loading indicator
showLoading(); showLoading();
// Get form data // Get form data
const formData = new FormData(form); const formData = new FormData(form);
const method = form.method.toLowerCase(); const method = form.method.toLowerCase();
const url = form.action || window.location.href; const url = form.action || window.location.href;
// Configure fetch options // Configure fetch options
const fetchOptions = { const fetchOptions = {
method: method, method: method,
headers: { headers: {
"X-AJAX-Navigation": "true", "X-AJAX-Navigation": "true",
}, },
}; };
// Handle different HTTP methods // Handle different HTTP methods
if (method === "get") { if (method === "get") {
const queryParams = new URLSearchParams(formData).toString(); const queryParams = new URLSearchParams(formData).toString();
handlePageNavigation(url + (queryParams ? "?" + queryParams : "")); handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
} else { } else {
fetchOptions.body = formData; fetchOptions.body = formData;
fetch(url, fetchOptions) fetch(url, fetchOptions)
.then((response) => { .then((response) => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.json(); return response.json();
}) })
.then((data) => { .then((data) => {
if (data.redirect) { if (data.redirect) {
// Handle server-side redirects // Handle server-side redirects
handlePageNavigation(data.redirect, true); handlePageNavigation(data.redirect, true);
} else { } else {
// Update page content // Update page content
mainContent.innerHTML = data.html; mainContent.innerHTML = data.html;
document.title = data.title || document.title; document.title = data.title || document.title;
// Re-initialize dynamic content // Re-initialize dynamic content
reloadScripts(mainContent); reloadScripts(mainContent);
attachEventListeners(); attachEventListeners();
initializePageScripts(); initializePageScripts();
// Update URL if needed // Update URL if needed
if (data.url) { if (data.url) {
history.pushState({ url: data.url }, document.title, data.url); history.pushState({ url: data.url }, document.title, data.url);
} }
} }
}) })
.catch((error) => { .catch((error) => {
console.error("Form submission error:", error); console.error("Form submission error:", error);
// Fallback to traditional form submission // Fallback to traditional form submission
form.submit(); form.submit();
}) })
.finally(() => { .finally(() => {
hideLoading(); hideLoading();
}); });
} }
} }
// Function to initialize scripts needed for the new page content // Function to initialize scripts needed for the new page content
function initializePageScripts() { function initializePageScripts() {
// Re-initialize any custom scripts that might be needed // Re-initialize any custom scripts that might be needed
if (typeof setupAjaxPagination === "function") { if (typeof setupAjaxPagination === "function") {
setupAjaxPagination(); setupAjaxPagination();
} }
// Initialize Bootstrap tooltips, popovers, etc. // Initialize Bootstrap tooltips, popovers, etc.
if (typeof bootstrap !== "undefined") { if (typeof bootstrap !== "undefined") {
// Initialize tooltips // Initialize tooltips
const tooltipTriggerList = [].slice.call( const tooltipTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="tooltip"]') document.querySelectorAll('[data-bs-toggle="tooltip"]'),
); );
tooltipTriggerList.map(function (tooltipTriggerEl) { tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl); return new bootstrap.Tooltip(tooltipTriggerEl);
}); });
// Initialize popovers // Initialize popovers
const popoverTriggerList = [].slice.call( const popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]') document.querySelectorAll('[data-bs-toggle="popover"]'),
); );
popoverTriggerList.map(function (popoverTriggerEl) { popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl); return new bootstrap.Popover(popoverTriggerEl);
}); });
} }
} }
// Function to attach event listeners to forms and links // Function to attach event listeners to forms and links
function attachEventListeners() { function attachEventListeners() {
// Handle AJAX navigation links // Handle AJAX navigation links
document.querySelectorAll(config.navLinkSelector).forEach((link) => { document.querySelectorAll(config.navLinkSelector).forEach((link) => {
if (!link.dataset.ajaxNavInitialized) { if (!link.dataset.ajaxNavInitialized) {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) { if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
return; // Let the browser handle these cases return; // Let the browser handle these cases
} }
e.preventDefault(); e.preventDefault();
handlePageNavigation(this.href); handlePageNavigation(this.href);
}); });
link.dataset.ajaxNavInitialized = "true"; link.dataset.ajaxNavInitialized = "true";
} }
}); });
// Handle forms with AJAX // Handle forms with AJAX
document document
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form") .querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
.forEach((form) => { .forEach((form) => {
if (!form.dataset.ajaxFormInitialized) { if (!form.dataset.ajaxFormInitialized) {
form.addEventListener("submit", (e) => handleFormSubmission(form, e)); form.addEventListener("submit", (e) => handleFormSubmission(form, e));
form.dataset.ajaxFormInitialized = "true"; form.dataset.ajaxFormInitialized = "true";
} }
}); });
} }
// Initial attachment of event listeners // Initial attachment of event listeners
attachEventListeners(); attachEventListeners();
// Handle browser back/forward buttons // Handle browser back/forward buttons
window.addEventListener("popstate", function (event) { window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) { if (event.state && event.state.url) {
handlePageNavigation(event.state.url, false); handlePageNavigation(event.state.url, false);
} else { } else {
// Fallback to current URL if no state // Fallback to current URL if no state
handlePageNavigation(window.location.href, false); handlePageNavigation(window.location.href, false);
} }
}); });
} }
}); });

View File

@ -6,101 +6,101 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Initialize AJAX pagination // Initialize AJAX pagination
setupAjaxPagination(); setupAjaxPagination();
// Function to set up AJAX pagination for the entire application // Function to set up AJAX pagination for the entire application
function setupAjaxPagination() { function setupAjaxPagination() {
// Configuration - can be customized per page if needed // Configuration - can be customized per page if needed
const config = { const config = {
contentContainerId: "ajax-content-container", // ID of the container to update contentContainerId: "ajax-content-container", // ID of the container to update
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
paginationLinkClass: "pagination-link", // Class for pagination links paginationLinkClass: "pagination-link", // Class for pagination links
retryMessage: "An error occurred while loading data. Please try again.", retryMessage: "An error occurred while loading data. Please try again.",
}; };
// Get container elements // Get container elements
const contentContainer = document.getElementById(config.contentContainerId); const contentContainer = document.getElementById(config.contentContainerId);
const loadingSpinner = document.getElementById(config.loadingSpinnerId); const loadingSpinner = document.getElementById(config.loadingSpinnerId);
// Exit if the page doesn't have the required elements // Exit if the page doesn't have the required elements
if (!contentContainer || !loadingSpinner) return; if (!contentContainer || !loadingSpinner) return;
// Function to handle pagination clicks // Function to handle pagination clicks
function setupPaginationListeners() { function setupPaginationListeners() {
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => { document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
handleAjaxNavigation(this.href); handleAjaxNavigation(this.href);
// Get the page number if available // Get the page number if available
const page = this.getAttribute("data-page"); const page = this.getAttribute("data-page");
// Update browser URL without refreshing // Update browser URL without refreshing
const newUrl = this.href; const newUrl = this.href;
history.pushState({ url: newUrl, page: page }, "", newUrl); history.pushState({ url: newUrl, page: page }, "", newUrl);
}); });
}); });
} }
// Function to handle AJAX navigation // Function to handle AJAX navigation
function handleAjaxNavigation(url) { function handleAjaxNavigation(url) {
// Show loading spinner // Show loading spinner
contentContainer.classList.add("d-none"); contentContainer.classList.add("d-none");
loadingSpinner.classList.remove("d-none"); loadingSpinner.classList.remove("d-none");
// Fetch data via AJAX // Fetch data via AJAX
fetch(url, { fetch(url, {
headers: { headers: {
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
}, },
}) })
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`); throw new Error(`Network response was not ok: ${response.status}`);
} }
return response.json(); return response.json();
}) })
.then((data) => { .then((data) => {
if (data.status === "success") { if (data.status === "success") {
// Update the content // Update the content
contentContainer.innerHTML = data.html_data; contentContainer.innerHTML = data.html_data;
// Re-attach event listeners to new pagination links // Re-attach event listeners to new pagination links
setupPaginationListeners(); setupPaginationListeners();
// Update any summary data if present and the page provides it // Update any summary data if present and the page provides it
if (typeof updateSummary === "function" && data.summary) { if (typeof updateSummary === "function" && data.summary) {
updateSummary(data); updateSummary(data);
} }
// Hide loading spinner, show content // Hide loading spinner, show content
loadingSpinner.classList.add("d-none"); loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none"); contentContainer.classList.remove("d-none");
// Scroll to top of the content container // Scroll to top of the content container
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" }); contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
} }
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
loadingSpinner.classList.add("d-none"); loadingSpinner.classList.add("d-none");
contentContainer.classList.remove("d-none"); contentContainer.classList.remove("d-none");
alert(config.retryMessage); alert(config.retryMessage);
}); });
} }
// Initial setup of event listeners // Initial setup of event listeners
setupPaginationListeners(); setupPaginationListeners();
// Handle browser back/forward buttons // Handle browser back/forward buttons
window.addEventListener("popstate", function (event) { window.addEventListener("popstate", function (event) {
if (event.state && event.state.url) { if (event.state && event.state.url) {
handleAjaxNavigation(event.state.url); handleAjaxNavigation(event.state.url);
} else { } else {
// If no state, fetch current URL // If no state, fetch current URL
handleAjaxNavigation(window.location.href); handleAjaxNavigation(window.location.href);
} }
}); });
} }
}); });

View File

@ -7,272 +7,272 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Chart responsiveness // Chart responsiveness
function resizeCharts() { function resizeCharts() {
const charts = document.querySelectorAll(".chart-container"); const charts = document.querySelectorAll(".chart-container");
charts.forEach((chart) => { charts.forEach((chart) => {
if (chart.id && window.Plotly) { if (chart.id && window.Plotly) {
Plotly.relayout(chart.id, { Plotly.relayout(chart.id, {
"xaxis.automargin": true, "xaxis.automargin": true,
"yaxis.automargin": true, "yaxis.automargin": true,
}); });
} }
}); });
} }
// Handle window resize // Handle window resize
window.addEventListener("resize", function () { window.addEventListener("resize", function () {
if (window.Plotly) { if (window.Plotly) {
resizeCharts(); resizeCharts();
} }
}); });
// Time range filtering // Time range filtering
const timeRangeDropdown = document.getElementById("timeRangeDropdown"); const timeRangeDropdown = document.getElementById("timeRangeDropdown");
if (timeRangeDropdown) { if (timeRangeDropdown) {
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item"); const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
timeRangeLinks.forEach((link) => { timeRangeLinks.forEach((link) => {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
const url = new URL(this.href); const url = new URL(this.href);
const dashboardId = url.searchParams.get("dashboard_id"); const dashboardId = url.searchParams.get("dashboard_id");
const timeRange = url.searchParams.get("time_range"); const timeRange = url.searchParams.get("time_range");
// Fetch updated data via AJAX // Fetch updated data via AJAX
if (dashboardId) { if (dashboardId) {
fetchDashboardData(dashboardId, timeRange); fetchDashboardData(dashboardId, timeRange);
e.preventDefault(); e.preventDefault();
} }
}); });
}); });
} }
// Function to fetch dashboard data // Function to fetch dashboard data
function fetchDashboardData(dashboardId, timeRange) { function fetchDashboardData(dashboardId, timeRange) {
const loadingOverlay = document.createElement("div"); const loadingOverlay = document.createElement("div");
loadingOverlay.className = "loading-overlay"; loadingOverlay.className = "loading-overlay";
loadingOverlay.innerHTML = loadingOverlay.innerHTML =
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>'; '<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
document.querySelector("main").appendChild(loadingOverlay); document.querySelector("main").appendChild(loadingOverlay);
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`) fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
throw new Error(`Network response was not ok: ${response.status}`); throw new Error(`Network response was not ok: ${response.status}`);
} }
return response.json(); return response.json();
}) })
.then((data) => { .then((data) => {
console.log("Dashboard API response:", data); console.log("Dashboard API response:", data);
updateDashboardStats(data); updateDashboardStats(data);
updateDashboardCharts(data); updateDashboardCharts(data);
// Update URL without page reload // Update URL without page reload
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.set("dashboard_id", dashboardId); url.searchParams.set("dashboard_id", dashboardId);
if (timeRange) { if (timeRange) {
url.searchParams.set("time_range", timeRange); url.searchParams.set("time_range", timeRange);
} }
window.history.pushState({}, "", url); window.history.pushState({}, "", url);
document.querySelector(".loading-overlay").remove(); document.querySelector(".loading-overlay").remove();
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching dashboard data:", error); console.error("Error fetching dashboard data:", error);
document.querySelector(".loading-overlay").remove(); document.querySelector(".loading-overlay").remove();
// Show error message // Show error message
const alertElement = document.createElement("div"); const alertElement = document.createElement("div");
alertElement.className = "alert alert-danger alert-dismissible fade show"; alertElement.className = "alert alert-danger alert-dismissible fade show";
alertElement.setAttribute("role", "alert"); alertElement.setAttribute("role", "alert");
alertElement.innerHTML = ` alertElement.innerHTML = `
Error loading dashboard data. Please try again. Error loading dashboard data. Please try again.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <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 to update dashboard statistics
function updateDashboardStats(data) { function updateDashboardStats(data) {
// Update total sessions // Update total sessions
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3"); const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
if (totalSessionsElement) { if (totalSessionsElement) {
totalSessionsElement.textContent = data.total_sessions; totalSessionsElement.textContent = data.total_sessions;
} }
// Update average response time // Update average response time
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3"); const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
if (avgResponseTimeElement) { if (avgResponseTimeElement) {
avgResponseTimeElement.textContent = data.avg_response_time + "s"; avgResponseTimeElement.textContent = data.avg_response_time + "s";
} }
// Update total tokens // Update total tokens
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3"); const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
if (totalTokensElement) { if (totalTokensElement) {
totalTokensElement.textContent = data.total_tokens; totalTokensElement.textContent = data.total_tokens;
} }
// Update total cost // Update total cost
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3"); const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
if (totalCostElement) { if (totalCostElement) {
totalCostElement.textContent = "€" + data.total_cost; totalCostElement.textContent = "€" + data.total_cost;
} }
} }
// Function to update dashboard charts // Function to update dashboard charts
function updateDashboardCharts(data) { function updateDashboardCharts(data) {
// Check if Plotly is available // Check if Plotly is available
if (!window.Plotly) { if (!window.Plotly) {
console.error("Plotly library not loaded!"); console.error("Plotly library not loaded!");
document.querySelectorAll(".chart-container").forEach((container) => { document.querySelectorAll(".chart-container").forEach((container) => {
container.innerHTML = container.innerHTML =
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>'; '<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
}); });
return; return;
} }
// Update sessions over time chart // Update sessions over time chart
const timeSeriesData = data.time_series_data; const timeSeriesData = data.time_series_data;
if (timeSeriesData && timeSeriesData.length > 0) { if (timeSeriesData && timeSeriesData.length > 0) {
try { try {
const timeSeriesX = timeSeriesData.map((item) => item.date); const timeSeriesX = timeSeriesData.map((item) => item.date);
const timeSeriesY = timeSeriesData.map((item) => item.count); const timeSeriesY = timeSeriesData.map((item) => item.count);
Plotly.react( Plotly.react(
"sessions-time-chart", "sessions-time-chart",
[ [
{ {
x: timeSeriesX, x: timeSeriesX,
y: timeSeriesY, y: timeSeriesY,
type: "scatter", type: "scatter",
mode: "lines+markers", mode: "lines+markers",
line: { line: {
color: "rgb(75, 192, 192)", color: "rgb(75, 192, 192)",
width: 2, width: 2,
}, },
marker: { marker: {
color: "rgb(75, 192, 192)", color: "rgb(75, 192, 192)",
size: 6, size: 6,
}, },
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 40, l: 40 }, margin: { t: 10, r: 10, b: 40, l: 40 },
xaxis: { xaxis: {
title: "Date", title: "Date",
}, },
yaxis: { yaxis: {
title: "Number of Sessions", title: "Number of Sessions",
}, },
} },
); );
} catch (error) { } catch (error) {
console.error("Error rendering time series chart:", error); console.error("Error rendering time series chart:", error);
document.getElementById("sessions-time-chart").innerHTML = document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>'; '<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
} }
} else { } else {
document.getElementById("sessions-time-chart").innerHTML = document.getElementById("sessions-time-chart").innerHTML =
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>'; '<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
} }
// Update sentiment chart // Update sentiment chart
const sentimentData = data.sentiment_data; const sentimentData = data.sentiment_data;
if (sentimentData && sentimentData.length > 0 && window.Plotly) { if (sentimentData && sentimentData.length > 0 && window.Plotly) {
const sentimentLabels = sentimentData.map((item) => item.sentiment); const sentimentLabels = sentimentData.map((item) => item.sentiment);
const sentimentValues = sentimentData.map((item) => item.count); const sentimentValues = sentimentData.map((item) => item.count);
const sentimentColors = sentimentLabels.map((sentiment) => { const sentimentColors = sentimentLabels.map((sentiment) => {
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)"; if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)"; if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)"; if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
return "rgb(201, 203, 207)"; return "rgb(201, 203, 207)";
}); });
Plotly.react( Plotly.react(
"sentiment-chart", "sentiment-chart",
[ [
{ {
values: sentimentValues, values: sentimentValues,
labels: sentimentLabels, labels: sentimentLabels,
type: "pie", type: "pie",
marker: { marker: {
colors: sentimentColors, colors: sentimentColors,
}, },
hole: 0.4, hole: 0.4,
textinfo: "label+percent", textinfo: "label+percent",
insidetextorientation: "radial", insidetextorientation: "radial",
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 10, l: 10 }, margin: { t: 10, r: 10, b: 10, l: 10 },
} },
); );
} }
// Update country chart // Update country chart
const countryData = data.country_data; const countryData = data.country_data;
if (countryData && countryData.length > 0 && window.Plotly) { if (countryData && countryData.length > 0 && window.Plotly) {
const countryLabels = countryData.map((item) => item.country); const countryLabels = countryData.map((item) => item.country);
const countryValues = countryData.map((item) => item.count); const countryValues = countryData.map((item) => item.count);
Plotly.react( Plotly.react(
"country-chart", "country-chart",
[ [
{ {
x: countryValues, x: countryValues,
y: countryLabels, y: countryLabels,
type: "bar", type: "bar",
orientation: "h", orientation: "h",
marker: { marker: {
color: "rgb(54, 162, 235)", color: "rgb(54, 162, 235)",
}, },
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 40, l: 100 }, margin: { t: 10, r: 10, b: 40, l: 100 },
xaxis: { xaxis: {
title: "Number of Sessions", title: "Number of Sessions",
}, },
} },
); );
} }
// Update category chart // Update category chart
const categoryData = data.category_data; const categoryData = data.category_data;
if (categoryData && categoryData.length > 0 && window.Plotly) { if (categoryData && categoryData.length > 0 && window.Plotly) {
const categoryLabels = categoryData.map((item) => item.category); const categoryLabels = categoryData.map((item) => item.category);
const categoryValues = categoryData.map((item) => item.count); const categoryValues = categoryData.map((item) => item.count);
Plotly.react( Plotly.react(
"category-chart", "category-chart",
[ [
{ {
labels: categoryLabels, labels: categoryLabels,
values: categoryValues, values: categoryValues,
type: "pie", type: "pie",
textinfo: "label+percent", textinfo: "label+percent",
insidetextorientation: "radial", insidetextorientation: "radial",
}, },
], ],
{ {
margin: { t: 10, r: 10, b: 10, l: 10 }, margin: { t: 10, r: 10, b: 10, l: 10 },
} },
); );
} }
} }
// Dashboard selector // Dashboard selector
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]'); const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
dashboardSelector.forEach((link) => { dashboardSelector.forEach((link) => {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
const url = new URL(this.href); const url = new URL(this.href);
const dashboardId = url.searchParams.get("dashboard_id"); const dashboardId = url.searchParams.get("dashboard_id");
// Fetch updated data via AJAX // Fetch updated data via AJAX
if (dashboardId) { if (dashboardId) {
fetchDashboardData(dashboardId); fetchDashboardData(dashboardId);
e.preventDefault(); e.preventDefault();
} }
}); });
}); });
}); });

View File

@ -6,147 +6,147 @@
*/ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Initialize tooltips // Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl); return new bootstrap.Tooltip(tooltipTriggerEl);
}); });
// Initialize popovers // Initialize popovers
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl); return new bootstrap.Popover(popoverTriggerEl);
}); });
// Toggle sidebar on mobile // Toggle sidebar on mobile
const sidebarToggle = document.querySelector("#sidebarToggle"); const sidebarToggle = document.querySelector("#sidebarToggle");
if (sidebarToggle) { if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () { sidebarToggle.addEventListener("click", function () {
document.querySelector(".sidebar").classList.toggle("show"); document.querySelector(".sidebar").classList.toggle("show");
}); });
} }
// Auto-dismiss alerts after 5 seconds // Auto-dismiss alerts after 5 seconds
setTimeout(function () { setTimeout(function () {
var alerts = document.querySelectorAll(".alert:not(.alert-important)"); var alerts = document.querySelectorAll(".alert:not(.alert-important)");
alerts.forEach(function (alert) { alerts.forEach(function (alert) {
if (alert && bootstrap.Alert.getInstance(alert)) { if (alert && bootstrap.Alert.getInstance(alert)) {
bootstrap.Alert.getInstance(alert).close(); bootstrap.Alert.getInstance(alert).close();
} }
}); });
}, 5000); }, 5000);
// Form validation // Form validation
const forms = document.querySelectorAll(".needs-validation"); const forms = document.querySelectorAll(".needs-validation");
forms.forEach(function (form) { forms.forEach(function (form) {
form.addEventListener( form.addEventListener(
"submit", "submit",
function (event) { function (event) {
if (!form.checkValidity()) { if (!form.checkValidity()) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
form.classList.add("was-validated"); form.classList.add("was-validated");
}, },
false false,
); );
}); });
// Confirm dialogs // Confirm dialogs
const confirmButtons = document.querySelectorAll("[data-confirm]"); const confirmButtons = document.querySelectorAll("[data-confirm]");
confirmButtons.forEach(function (button) { confirmButtons.forEach(function (button) {
button.addEventListener("click", function (event) { button.addEventListener("click", function (event) {
if (!confirm(this.dataset.confirm || "Are you sure?")) { if (!confirm(this.dataset.confirm || "Are you sure?")) {
event.preventDefault(); event.preventDefault();
} }
}); });
}); });
// Back button // Back button
const backButtons = document.querySelectorAll(".btn-back"); const backButtons = document.querySelectorAll(".btn-back");
backButtons.forEach(function (button) { backButtons.forEach(function (button) {
button.addEventListener("click", function (event) { button.addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
window.history.back(); window.history.back();
}); });
}); });
// File input customization // File input customization
const fileInputs = document.querySelectorAll(".custom-file-input"); const fileInputs = document.querySelectorAll(".custom-file-input");
fileInputs.forEach(function (input) { fileInputs.forEach(function (input) {
input.addEventListener("change", function (e) { input.addEventListener("change", function (e) {
const fileName = this.files[0]?.name || "Choose file"; const fileName = this.files[0]?.name || "Choose file";
const nextSibling = this.nextElementSibling; const nextSibling = this.nextElementSibling;
if (nextSibling) { if (nextSibling) {
nextSibling.innerText = fileName; nextSibling.innerText = fileName;
} }
}); });
}); });
// Search form submit on enter // Search form submit on enter
const searchInputs = document.querySelectorAll(".search-input"); const searchInputs = document.querySelectorAll(".search-input");
searchInputs.forEach(function (input) { searchInputs.forEach(function (input) {
input.addEventListener("keypress", function (e) { input.addEventListener("keypress", function (e) {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
this.closest("form").submit(); this.closest("form").submit();
} }
}); });
}); });
// Toggle password visibility // Toggle password visibility
const togglePasswordButtons = document.querySelectorAll(".toggle-password"); const togglePasswordButtons = document.querySelectorAll(".toggle-password");
togglePasswordButtons.forEach(function (button) { togglePasswordButtons.forEach(function (button) {
button.addEventListener("click", function () { button.addEventListener("click", function () {
const target = document.querySelector(this.dataset.target); const target = document.querySelector(this.dataset.target);
if (target) { if (target) {
const type = target.getAttribute("type") === "password" ? "text" : "password"; const type = target.getAttribute("type") === "password" ? "text" : "password";
target.setAttribute("type", type); target.setAttribute("type", type);
this.querySelector("i").classList.toggle("fa-eye"); this.querySelector("i").classList.toggle("fa-eye");
this.querySelector("i").classList.toggle("fa-eye-slash"); this.querySelector("i").classList.toggle("fa-eye-slash");
} }
}); });
}); });
// Dropdown menu positioning // Dropdown menu positioning
const dropdowns = document.querySelectorAll(".dropdown-menu"); const dropdowns = document.querySelectorAll(".dropdown-menu");
dropdowns.forEach(function (dropdown) { dropdowns.forEach(function (dropdown) {
dropdown.addEventListener("click", function (e) { dropdown.addEventListener("click", function (e) {
e.stopPropagation(); e.stopPropagation();
}); });
}); });
// Responsive table handling // Responsive table handling
const tables = document.querySelectorAll(".table-responsive"); const tables = document.querySelectorAll(".table-responsive");
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
tables.forEach(function (table) { tables.forEach(function (table) {
table.classList.add("table-responsive-force"); table.classList.add("table-responsive-force");
}); });
} }
// Handle special links (printable views, exports) // Handle special links (printable views, exports)
const printLinks = document.querySelectorAll(".print-link"); const printLinks = document.querySelectorAll(".print-link");
printLinks.forEach(function (link) { printLinks.forEach(function (link) {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
e.preventDefault(); e.preventDefault();
window.print(); window.print();
}); });
}); });
const exportLinks = document.querySelectorAll("[data-export]"); const exportLinks = document.querySelectorAll("[data-export]");
exportLinks.forEach(function (link) { exportLinks.forEach(function (link) {
link.addEventListener("click", function (e) { link.addEventListener("click", function (e) {
// Handle export functionality if needed // Handle export functionality if needed
console.log("Export requested:", this.dataset.export); console.log("Export requested:", this.dataset.export);
}); });
}); });
// Handle sidebar collapse on small screens // Handle sidebar collapse on small screens
function handleSidebarOnResize() { function handleSidebarOnResize() {
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
document.querySelector(".sidebar")?.classList.remove("show"); 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 title %}Login | Chat Analytics{% endblock %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="card mt-4"> <div class="card mt-4">
<div class="card-header"> <div class="card-header">
<h4 class="card-title mb-0">Login</h4> <h4 class="card-title mb-0">Login</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Login</button> <button type="submit" class="btn btn-primary">Login</button>
</div> </div>
</form> </form>
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
<p class="mb-0">Don't have an account? <a href="{% url 'register' %}">Register</a></p> <p class="mb-0">
</div> Don't have an account? <a href="{% url 'register' %}">Register</a>
</div> </p>
</div> </div>
</div> </div>
</div>
</div>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ services:
- DEBUG=0 - DEBUG=0
- SECRET_KEY=your_secret_key_here - SECRET_KEY=your_secret_key_here
- ALLOWED_HOSTS=localhost,127.0.0.1 - ALLOWED_HOSTS=localhost,127.0.0.1
- DJANGO_SETTINGS_MODULE=dashboard_project.settings
depends_on: depends_on:
- db - db
@ -27,6 +28,8 @@ services:
- POSTGRES_USER=postgres - POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres - POSTGRES_PASSWORD=postgres
- POSTGRES_DB=dashboard_db - POSTGRES_DB=dashboard_db
ports:
- "5432:5432"
nginx: nginx:
image: nginx:latest 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": { "name": "livegraphsdjango",
"prettier": "^3.5.3", "version": "0.1.0",
"prettier-plugin-jinja-template": "^2.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] [project]
name = "livegraphsdjango" name = "livegraphsdjango"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Live Graphs Django Dashboard"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" 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 = [ dependencies = [
"crispy-bootstrap5>=2025.4", "crispy-bootstrap5>=2025.4",
"django>=5.2.1", "django>=5.2.1",
@ -17,16 +29,29 @@ dependencies = [
"whitenoise>=6.9.0", "whitenoise>=6.9.0",
] ]
[project.scripts] [dependency-groups]
# Django management commands dev = [
"manage" = "dashboard_project.manage:main" "bandit>=1.8.3",
"runserver" = "dashboard_project.manage:main" "black>=25.1.0",
"migrate" = "dashboard_project.manage:main" "coverage>=7.8.0",
"makemigrations" = "dashboard_project.manage:main" "django-debug-toolbar>=5.2.0",
"collectstatic" = "dashboard_project.manage:main" "django-stubs>=5.2.0",
"createsuperuser" = "dashboard_project.manage:main" "mypy>=1.15.0",
"shell" = "dashboard_project.manage:main" "pre-commit>=4.2.0",
"test" = "dashboard_project.manage:main" "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] [tool.ruff]
# Exclude a variety of commonly ignored directories. # Exclude a variety of commonly ignored directories.
@ -67,7 +92,7 @@ indent-width = 4
target-version = "py313" target-version = "py313"
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "I"] select = ["E", "F", "I", "B", "C4", "ARG", "SIM", "PERF"]
ignore = ["E501"] ignore = ["E501"]
fixable = ["ALL"] fixable = ["ALL"]
unfixable = [] unfixable = []
@ -76,3 +101,55 @@ unfixable = []
quote-style = "double" quote-style = "double"
indent-style = "space" indent-style = "space"
line-ending = "lf" 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" }, { 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]] [[package]]
name = "crispy-bootstrap5" name = "crispy-bootstrap5"
version = "2025.4" 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" }, { 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]] [[package]]
name = "django" name = "django"
version = "5.2.1" 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" }, { 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]] [[package]]
name = "gunicorn" name = "gunicorn"
version = "23.0.0" 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" }, { 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]] [[package]]
name = "livegraphsdjango" name = "livegraphsdjango"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "crispy-bootstrap5" }, { name = "crispy-bootstrap5" },
{ name = "django" }, { name = "django" },
@ -89,6 +261,20 @@ dependencies = [
{ name = "whitenoise" }, { 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] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "crispy-bootstrap5", specifier = ">=2025.4" }, { name = "crispy-bootstrap5", specifier = ">=2025.4" },
@ -103,6 +289,69 @@ requires-dist = [
{ name = "whitenoise", specifier = ">=6.9.0" }, { 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]] [[package]]
name = "narwhals" name = "narwhals"
version = "1.39.1" 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" }, { 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]] [[package]]
name = "numpy" name = "numpy"
version = "2.2.5" 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" }, { 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]] [[package]]
name = "plotly" name = "plotly"
version = "6.1.0" 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" }, { 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]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" 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" }, { 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]] [[package]]
name = "six" name = "six"
version = "1.17.0" 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" }, { 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]] [[package]]
name = "tzdata" name = "tzdata"
version = "2025.2" 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" }, { 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]] [[package]]
name = "whitenoise" name = "whitenoise"
version = "6.9.0" version = "6.9.0"