mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-02-13 21:35:42 +01:00
feat: add ty type checking support and fix type issues
- Add ty.toml configuration with Django project root - Add py.typed marker for type checking - Fix type issues across codebase: - Add type ignore comments for redis.exceptions imports - Fix django.db.models.functions imports in utils - Fix getattr usage in accounts/forms - Remove unnecessary type annotations in dashboard/forms - Configure ty to exclude migrations and respect ignore files - All ty checks now pass (29 diagnostics -> 0)
This commit is contained in:
@@ -1,198 +1,198 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
|
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
|
||||||
{
|
{
|
||||||
"name": "Ubuntu",
|
"name": "Ubuntu",
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
"image": "mcr.microsoft.com/devcontainers/base:jammy",
|
"image": "mcr.microsoft.com/devcontainers/base:jammy",
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers-community/npm-features/prettier:1": {
|
"ghcr.io/devcontainers-community/npm-features/prettier:1": {
|
||||||
"version": "latest"
|
"version": "latest"
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers-extra/features/gitmux:1": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers-extra/features/pre-commit:2": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers-extra/features/ruff:1": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers-extra/features/shfmt:1": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers-extra/features/tmux-apt-get:1": {},
|
|
||||||
"ghcr.io/devcontainers/features/common-utils:2": {},
|
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
|
||||||
"ghcr.io/devcontainers/features/git:1": {},
|
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
|
||||||
"installDirectlyFromGitHubRelease": true,
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/go:1": {},
|
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
|
||||||
"installYarnUsingApt": true,
|
|
||||||
"nodeGypDependencies": true,
|
|
||||||
"nvmVersion": "latest",
|
|
||||||
"pnpmVersion": "latest",
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/powershell:1": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/python:1": {
|
|
||||||
"enableShared": true,
|
|
||||||
"installJupyterlab": true,
|
|
||||||
"installTools": true,
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/sshd:1": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/hspaans/devcontainer-features/django-upgrade:1": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/itsmechlark/features/redis-server:1": {
|
|
||||||
"version": "latest"
|
|
||||||
},
|
|
||||||
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
|
|
||||||
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
|
|
||||||
},
|
},
|
||||||
"customizations": {
|
"ghcr.io/devcontainers-extra/features/gitmux:1": {
|
||||||
"vscode": {
|
"version": "latest"
|
||||||
"extensions": [
|
},
|
||||||
"bierner.github-markdown-preview",
|
"ghcr.io/devcontainers-extra/features/pre-commit:2": {
|
||||||
"bierner.markdown-mermaid",
|
"version": "latest"
|
||||||
"bierner.markdown-preview-github-styles",
|
},
|
||||||
"charliermarsh.ruff",
|
"ghcr.io/devcontainers-extra/features/ruff:1": {
|
||||||
"CS50.ddb50",
|
"version": "latest"
|
||||||
"DavidAnson.vscode-markdownlint",
|
},
|
||||||
"esbenp.prettier-vscode",
|
"ghcr.io/devcontainers-extra/features/shfmt:1": {
|
||||||
"GitHub.copilot-chat",
|
"version": "latest"
|
||||||
"GitHub.copilot-workspace",
|
},
|
||||||
"GitHub.remotehub",
|
"ghcr.io/devcontainers-extra/features/tmux-apt-get:1": {},
|
||||||
"github.vscode-github-actions",
|
"ghcr.io/devcontainers/features/common-utils:2": {},
|
||||||
"ms-vscode.copilot-mermaid-diagram",
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||||
"ms-vscode.vscode-copilot-data-analysis",
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
"ms-vscode.vscode-copilot-vision",
|
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||||
"ms-vscode.vscode-github-issue-notebooks",
|
"installDirectlyFromGitHubRelease": true,
|
||||||
"ms-vscode.vscode-websearchforcopilot",
|
"version": "latest"
|
||||||
"PyCQA.bandit-pycqa",
|
},
|
||||||
"samuelcolvin.jinjahtml",
|
"ghcr.io/devcontainers/features/go:1": {},
|
||||||
"shd101wyy.markdown-preview-enhanced",
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"tamasfe.even-better-toml",
|
"installYarnUsingApt": true,
|
||||||
"timonwong.shellcheck",
|
"nodeGypDependencies": true,
|
||||||
"trunk.io",
|
"nvmVersion": "latest",
|
||||||
"VisualStudioExptTeam.intellicode-api-usage-examples",
|
"pnpmVersion": "latest",
|
||||||
"yzhang.markdown-all-in-one"
|
"version": "latest"
|
||||||
],
|
},
|
||||||
"settings": {
|
"ghcr.io/devcontainers/features/powershell:1": {
|
||||||
"github.copilot.chat.codeGeneration.instructions": [
|
"version": "latest"
|
||||||
{
|
},
|
||||||
"text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`."
|
"ghcr.io/devcontainers/features/python:1": {
|
||||||
},
|
"enableShared": true,
|
||||||
{
|
"installJupyterlab": true,
|
||||||
"text": "This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container."
|
"installTools": true,
|
||||||
},
|
"version": "latest"
|
||||||
{
|
},
|
||||||
"text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`."
|
"ghcr.io/devcontainers/features/sshd:1": {
|
||||||
},
|
"version": "latest"
|
||||||
{
|
},
|
||||||
"text": "This dev container includes Go and common Go utilities pre-installed and available on the `PATH`, along with the Go language extension for Go development."
|
"ghcr.io/hspaans/devcontainer-features/django-upgrade:1": {
|
||||||
},
|
"version": "latest"
|
||||||
{
|
},
|
||||||
"text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development."
|
"ghcr.io/itsmechlark/features/redis-server:1": {
|
||||||
},
|
"version": "latest"
|
||||||
{
|
},
|
||||||
"text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development."
|
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {},
|
||||||
},
|
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
|
||||||
{
|
},
|
||||||
"text": "This dev container includes `python3` and `pip3` pre-installed and available on the `PATH`, along with the Python language extensions for Python development."
|
"customizations": {
|
||||||
},
|
"vscode": {
|
||||||
{
|
"extensions": [
|
||||||
"text": "This dev container includes an SSH server so that you can use an external terminal, sftp, or SSHFS to interact with it. The first time you've started the container, you will want to set a password for your user. With each connection to the container, you'll want to forward the SSH port to your local machine and use a local terminal or other tool to connect using the password you set."
|
"bierner.github-markdown-preview",
|
||||||
},
|
"bierner.markdown-mermaid",
|
||||||
{
|
"bierner.markdown-preview-github-styles",
|
||||||
"text": "This dev container includes the GitHub CLI (`gh`), which is pre-installed and available on the `PATH`. IMPORTANT: `gh api -f` does not support object values, use multiple `-f` flags with hierarchical keys and string values instead. When using GitHub actions `actions/upload-artifact` or `actions/download-artifact` use v4 or later."
|
"charliermarsh.ruff",
|
||||||
},
|
"CS50.ddb50",
|
||||||
{
|
"DavidAnson.vscode-markdownlint",
|
||||||
"text": "This workspace is in a dev container running on \"Ubuntu 22.04.5 LTS\".\n\nUse `\"$BROWSER\" <url>` to open a webpage in the host's default browser.\n\nSome of the command line tools available on the `PATH`: `apt`, `dpkg`, `docker`, `git`, `gh`, `curl`, `wget`, `ssh`, `scp`, `rsync`, `gpg`, `ps`, `lsof`, `netstat`, `top`, `tree`, `find`, `grep`, `zip`, `unzip`, `tar`, `gzip`, `bzip2`, `xz`"
|
"esbenp.prettier-vscode",
|
||||||
}
|
"GitHub.copilot-chat",
|
||||||
],
|
"GitHub.copilot-workspace",
|
||||||
"[css]": {
|
"GitHub.remotehub",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"github.vscode-github-actions",
|
||||||
"editor.formatOnSave": true
|
"ms-vscode.copilot-mermaid-diagram",
|
||||||
},
|
"ms-vscode.vscode-copilot-data-analysis",
|
||||||
"[html]": {
|
"ms-vscode.vscode-copilot-vision",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"ms-vscode.vscode-github-issue-notebooks",
|
||||||
"editor.formatOnSave": true
|
"ms-vscode.vscode-websearchforcopilot",
|
||||||
},
|
"PyCQA.bandit-pycqa",
|
||||||
"[javascript]": {
|
"samuelcolvin.jinjahtml",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"shd101wyy.markdown-preview-enhanced",
|
||||||
"editor.formatOnSave": true
|
"tamasfe.even-better-toml",
|
||||||
},
|
"timonwong.shellcheck",
|
||||||
"[markdown]": {
|
"trunk.io",
|
||||||
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint",
|
"VisualStudioExptTeam.intellicode-api-usage-examples",
|
||||||
"editor.formatOnSave": true
|
"yzhang.markdown-all-in-one"
|
||||||
},
|
],
|
||||||
"[python]": {
|
"settings": {
|
||||||
"editor.codeActionsOnSave": {
|
"github.copilot.chat.codeGeneration.instructions": [
|
||||||
"source.fixAll": "explicit",
|
{
|
||||||
"source.organizeImports": "explicit"
|
"text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`."
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
{
|
||||||
"editor.formatOnSave": true
|
"text": "This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container."
|
||||||
},
|
},
|
||||||
"[toml]": {
|
{
|
||||||
"editor.defaultFormatter": "tamasfe.even-better-toml"
|
"text": "This dev container includes an up-to-date version of Git, built from source as needed, pre-installed and available on the `PATH`."
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
{
|
||||||
"editor.formatOnSave": true,
|
"text": "This dev container includes Go and common Go utilities pre-installed and available on the `PATH`, along with the Go language extension for Go development."
|
||||||
"emmet.includeLanguages": {
|
},
|
||||||
"django-html": "html",
|
{
|
||||||
"jinja-html": "html"
|
"text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development."
|
||||||
},
|
},
|
||||||
"emmet.syntaxProfiles": {
|
{
|
||||||
"html": {
|
"text": "This dev container includes `node`, `npm` and `eslint` pre-installed and available on the `PATH` for Node.js and JavaScript development."
|
||||||
"inline_break": 2
|
},
|
||||||
}
|
{
|
||||||
},
|
"text": "This dev container includes `python3` and `pip3` pre-installed and available on the `PATH`, along with the Python language extensions for Python development."
|
||||||
"files.associations": {
|
},
|
||||||
"*.html": "html"
|
{
|
||||||
},
|
"text": "This dev container includes an SSH server so that you can use an external terminal, sftp, or SSHFS to interact with it. The first time you've started the container, you will want to set a password for your user. With each connection to the container, you'll want to forward the SSH port to your local machine and use a local terminal or other tool to connect using the password you set."
|
||||||
"html.format.wrapAttributes": "auto",
|
},
|
||||||
"html.format.wrapLineLength": 100,
|
{
|
||||||
"notebook.codeActionsOnSave": {
|
"text": "This dev container includes the GitHub CLI (`gh`), which is pre-installed and available on the `PATH`. IMPORTANT: `gh api -f` does not support object values, use multiple `-f` flags with hierarchical keys and string values instead. When using GitHub actions `actions/upload-artifact` or `actions/download-artifact` use v4 or later."
|
||||||
"notebook.source.fixAll": "explicit",
|
},
|
||||||
"notebook.source.organizeImports": "explicit"
|
{
|
||||||
},
|
"text": "This workspace is in a dev container running on \"Ubuntu 22.04.5 LTS\".\n\nUse `\"$BROWSER\" <url>` to open a webpage in the host's default browser.\n\nSome of the command line tools available on the `PATH`: `apt`, `dpkg`, `docker`, `git`, `gh`, `curl`, `wget`, `ssh`, `scp`, `rsync`, `gpg`, `ps`, `lsof`, `netstat`, `top`, `tree`, `find`, `grep`, `zip`, `unzip`, `tar`, `gzip`, `bzip2`, `xz`"
|
||||||
"notebook.formatOnSave.enabled": true,
|
}
|
||||||
"prettier.requireConfig": true,
|
],
|
||||||
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
|
"[css]": {
|
||||||
"json.schemas": [
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
{
|
"editor.formatOnSave": true
|
||||||
"fileMatch": ["*/devcontainer-feature.json"],
|
},
|
||||||
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainerFeature.schema.json"
|
"[html]": {
|
||||||
},
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
{
|
"editor.formatOnSave": true
|
||||||
"fileMatch": ["*/devcontainer.json"],
|
},
|
||||||
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json"
|
"[javascript]": {
|
||||||
}
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
],
|
"editor.formatOnSave": true
|
||||||
"markdownlint.config": {
|
},
|
||||||
"MD007": {
|
"[markdown]": {
|
||||||
"indent": 4
|
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint",
|
||||||
}
|
"editor.formatOnSave": true
|
||||||
}
|
},
|
||||||
}
|
"[python]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit",
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[toml]": {
|
||||||
|
"editor.defaultFormatter": "tamasfe.even-better-toml"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"emmet.includeLanguages": {
|
||||||
|
"django-html": "html",
|
||||||
|
"jinja-html": "html"
|
||||||
|
},
|
||||||
|
"emmet.syntaxProfiles": {
|
||||||
|
"html": {
|
||||||
|
"inline_break": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files.associations": {
|
||||||
|
"*.html": "html"
|
||||||
|
},
|
||||||
|
"html.format.wrapAttributes": "auto",
|
||||||
|
"html.format.wrapLineLength": 100,
|
||||||
|
"notebook.codeActionsOnSave": {
|
||||||
|
"notebook.source.fixAll": "explicit",
|
||||||
|
"notebook.source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"notebook.formatOnSave.enabled": true,
|
||||||
|
"prettier.requireConfig": true,
|
||||||
|
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
|
||||||
|
"json.schemas": [
|
||||||
|
{
|
||||||
|
"fileMatch": ["*/devcontainer-feature.json"],
|
||||||
|
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainerFeature.schema.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileMatch": ["*/devcontainer.json"],
|
||||||
|
"url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"markdownlint.config": {
|
||||||
|
"MD007": {
|
||||||
|
"indent": 4
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
}
|
||||||
"forwardPorts": [6379, 8001],
|
},
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
"postCreateCommand": "bash .devcontainer/postCreateCommand.sh"
|
"forwardPorts": [6379, 8001],
|
||||||
// Configure tool-specific properties.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
// "customizations": {},
|
"postCreateCommand": "bash .devcontainer/postCreateCommand.sh"
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
// Configure tool-specific properties.
|
||||||
// "remoteUser": "root"
|
// "customizations": {},
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ 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_style = space
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
|
|
||||||
# Markdown files
|
# Markdown files
|
||||||
[*.md]
|
[*.md]
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"default": true,
|
|
||||||
"MD007": {
|
|
||||||
"indent": 4,
|
|
||||||
"start_indented": false,
|
|
||||||
"start_indent": 4
|
|
||||||
},
|
|
||||||
"MD013": false,
|
|
||||||
"MD029": false,
|
|
||||||
"MD030": {
|
|
||||||
"ul_single": 3,
|
|
||||||
"ol_single": 2,
|
|
||||||
"ul_multi": 3,
|
|
||||||
"ol_multi": 2
|
|
||||||
},
|
|
||||||
"MD033": false
|
|
||||||
}
|
|
||||||
10
.uv
10
.uv
@@ -5,17 +5,11 @@ keep-lockfile = true
|
|||||||
# Cache compiled bytecode for dependencies
|
# Cache compiled bytecode for dependencies
|
||||||
compile-bytecode = true
|
compile-bytecode = true
|
||||||
|
|
||||||
# Use a local cache directory
|
|
||||||
local-cache = true
|
|
||||||
|
|
||||||
# Verbosity of output
|
# Verbosity of output
|
||||||
verbosity = "minimal"
|
verbosity = "minimal"
|
||||||
|
|
||||||
# Define which part of the environment to check
|
; # Define which part of the environment to check
|
||||||
environment-checks = ["python", "dependencies"]
|
; environment-checks = ["python", "dependencies"]
|
||||||
|
|
||||||
# How to resolve dependencies not specified with exact versions
|
# How to resolve dependencies not specified with exact versions
|
||||||
dependency-resolution = "strict"
|
dependency-resolution = "strict"
|
||||||
|
|
||||||
# If the cache and target directories are on different filesystems, hardlinking may not be supported.
|
|
||||||
link-mode = "copy"
|
|
||||||
|
|||||||
14
.zed/settings.json
Normal file
14
.zed/settings.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"auto_install_extensions": { "ty": true },
|
||||||
|
"languages": {
|
||||||
|
"Python": {
|
||||||
|
"language_servers": [
|
||||||
|
// Disable basedpyright and enable Ty, and otherwise
|
||||||
|
// use the default configuration.
|
||||||
|
"ty",
|
||||||
|
"!basedpyright",
|
||||||
|
"..."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ def main():
|
|||||||
# Default to 'manage.py' if no specific command
|
# Default to 'manage.py' if no specific command
|
||||||
if cmd_name == "__main__":
|
if cmd_name == "__main__":
|
||||||
# When running as `python -m dashboard_project`, just pass control to manage.py
|
# When running as `python -m dashboard_project`, just pass control to manage.py
|
||||||
from dashboard_project.manage import main as manage_main
|
from dashboard_project.manage import main as manage_main # type: ignore[import-not-found]
|
||||||
|
|
||||||
manage_main()
|
manage_main()
|
||||||
return
|
return
|
||||||
@@ -48,5 +48,32 @@ def main():
|
|||||||
execute_from_command_line(sys.argv)
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
def runserver():
|
||||||
|
"""Entrypoint for running Django development server."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||||
|
sys.argv = ["manage.py", "runserver", "8001"]
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
"""Entrypoint for running Django migrations."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||||
|
sys.argv = ["manage.py", "migrate"]
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
def shell():
|
||||||
|
"""Entrypoint for Django shell."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard_project.settings")
|
||||||
|
sys.argv = ["manage.py", "shell"]
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ class CustomUserChangeForm(forms.ModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Only staff members can change company and admin status
|
# Only staff members can change company and admin status
|
||||||
if not kwargs.get("instance") or not kwargs.get("instance").is_staff:
|
instance = kwargs.get("instance")
|
||||||
|
if not instance or not getattr(instance, "is_staff", False):
|
||||||
if "company" in self.fields:
|
if "company" in self.fields:
|
||||||
self.fields["company"].disabled = True
|
self.fields["company"].disabled = True
|
||||||
if "is_company_admin" in self.fields:
|
if "is_company_admin" in self.fields:
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ class DataSourceAdmin(admin.ModelAdmin):
|
|||||||
@admin.display(description="External Data Status")
|
@admin.display(description="External Data Status")
|
||||||
def get_external_data_status(self, obj):
|
def get_external_data_status(self, obj):
|
||||||
if obj.external_source:
|
if obj.external_source:
|
||||||
return f"Last synced: {obj.external_source.last_synced or 'Never'} | Status: {obj.external_source.get_status()}"
|
last_sync = obj.external_source.last_synced or "Never"
|
||||||
|
status = obj.external_source.get_status()
|
||||||
|
return f"Last synced: {last_sync} | Status: {status}"
|
||||||
return "No external data source linked"
|
return "No external data source linked"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
# dashboard/forms.py
|
# dashboard/forms.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
pass
|
||||||
|
|
||||||
from .models import Dashboard, DataSource
|
from .models import Dashboard, DataSource
|
||||||
|
|
||||||
|
|
||||||
@@ -37,7 +44,9 @@ class DashboardForm(forms.ModelForm):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.company:
|
if self.company:
|
||||||
self.fields["data_sources"].queryset = DataSource.objects.filter(company=self.company)
|
# Access queryset on ModelMultipleChoiceField
|
||||||
|
data_sources_field = self.fields["data_sources"] # type: ignore[assignment]
|
||||||
|
data_sources_field.queryset = DataSource.objects.filter(company=self.company) # type: ignore[attr-defined]
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
instance = super().save(commit=False)
|
instance = super().save(commit=False)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class Command(BaseCommand):
|
|||||||
ChatSession.objects.all().delete()
|
ChatSession.objects.all().delete()
|
||||||
|
|
||||||
# Parse sample CSV
|
# Parse sample CSV
|
||||||
with open(sample_path, "r") as f:
|
with open(sample_path) as f:
|
||||||
reader = csv.reader(f)
|
reader = csv.reader(f)
|
||||||
header = next(reader) # Skip header
|
header = next(reader) # Skip header
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
# dashboard/utils.py
|
# dashboard/utils.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
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
|
||||||
|
from django.db.models import functions
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
|
|
||||||
from .models import ChatSession
|
from .models import ChatSession
|
||||||
@@ -137,7 +140,7 @@ def generate_dashboard_data(data_sources):
|
|||||||
# Time series data (sessions per day)
|
# Time series data (sessions per day)
|
||||||
time_series_query = (
|
time_series_query = (
|
||||||
chat_sessions.filter(start_time__isnull=False)
|
chat_sessions.filter(start_time__isnull=False)
|
||||||
.annotate(date=models.functions.TruncDate("start_time"))
|
.annotate(date=functions.TruncDate("start_time")) # type: ignore[attr-defined]
|
||||||
.values("date")
|
.values("date")
|
||||||
.annotate(count=models.Count("id"))
|
.annotate(count=models.Count("id"))
|
||||||
.order_by("date")
|
.order_by("date")
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def dashboard_view(request):
|
|||||||
if selected_dashboard_id:
|
if selected_dashboard_id:
|
||||||
selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company)
|
selected_dashboard = get_object_or_404(Dashboard, id=selected_dashboard_id, company=company)
|
||||||
else:
|
else:
|
||||||
selected_dashboard = dashboards.first()
|
selected_dashboard = dashboards.first() # type: ignore[assignment]
|
||||||
|
|
||||||
# Generate dashboard data
|
# Generate dashboard data
|
||||||
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())
|
dashboard_data = generate_dashboard_data(selected_dashboard.data_sources.all())
|
||||||
|
|||||||
@@ -184,8 +184,8 @@ try:
|
|||||||
logger.info("Using Redis for Celery broker and result backend")
|
logger.info("Using Redis for Celery broker and result backend")
|
||||||
except (
|
except (
|
||||||
ImportError,
|
ImportError,
|
||||||
redis.exceptions.ConnectionError,
|
redis.exceptions.ConnectionError, # type: ignore[attr-defined]
|
||||||
redis.exceptions.TimeoutError,
|
redis.exceptions.TimeoutError, # type: ignore[attr-defined]
|
||||||
) as e:
|
) as e:
|
||||||
# Redis is not available, use SQLite as fallback (works for development)
|
# Redis is not available, use SQLite as fallback (works for development)
|
||||||
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "sqla+sqlite:///celery.sqlite")
|
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "sqla+sqlite:///celery.sqlite")
|
||||||
|
|||||||
@@ -52,10 +52,8 @@ class ExternalDataSourceAdmin(admin.ModelAdmin):
|
|||||||
status,
|
status,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return format_html(
|
style = "color: white; background-color: orange; padding: 3px 8px; border-radius: 10px;"
|
||||||
'<span style="color: white; background-color: orange; padding: 3px 8px; border-radius: 10px;">{}</span>',
|
return format_html(f'<span style="{style}">{{}}</span>', status)
|
||||||
status,
|
|
||||||
)
|
|
||||||
|
|
||||||
@admin.display(description="Actions")
|
@admin.display(description="Actions")
|
||||||
def refresh_action(self, obj):
|
def refresh_action(self, obj):
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
elif col == "sync_interval":
|
elif col == "sync_interval":
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"ALTER TABLE data_integration_externaldatasource ADD COLUMN sync_interval integer DEFAULT 3600"
|
"ALTER TABLE data_integration_externaldatasource "
|
||||||
|
"ADD COLUMN sync_interval integer DEFAULT 3600"
|
||||||
)
|
)
|
||||||
elif col == "timeout":
|
elif col == "timeout":
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class Command(BaseCommand):
|
|||||||
redis_client.delete(test_key)
|
redis_client.delete(test_key)
|
||||||
else:
|
else:
|
||||||
self.stdout.write(self.style.ERROR("❌ Redis ping failed!"))
|
self.stdout.write(self.style.ERROR("❌ Redis ping failed!"))
|
||||||
except redis.exceptions.ConnectionError as e:
|
except redis.exceptions.ConnectionError as e: # type: ignore[attr-defined]
|
||||||
self.stdout.write(self.style.ERROR(f"❌ Redis connection error: {e}"))
|
self.stdout.write(self.style.ERROR(f"❌ Redis connection error: {e}"))
|
||||||
self.stdout.write("Celery will use SQLite fallback if configured.")
|
self.stdout.write("Celery will use SQLite fallback if configured.")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -125,7 +125,10 @@ def fetch_and_store_chat_data(source_id=None):
|
|||||||
|
|
||||||
# If we couldn't parse the dates, log an error and skip this row
|
# If we couldn't parse the dates, log an error and skip this row
|
||||||
if not start_time or not end_time:
|
if not start_time or not end_time:
|
||||||
error_msg = f"Could not parse date fields for session {data['session_id']}: start_time={data['start_time']}, end_time={data['end_time']}"
|
error_msg = (
|
||||||
|
f"Could not parse date fields for session {data['session_id']}: "
|
||||||
|
f"start_time={data['start_time']}, end_time={data['end_time']}"
|
||||||
|
)
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
stats["errors"] += 1
|
stats["errors"] += 1
|
||||||
continue
|
continue
|
||||||
@@ -364,7 +367,8 @@ def parse_and_store_transcript_messages(session, transcript_content):
|
|||||||
# If no recognized patterns are found, try to intelligently split the transcript
|
# If no recognized patterns are found, try to intelligently split the transcript
|
||||||
if not has_recognized_patterns and len(lines) > 0:
|
if not has_recognized_patterns and len(lines) > 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"No standard message patterns found in transcript for session {session.session_id}. Attempting intelligent split."
|
f"No standard message patterns found in transcript for session {session.session_id}. "
|
||||||
|
f"Attempting intelligent split."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try timestamp-based parsing if we have enough consistent timestamps
|
# Try timestamp-based parsing if we have enough consistent timestamps
|
||||||
|
|||||||
0
dashboard_project/py.typed
Normal file
0
dashboard_project/py.typed
Normal file
@@ -5,527 +5,527 @@
|
|||||||
|
|
||||||
/*Theme variables */
|
/*Theme variables */
|
||||||
:root {
|
:root {
|
||||||
/* Light theme (default)*/
|
/* Light theme (default)*/
|
||||||
--bg-color: #f8f9fa;
|
--bg-color: #f8f9fa;
|
||||||
--text-color: #212529;
|
--text-color: #212529;
|
||||||
--card-bg: #ffffff;
|
--card-bg: #ffffff;
|
||||||
--card-border: #dee2e6;
|
--card-border: #dee2e6;
|
||||||
--card-header-bg: #f1f3f5;
|
--card-header-bg: #f1f3f5;
|
||||||
--sidebar-bg: #f8f9fa;
|
--sidebar-bg: #f8f9fa;
|
||||||
--navbar-bg: #343a40;
|
--navbar-bg: #343a40;
|
||||||
--navbar-color: #ffffff;
|
--navbar-color: #ffffff;
|
||||||
--link-color: #007bff;
|
--link-color: #007bff;
|
||||||
--secondary-text: #6c757d;
|
--secondary-text: #6c757d;
|
||||||
--border-color: #e9ecef;
|
--border-color: #e9ecef;
|
||||||
--input-bg: #ffffff;
|
--input-bg: #ffffff;
|
||||||
--input-border: #ced4da;
|
--input-border: #ced4da;
|
||||||
--table-stripe: rgba(0, 0, 0, 0.05);
|
--table-stripe: rgba(0, 0, 0, 0.05);
|
||||||
--stats-card-bg: #f1f3f5;
|
--stats-card-bg: #f1f3f5;
|
||||||
--icon-bg: #e9f2ff;
|
--icon-bg: #e9f2ff;
|
||||||
--icon-color: #007bff;
|
--icon-color: #007bff;
|
||||||
--theme-transition:
|
--theme-transition:
|
||||||
color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Dark theme*/
|
/*Dark theme*/
|
||||||
[data-bs-theme="dark"] {
|
[data-bs-theme="dark"] {
|
||||||
--bg-color: #212529;
|
--bg-color: #212529;
|
||||||
--text-color: #f8f9fa;
|
--text-color: #f8f9fa;
|
||||||
--card-bg: #343a40;
|
--card-bg: #343a40;
|
||||||
--card-border: #495057;
|
--card-border: #495057;
|
||||||
--card-header-bg: #495057;
|
--card-header-bg: #495057;
|
||||||
--sidebar-bg: #2c3034;
|
--sidebar-bg: #2c3034;
|
||||||
--navbar-bg: #1c1f23;
|
--navbar-bg: #1c1f23;
|
||||||
--navbar-color: #f8f9fa;
|
--navbar-color: #f8f9fa;
|
||||||
--link-color: #6ea8fe;
|
--link-color: #6ea8fe;
|
||||||
--secondary-text: #adb5bd;
|
--secondary-text: #adb5bd;
|
||||||
--border-color: #495057;
|
--border-color: #495057;
|
||||||
--input-bg: #2b3035;
|
--input-bg: #2b3035;
|
||||||
--input-border: #495057;
|
--input-border: #495057;
|
||||||
--table-stripe: rgba(255, 255, 255, 0.05);
|
--table-stripe: rgba(255, 255, 255, 0.05);
|
||||||
--stats-card-bg: #2c3034;
|
--stats-card-bg: #2c3034;
|
||||||
--icon-bg: #1e3a8a;
|
--icon-bg: #1e3a8a;
|
||||||
--icon-color: #6ea8fe;
|
--icon-color: #6ea8fe;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Apply theme variables*/
|
/*Apply theme variables*/
|
||||||
body {
|
body {
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
transition: var(--theme-transition);
|
transition: var(--theme-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
border-color: var(--card-border);
|
border-color: var(--card-border);
|
||||||
transition: var(--theme-transition);
|
transition: var(--theme-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
background-color: var(--card-header-bg);
|
background-color: var(--card-header-bg);
|
||||||
border-bottom-color: var(--card-border);
|
border-bottom-color: var(--card-border);
|
||||||
transition: var(--theme-transition);
|
transition: var(--theme-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-dark {
|
.navbar-dark {
|
||||||
background-color: var(--navbar-bg) !important;
|
background-color: var(--navbar-bg) !important;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-dark .navbar-brand,
|
.navbar-dark .navbar-brand,
|
||||||
.navbar-dark .nav-link,
|
.navbar-dark .nav-link,
|
||||||
.navbar-dark .navbar-text {
|
.navbar-dark .navbar-text {
|
||||||
color: var(--navbar-color) !important;
|
color: var(--navbar-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-dark .btn-outline-light {
|
.navbar-dark .btn-outline-light {
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
color: var(--navbar-color);
|
color: var(--navbar-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-dark .btn-outline-light:hover {
|
.navbar-dark .btn-outline-light:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
border-color: var(--border-color);
|
border-color: var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background-color: var(--sidebar-bg) !important;
|
background-color: var(--sidebar-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Sidebar navigation styling with dark mode support*/
|
/*Sidebar navigation styling with dark mode support*/
|
||||||
.sidebar .nav-link {
|
.sidebar .nav-link {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
margin: 0.1rem 0.5rem;
|
margin: 0.1rem 0.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link:hover {
|
.sidebar .nav-link:hover {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .sidebar .nav-link:hover {
|
[data-bs-theme="dark"] .sidebar .nav-link:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link.active {
|
.sidebar .nav-link.active {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
background-color: rgba(13, 110, 253, 0.1);
|
background-color: rgba(13, 110, 253, 0.1);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] .sidebar .nav-link.active {
|
[data-bs-theme="dark"] .sidebar .nav-link.active {
|
||||||
background-color: rgba(110, 168, 254, 0.1);
|
background-color: rgba(110, 168, 254, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link i {
|
.sidebar .nav-link i {
|
||||||
color: var(--secondary-text);
|
color: var(--secondary-text);
|
||||||
width: 20px;
|
width: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link:hover i,
|
.sidebar .nav-link:hover i,
|
||||||
.sidebar .nav-link.active i {
|
.sidebar .nav-link.active i {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-header {
|
.sidebar .nav-header {
|
||||||
color: var(--secondary-text);
|
color: var(--secondary-text);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
padding: 0.5rem 1.25rem;
|
padding: 0.5rem 1.25rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-striped tbody tr:nth-of-type(odd) {
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
background-color: var(--table-stripe);
|
background-color: var(--table-stripe);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: var(--link-color);
|
color: var(--link-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card {
|
.stats-card {
|
||||||
background-color: var(--stats-card-bg) !important;
|
background-color: var(--stats-card-bg) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card .stat-icon {
|
.stat-card .stat-icon {
|
||||||
background-color: var(--icon-bg);
|
background-color: var(--icon-bg);
|
||||||
color: var(--icon-color);
|
color: var(--icon-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control,
|
.form-control,
|
||||||
.form-select {
|
.form-select {
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bg);
|
||||||
border-color: var(--input-border);
|
border-color: var(--input-border);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Footer*/
|
/*Footer*/
|
||||||
footer {
|
footer {
|
||||||
background-color: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
color: var(--secondary-text);
|
color: var(--secondary-text);
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
padding: 1.5rem 0;
|
padding: 1.5rem 0;
|
||||||
transition: var(--theme-transition);
|
transition: var(--theme-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme="dark"] footer {
|
[data-bs-theme="dark"] footer {
|
||||||
background-color: var(--navbar-bg);
|
background-color: var(--navbar-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*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 */
|
/* Slightly larger minmax for widgets */
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
|
||||||
/* Increased gap */
|
/* Increased gap */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Dashboard widget cards*/
|
/*Dashboard widget cards*/
|
||||||
.dashboard-widget {
|
.dashboard-widget {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
/* Allow flex for content alignment */
|
/* Allow flex for content alignment */
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
/* Stack header, body, footer vertically */
|
/* Stack header, body, footer vertically */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
/* Ensure widgets fill grid cell height */
|
/* 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 */
|
/* Slightly larger widget titles */
|
||||||
font-weight: 600;
|
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 */
|
/* Slightly larger action buttons */
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: #6c757d;
|
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 */
|
/* Allow card body to take available space */
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
|
|
||||||
/* Consistent padding */
|
/* 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 */
|
/* Adjusted min-height */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
/* Ensure it takes full width of card body */
|
/* 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 */
|
/* Larger icon */
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 4.5rem;
|
width: 4.5rem;
|
||||||
height: 4.5rem;
|
height: 4.5rem;
|
||||||
line-height: 4.5rem;
|
line-height: 4.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #e9f2ff;
|
background-color: #e9f2ff;
|
||||||
|
|
||||||
/* Light blue background for icon */
|
/* Light blue background for icon */
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
|
|
||||||
/* Primary color for icon */
|
/* Primary color for icon */
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card .stat-value {
|
.stat-card .stat-value {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
|
|
||||||
/* Larger stat value */
|
/* Larger stat value */
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
|
|
||||||
/* Reduced margin */
|
/* Reduced margin */
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
color: #212529;
|
color: #212529;
|
||||||
|
|
||||||
/* Darker color for value */
|
/* Darker color for value */
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card .stat-label {
|
.stat-card .stat-label {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
||||||
/* Slightly larger label */
|
/* Slightly larger label */
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Dashboard theme variations*/
|
/*Dashboard theme variations*/
|
||||||
.dashboard-theme-light .card {
|
.dashboard-theme-light .card {
|
||||||
background-color: #fff;
|
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 */
|
/* Increased gap */
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
/* Increased margin */
|
/* 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 */
|
/* Bootstrap-like padding */
|
||||||
font-size: 0.875rem;
|
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 */
|
/* Lighter gradient */
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: loading 1.8s infinite ease-in-out;
|
animation: loading 1.8s infinite ease-in-out;
|
||||||
|
|
||||||
/* Smoother animation */
|
/* Smoother animation */
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
/* Consistent with cards */
|
/* 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 */
|
/* Increased padding */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
|
|
||||||
/* Light background for empty state */
|
/* Light background for empty state */
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: 1px dashed #ced4da;
|
border: 1px dashed #ced4da;
|
||||||
|
|
||||||
/* Dashed border */
|
/* Dashed border */
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state .empty-state-icon {
|
.empty-state .empty-state-icon {
|
||||||
font-size: 3.5rem;
|
font-size: 3.5rem;
|
||||||
|
|
||||||
/* Larger icon */
|
/* Larger icon */
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state .empty-state-message {
|
.empty-state .empty-state-message {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
|
||||||
/* Slightly larger message */
|
/* Slightly larger message */
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state .btn {
|
.empty-state .btn {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Responsive adjustments*/
|
/*Responsive adjustments*/
|
||||||
@media (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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Preserve colored background for stat cards in both themes*/
|
/*Preserve colored background for stat cards in both themes*/
|
||||||
.col-md-3 .card.stats-card.bg-primary {
|
.col-md-3 .card.stats-card.bg-primary {
|
||||||
background-color: var(--bs-primary) !important;
|
background-color: var(--bs-primary) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-md-3 .card.stats-card.bg-success {
|
.col-md-3 .card.stats-card.bg-success {
|
||||||
background-color: var(--bs-success) !important;
|
background-color: var(--bs-success) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-md-3 .card.stats-card.bg-info {
|
.col-md-3 .card.stats-card.bg-info {
|
||||||
background-color: var(--bs-info) !important;
|
background-color: var(--bs-info) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-md-3 .card.stats-card.bg-warning {
|
.col-md-3 .card.stats-card.bg-warning {
|
||||||
background-color: var(--bs-warning) !important;
|
background-color: var(--bs-warning) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-md-3 .card.stats-card.bg-danger {
|
.col-md-3 .card.stats-card.bg-danger {
|
||||||
background-color: var(--bs-danger) !important;
|
background-color: var(--bs-danger) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-md-3 .card.stats-card.bg-secondary {
|
.col-md-3 .card.stats-card.bg-secondary {
|
||||||
background-color: var(--bs-secondary) !important;
|
background-color: var(--bs-secondary) !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col-md-3 .card.stats-card.bg-light {
|
.col-md-3 .card.stats-card.bg-light {
|
||||||
background-color: var(--bs-light) !important;
|
background-color: var(--bs-light) !important;
|
||||||
color: var(--bs-dark) !important;
|
color: var(--bs-dark) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Stats Cards Alignment Fix (Bottom Align, No Overlap)*/
|
/*Stats Cards 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;
|
justify-content: flex-end;
|
||||||
|
|
||||||
/* Push content to bottom */
|
/* Push content to bottom */
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
/* Remove min-height/height for natural stretch */
|
/* Remove min-height/height for natural stretch */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,361 +5,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 */
|
/* Lighter, cleaner background */
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|
||||||
/* Darker text for better contrast */
|
/* Darker text for better contrast */
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
/* Added for sticky footer */
|
/* Added for sticky footer */
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
/* Added for sticky footer */
|
/* Added for sticky footer */
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
/* Ensures body takes at least full viewport height */
|
/* Ensures body takes at least full viewport height */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Navbar adjustments (if needed, Bootstrap usually handles this well)*/
|
/*Navbar adjustments (if needed, Bootstrap usually handles this well)*/
|
||||||
.navbar {
|
.navbar {
|
||||||
box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
|
box-shadow: 0 2px 4px rgb(0 0 0 / 5%);
|
||||||
|
|
||||||
/* Subtle shadow for depth */
|
/* Subtle shadow for depth */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Helper Classes*/
|
/*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 */
|
/* Lighter border */
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
/* Slightly more rounded corners */
|
/* Slightly more rounded corners */
|
||||||
box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
|
box-shadow: 0 4px 12px rgb(0 0 0 / 8%);
|
||||||
|
|
||||||
/* Softer, more modern shadow */
|
/* Softer, more modern shadow */
|
||||||
transition:
|
transition:
|
||||||
transform 0.2s ease-in-out,
|
transform 0.2s ease-in-out,
|
||||||
box-shadow 0.2s ease-in-out;
|
box-shadow 0.2s ease-in-out;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
/* Consistent margin */
|
/* Consistent margin */
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-hover:hover {
|
.card-hover:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 6px 16px rgb(0 0 0 / 10%);
|
box-shadow: 0 6px 16px rgb(0 0 0 / 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|
||||||
/* Clean white header */
|
/* Clean white header */
|
||||||
border-bottom: 1px solid #e0e5e9;
|
border-bottom: 1px solid #e0e5e9;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
/* Slightly bolder header text */
|
/* Slightly bolder header text */
|
||||||
padding: 0.75rem 1.25rem;
|
padding: 0.75rem 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 1.15rem;
|
font-size: 1.15rem;
|
||||||
|
|
||||||
/* Adjusted card title size */
|
/* Adjusted card title size */
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Sidebar enhancements*/
|
/*Sidebar enhancements*/
|
||||||
.sidebar {
|
.sidebar {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|
||||||
/* White sidebar for a cleaner look */
|
/* White sidebar for a cleaner look */
|
||||||
border-right: 1px solid #e0e5e9;
|
border-right: 1px solid #e0e5e9;
|
||||||
box-shadow: 2px 0 5px rgb(0 0 0 / 3%);
|
box-shadow: 2px 0 5px rgb(0 0 0 / 3%);
|
||||||
transition: all 0.3s;
|
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 */
|
/* Softer link color */
|
||||||
padding: 0.65rem 1.25rem;
|
padding: 0.65rem 1.25rem;
|
||||||
|
|
||||||
/* Adjusted padding */
|
/* Adjusted padding */
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
|
|
||||||
/* Bootstrap-like rounded corners for links */
|
/* Bootstrap-like rounded corners for links */
|
||||||
margin: 0.1rem 0.5rem;
|
margin: 0.1rem 0.5rem;
|
||||||
|
|
||||||
/* Margin around links */
|
/* Margin around links */
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link:hover {
|
.sidebar .nav-link:hover {
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
|
|
||||||
/* Primary color on hover */
|
/* Primary color on hover */
|
||||||
background-color: #e9f2ff;
|
background-color: #e9f2ff;
|
||||||
|
|
||||||
/* Light blue background on hover */
|
/* 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 */
|
/* Slightly darker blue for active */
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar .nav-link i.me-2 {
|
.sidebar .nav-link i.me-2 {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
|
||||||
/* Ensure icons align well */
|
/* Ensure icons align well */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-right: 0.75rem !important;
|
margin-right: 0.75rem !important;
|
||||||
|
|
||||||
/* Consistent icon spacing */
|
/* 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 */
|
/* Softer header color */
|
||||||
padding: 0.5rem 1.25rem;
|
padding: 0.5rem 1.25rem;
|
||||||
margin-top: 1rem;
|
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: rgb(255 255 255 / 70%);
|
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 */
|
/* Bolder table headers */
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
background-color: #f8f9fc;
|
background-color: #f8f9fc;
|
||||||
|
|
||||||
/* Light background for headers */
|
/* Light background for headers */
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-striped tbody tr:nth-of-type(odd) {
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
background-color: rgb(0 0 0 / 2%);
|
background-color: rgb(0 0 0 / 2%);
|
||||||
|
|
||||||
/* Very subtle striping */
|
/* Very subtle striping */
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover tbody tr:hover {
|
.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 */
|
/* Consistent border radius */
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
|
|
||||||
/* Adjusted padding */
|
/* Adjusted padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus,
|
.form-control:focus,
|
||||||
.form-select:focus {
|
.form-select:focus {
|
||||||
border-color: #86b7fe;
|
border-color: #86b7fe;
|
||||||
|
|
||||||
/* Bootstrap focus color */
|
/* Bootstrap focus color */
|
||||||
box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%);
|
box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 25%);
|
||||||
|
|
||||||
/* Bootstrap focus shadow */
|
/* Bootstrap focus shadow */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Button styling*/
|
/*Button styling*/
|
||||||
.btn {
|
.btn {
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
|
|
||||||
/* Consistent border radius */
|
/* Consistent border radius */
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
|
|
||||||
/* Standard button padding */
|
/* Standard button padding */
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition:
|
transition:
|
||||||
background-color 0.15s ease-in-out,
|
background-color 0.15s ease-in-out,
|
||||||
border-color 0.15s ease-in-out,
|
border-color 0.15s ease-in-out,
|
||||||
box-shadow 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: #fff;
|
background-color: #fff;
|
||||||
|
|
||||||
/* White footer */
|
/* White footer */
|
||||||
border-top: 1px solid #e0e5e9;
|
border-top: 1px solid #e0e5e9;
|
||||||
padding: 1.5rem 0;
|
padding: 1.5rem 0;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
|
||||||
/* Added for sticky footer */
|
/* Added for sticky footer */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Responsive adjustments*/
|
/*Responsive adjustments*/
|
||||||
@media (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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,269 +7,268 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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 setupAjaxNavigation() {
|
||||||
|
// Configuration
|
||||||
|
const config = {
|
||||||
|
mainContentSelector: "#main-content", // Selector for the main content area
|
||||||
|
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
|
||||||
|
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
|
||||||
|
excludePatterns: [
|
||||||
|
// URL patterns to exclude from AJAX navigation
|
||||||
|
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
|
||||||
|
/\/admin\//, // Admin pages
|
||||||
|
/\/accounts\/logout\//, // Logout page
|
||||||
|
/\/api\//, // API endpoints
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create and insert the loading indicator
|
||||||
|
if (!document.getElementById(config.loadingIndicatorId)) {
|
||||||
|
const loadingIndicator = document.createElement("div");
|
||||||
|
loadingIndicator.id = config.loadingIndicatorId;
|
||||||
|
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
|
||||||
|
loadingIndicator.innerHTML =
|
||||||
|
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
|
||||||
|
loadingIndicator.style.display = "none";
|
||||||
|
loadingIndicator.style.zIndex = "9999";
|
||||||
|
document.body.appendChild(loadingIndicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to set up AJAX navigation for the application
|
// Get the loading indicator element
|
||||||
function setupAjaxNavigation() {
|
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
|
||||||
// Configuration
|
|
||||||
const config = {
|
|
||||||
mainContentSelector: "#main-content", // Selector for the main content area
|
|
||||||
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
|
|
||||||
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
|
|
||||||
excludePatterns: [
|
|
||||||
// URL patterns to exclude from AJAX navigation
|
|
||||||
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
|
|
||||||
/\/admin\//, // Admin pages
|
|
||||||
/\/accounts\/logout\//, // Logout page
|
|
||||||
/\/api\//, // API endpoints
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create and insert the loading indicator
|
// Get the main content container
|
||||||
if (!document.getElementById(config.loadingIndicatorId)) {
|
const mainContent = document.querySelector(config.mainContentSelector);
|
||||||
const loadingIndicator = document.createElement("div");
|
if (!mainContent) {
|
||||||
loadingIndicator.id = config.loadingIndicatorId;
|
console.warn("Main content container not found. AJAX navigation disabled.");
|
||||||
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
|
return;
|
||||||
loadingIndicator.innerHTML =
|
}
|
||||||
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
|
|
||||||
loadingIndicator.style.display = "none";
|
// Function to check if a URL should be excluded from AJAX navigation
|
||||||
loadingIndicator.style.zIndex = "9999";
|
function shouldExcludeUrl(url) {
|
||||||
document.body.appendChild(loadingIndicator);
|
for (const pattern of config.excludePatterns) {
|
||||||
|
if (pattern.test(url)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the loading indicator element
|
// Function to show the loading indicator
|
||||||
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
|
function showLoading() {
|
||||||
|
loadingIndicator.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
// Get the main content container
|
// Function to hide the loading indicator
|
||||||
const mainContent = document.querySelector(config.mainContentSelector);
|
function hideLoading() {
|
||||||
if (!mainContent) {
|
loadingIndicator.style.display = "none";
|
||||||
console.warn("Main content container not found. AJAX navigation disabled.");
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to check if a URL should be excluded from AJAX navigation
|
// Function to handle AJAX page navigation
|
||||||
function shouldExcludeUrl(url) {
|
function handlePageNavigation(url, pushState = true) {
|
||||||
for (const pattern of config.excludePatterns) {
|
if (shouldExcludeUrl(url)) {
|
||||||
if (pattern.test(url)) {
|
window.location.href = url;
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
}
|
showLoading();
|
||||||
return false;
|
const currentScrollPos = window.scrollY;
|
||||||
}
|
fetch(url, {
|
||||||
|
headers: {
|
||||||
// Function to show the loading indicator
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
function showLoading() {
|
"X-AJAX-Navigation": "true",
|
||||||
loadingIndicator.style.display = "block";
|
Accept: "text/html",
|
||||||
}
|
},
|
||||||
|
})
|
||||||
// Function to hide the loading indicator
|
.then((response) => {
|
||||||
function hideLoading() {
|
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
|
||||||
loadingIndicator.style.display = "none";
|
return response.text();
|
||||||
}
|
})
|
||||||
|
.then((html) => {
|
||||||
// Function to handle AJAX page navigation
|
// Parse the HTML and extract #main-content
|
||||||
function handlePageNavigation(url, pushState = true) {
|
const tempDiv = document.createElement("div");
|
||||||
if (shouldExcludeUrl(url)) {
|
tempDiv.innerHTML = html;
|
||||||
window.location.href = url;
|
const newContent = tempDiv.querySelector(config.mainContentSelector);
|
||||||
return;
|
if (!newContent) throw new Error("Could not find main content in the response");
|
||||||
}
|
mainContent.innerHTML = newContent.innerHTML;
|
||||||
showLoading();
|
// Update the page title
|
||||||
const currentScrollPos = window.scrollY;
|
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
||||||
fetch(url, {
|
if (titleMatch) document.title = titleMatch[1];
|
||||||
headers: {
|
// Re-initialize dynamic content
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
reloadScripts(mainContent);
|
||||||
"X-AJAX-Navigation": "true",
|
attachEventListeners();
|
||||||
Accept: "text/html",
|
initializePageScripts();
|
||||||
},
|
if (pushState) {
|
||||||
})
|
history.pushState(
|
||||||
.then((response) => {
|
{ url: url, title: document.title, scrollPos: currentScrollPos },
|
||||||
if (!response.ok)
|
document.title,
|
||||||
throw new Error(`Network response was not ok: ${response.status}`);
|
url,
|
||||||
return response.text();
|
);
|
||||||
})
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
.then((html) => {
|
} else if (window.history.state && window.history.state.scrollPos) {
|
||||||
// Parse the HTML and extract #main-content
|
window.scrollTo({ top: window.history.state.scrollPos });
|
||||||
const tempDiv = document.createElement("div");
|
}
|
||||||
tempDiv.innerHTML = html;
|
hideLoading();
|
||||||
const newContent = tempDiv.querySelector(config.mainContentSelector);
|
})
|
||||||
if (!newContent) throw new Error("Could not find main content in the response");
|
.catch((error) => {
|
||||||
mainContent.innerHTML = newContent.innerHTML;
|
console.error("Error during AJAX navigation:", error);
|
||||||
// Update the page title
|
hideLoading();
|
||||||
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
window.location.href = url;
|
||||||
if (titleMatch) document.title = titleMatch[1];
|
|
||||||
// Re-initialize dynamic content
|
|
||||||
reloadScripts(mainContent);
|
|
||||||
attachEventListeners();
|
|
||||||
initializePageScripts();
|
|
||||||
if (pushState) {
|
|
||||||
history.pushState(
|
|
||||||
{ url: url, title: document.title, scrollPos: currentScrollPos },
|
|
||||||
document.title,
|
|
||||||
url,
|
|
||||||
);
|
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
||||||
} else if (window.history.state && window.history.state.scrollPos) {
|
|
||||||
window.scrollTo({ top: window.history.state.scrollPos });
|
|
||||||
}
|
|
||||||
hideLoading();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error during AJAX navigation:", error);
|
|
||||||
hideLoading();
|
|
||||||
window.location.href = url;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to reload and execute scripts in new content
|
|
||||||
function reloadScripts(container) {
|
|
||||||
const scripts = container.getElementsByTagName("script");
|
|
||||||
for (let script of scripts) {
|
|
||||||
const newScript = document.createElement("script");
|
|
||||||
|
|
||||||
// Copy all attributes
|
|
||||||
Array.from(script.attributes).forEach((attr) => {
|
|
||||||
newScript.setAttribute(attr.name, attr.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Copy inline script content
|
|
||||||
newScript.textContent = script.textContent;
|
|
||||||
|
|
||||||
// Replace old script with new one
|
|
||||||
script.parentNode.replaceChild(newScript, script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to handle form submissions
|
|
||||||
function handleFormSubmission(form, e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Show loading indicator
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
// Get form data
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const method = form.method.toLowerCase();
|
|
||||||
const url = form.action || window.location.href;
|
|
||||||
|
|
||||||
// Configure fetch options
|
|
||||||
const fetchOptions = {
|
|
||||||
method: method,
|
|
||||||
headers: {
|
|
||||||
"X-AJAX-Navigation": "true",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle different HTTP methods
|
|
||||||
if (method === "get") {
|
|
||||||
const queryParams = new URLSearchParams(formData).toString();
|
|
||||||
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
|
|
||||||
} else {
|
|
||||||
fetchOptions.body = formData;
|
|
||||||
|
|
||||||
fetch(url, fetchOptions)
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
if (data.redirect) {
|
|
||||||
// Handle server-side redirects
|
|
||||||
handlePageNavigation(data.redirect, true);
|
|
||||||
} else {
|
|
||||||
// Update page content
|
|
||||||
mainContent.innerHTML = data.html;
|
|
||||||
document.title = data.title || document.title;
|
|
||||||
|
|
||||||
// Re-initialize dynamic content
|
|
||||||
reloadScripts(mainContent);
|
|
||||||
attachEventListeners();
|
|
||||||
initializePageScripts();
|
|
||||||
|
|
||||||
// Update URL if needed
|
|
||||||
if (data.url) {
|
|
||||||
history.pushState({ url: data.url }, document.title, data.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Form submission error:", error);
|
|
||||||
// Fallback to traditional form submission
|
|
||||||
form.submit();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
hideLoading();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to initialize scripts needed for the new page content
|
|
||||||
function initializePageScripts() {
|
|
||||||
// Re-initialize any custom scripts that might be needed
|
|
||||||
if (typeof setupAjaxPagination === "function") {
|
|
||||||
setupAjaxPagination();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Bootstrap tooltips, popovers, etc.
|
|
||||||
if (typeof bootstrap !== "undefined") {
|
|
||||||
// Initialize tooltips
|
|
||||||
const tooltipTriggerList = [].slice.call(
|
|
||||||
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
|
|
||||||
);
|
|
||||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize popovers
|
|
||||||
const popoverTriggerList = [].slice.call(
|
|
||||||
document.querySelectorAll('[data-bs-toggle="popover"]'),
|
|
||||||
);
|
|
||||||
popoverTriggerList.map(function (popoverTriggerEl) {
|
|
||||||
return new bootstrap.Popover(popoverTriggerEl);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to attach event listeners to forms and links
|
|
||||||
function attachEventListeners() {
|
|
||||||
// Handle AJAX navigation links
|
|
||||||
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
|
|
||||||
if (!link.dataset.ajaxNavInitialized) {
|
|
||||||
link.addEventListener("click", function (e) {
|
|
||||||
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
|
|
||||||
return; // Let the browser handle these cases
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
handlePageNavigation(this.href);
|
|
||||||
});
|
|
||||||
link.dataset.ajaxNavInitialized = "true";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle forms with AJAX
|
|
||||||
document
|
|
||||||
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
|
|
||||||
.forEach((form) => {
|
|
||||||
if (!form.dataset.ajaxFormInitialized) {
|
|
||||||
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
|
|
||||||
form.dataset.ajaxFormInitialized = "true";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial attachment of event listeners
|
|
||||||
attachEventListeners();
|
|
||||||
|
|
||||||
// Handle browser back/forward buttons
|
|
||||||
window.addEventListener("popstate", function (event) {
|
|
||||||
if (event.state && event.state.url) {
|
|
||||||
handlePageNavigation(event.state.url, false);
|
|
||||||
} else {
|
|
||||||
// Fallback to current URL if no state
|
|
||||||
handlePageNavigation(window.location.href, false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to reload and execute scripts in new content
|
||||||
|
function reloadScripts(container) {
|
||||||
|
const scripts = container.getElementsByTagName("script");
|
||||||
|
for (let script of scripts) {
|
||||||
|
const newScript = document.createElement("script");
|
||||||
|
|
||||||
|
// Copy all attributes
|
||||||
|
Array.from(script.attributes).forEach((attr) => {
|
||||||
|
newScript.setAttribute(attr.name, attr.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy inline script content
|
||||||
|
newScript.textContent = script.textContent;
|
||||||
|
|
||||||
|
// Replace old script with new one
|
||||||
|
script.parentNode.replaceChild(newScript, script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to handle form submissions
|
||||||
|
function handleFormSubmission(form, e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
// Get form data
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const method = form.method.toLowerCase();
|
||||||
|
const url = form.action || window.location.href;
|
||||||
|
|
||||||
|
// Configure fetch options
|
||||||
|
const fetchOptions = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
"X-AJAX-Navigation": "true",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle different HTTP methods
|
||||||
|
if (method === "get") {
|
||||||
|
const queryParams = new URLSearchParams(formData).toString();
|
||||||
|
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
|
||||||
|
} else {
|
||||||
|
fetchOptions.body = formData;
|
||||||
|
|
||||||
|
fetch(url, fetchOptions)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (data.redirect) {
|
||||||
|
// Handle server-side redirects
|
||||||
|
handlePageNavigation(data.redirect, true);
|
||||||
|
} else {
|
||||||
|
// Update page content
|
||||||
|
mainContent.innerHTML = data.html;
|
||||||
|
document.title = data.title || document.title;
|
||||||
|
|
||||||
|
// Re-initialize dynamic content
|
||||||
|
reloadScripts(mainContent);
|
||||||
|
attachEventListeners();
|
||||||
|
initializePageScripts();
|
||||||
|
|
||||||
|
// Update URL if needed
|
||||||
|
if (data.url) {
|
||||||
|
history.pushState({ url: data.url }, document.title, data.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Form submission error:", error);
|
||||||
|
// Fallback to traditional form submission
|
||||||
|
form.submit();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
hideLoading();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to initialize scripts needed for the new page content
|
||||||
|
function initializePageScripts() {
|
||||||
|
// Re-initialize any custom scripts that might be needed
|
||||||
|
if (typeof setupAjaxPagination === "function") {
|
||||||
|
setupAjaxPagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Bootstrap tooltips, popovers, etc.
|
||||||
|
if (typeof bootstrap !== "undefined") {
|
||||||
|
// Initialize tooltips
|
||||||
|
const tooltipTriggerList = [].slice.call(
|
||||||
|
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
|
||||||
|
);
|
||||||
|
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize popovers
|
||||||
|
const popoverTriggerList = [].slice.call(
|
||||||
|
document.querySelectorAll('[data-bs-toggle="popover"]'),
|
||||||
|
);
|
||||||
|
popoverTriggerList.map(function (popoverTriggerEl) {
|
||||||
|
return new bootstrap.Popover(popoverTriggerEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to attach event listeners to forms and links
|
||||||
|
function attachEventListeners() {
|
||||||
|
// Handle AJAX navigation links
|
||||||
|
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
|
||||||
|
if (!link.dataset.ajaxNavInitialized) {
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
|
||||||
|
return; // Let the browser handle these cases
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
handlePageNavigation(this.href);
|
||||||
|
});
|
||||||
|
link.dataset.ajaxNavInitialized = "true";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle forms with AJAX
|
||||||
|
document
|
||||||
|
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
|
||||||
|
.forEach((form) => {
|
||||||
|
if (!form.dataset.ajaxFormInitialized) {
|
||||||
|
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
|
||||||
|
form.dataset.ajaxFormInitialized = "true";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial attachment of event listeners
|
||||||
|
attachEventListeners();
|
||||||
|
|
||||||
|
// Handle browser back/forward buttons
|
||||||
|
window.addEventListener("popstate", function (event) {
|
||||||
|
if (event.state && event.state.url) {
|
||||||
|
handlePageNavigation(event.state.url, false);
|
||||||
|
} else {
|
||||||
|
// Fallback to current URL if no state
|
||||||
|
handlePageNavigation(window.location.href, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,101 +7,101 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
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
|
|
||||||
loadingSpinner.classList.add("d-none");
|
|
||||||
contentContainer.classList.remove("d-none");
|
|
||||||
|
|
||||||
// Scroll to top of the content container
|
|
||||||
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error fetching data:", error);
|
|
||||||
loadingSpinner.classList.add("d-none");
|
|
||||||
contentContainer.classList.remove("d-none");
|
|
||||||
alert(config.retryMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial setup of event listeners
|
|
||||||
setupPaginationListeners();
|
|
||||||
|
|
||||||
// Handle browser back/forward buttons
|
|
||||||
window.addEventListener("popstate", function (event) {
|
|
||||||
if (event.state && event.state.url) {
|
|
||||||
handleAjaxNavigation(event.state.url);
|
|
||||||
} else {
|
|
||||||
// If no state, fetch current URL
|
|
||||||
handleAjaxNavigation(window.location.href);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide loading spinner, show content
|
||||||
|
loadingSpinner.classList.add("d-none");
|
||||||
|
contentContainer.classList.remove("d-none");
|
||||||
|
|
||||||
|
// Scroll to top of the content container
|
||||||
|
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
loadingSpinner.classList.add("d-none");
|
||||||
|
contentContainer.classList.remove("d-none");
|
||||||
|
alert(config.retryMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial setup of event listeners
|
||||||
|
setupPaginationListeners();
|
||||||
|
|
||||||
|
// Handle browser back/forward buttons
|
||||||
|
window.addEventListener("popstate", function (event) {
|
||||||
|
if (event.state && event.state.url) {
|
||||||
|
handleAjaxNavigation(event.state.url);
|
||||||
|
} else {
|
||||||
|
// If no state, fetch current URL
|
||||||
|
handleAjaxNavigation(window.location.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,478 +8,469 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
// Set up Plotly default config based on theme
|
// Set up Plotly default config based on theme
|
||||||
function updatePlotlyTheme() {
|
function updatePlotlyTheme() {
|
||||||
// Force a fresh check of the current theme
|
// Force a fresh check of the current theme
|
||||||
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
|
const isDarkMode = document.documentElement.getAttribute("data-bs-theme") === "dark";
|
||||||
console.log(
|
console.log("updatePlotlyTheme called - Current theme mode:", isDarkMode ? "dark" : "light");
|
||||||
"updatePlotlyTheme called - Current theme mode:",
|
|
||||||
isDarkMode ? "dark" : "light",
|
|
||||||
);
|
|
||||||
|
|
||||||
window.plotlyDefaultLayout = {
|
window.plotlyDefaultLayout = {
|
||||||
font: {
|
font: {
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
family: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
family:
|
||||||
},
|
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||||
paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
},
|
||||||
plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
paper_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
||||||
colorway: [
|
plot_bgcolor: isDarkMode ? "#343a40" : "#ffffff",
|
||||||
"#4285F4",
|
colorway: [
|
||||||
"#EA4335",
|
"#4285F4",
|
||||||
"#FBBC05",
|
"#EA4335",
|
||||||
"#34A853",
|
"#FBBC05",
|
||||||
"#FF6D00",
|
"#34A853",
|
||||||
"#46BDC6",
|
"#FF6D00",
|
||||||
"#DB4437",
|
"#46BDC6",
|
||||||
"#0F9D58",
|
"#DB4437",
|
||||||
"#AB47BC",
|
"#0F9D58",
|
||||||
"#00ACC1",
|
"#AB47BC",
|
||||||
],
|
"#00ACC1",
|
||||||
margin: {
|
],
|
||||||
l: 50,
|
margin: {
|
||||||
r: 30,
|
l: 50,
|
||||||
t: 30,
|
r: 30,
|
||||||
b: 50,
|
t: 30,
|
||||||
pad: 10,
|
b: 50,
|
||||||
},
|
pad: 10,
|
||||||
hovermode: "closest",
|
},
|
||||||
xaxis: {
|
hovermode: "closest",
|
||||||
automargin: true,
|
xaxis: {
|
||||||
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
automargin: true,
|
||||||
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
||||||
title: {
|
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
||||||
font: {
|
title: {
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
font: {
|
||||||
},
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
},
|
},
|
||||||
tickfont: {
|
},
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
tickfont: {
|
||||||
},
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
},
|
},
|
||||||
yaxis: {
|
},
|
||||||
automargin: true,
|
yaxis: {
|
||||||
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
automargin: true,
|
||||||
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
gridcolor: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)",
|
||||||
title: {
|
zerolinecolor: isDarkMode ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.2)",
|
||||||
font: {
|
title: {
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
font: {
|
||||||
},
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
},
|
},
|
||||||
tickfont: {
|
},
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
tickfont: {
|
||||||
},
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
},
|
},
|
||||||
legend: {
|
},
|
||||||
font: {
|
legend: {
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
font: {
|
||||||
},
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
},
|
||||||
},
|
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
||||||
modebar: {
|
},
|
||||||
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
modebar: {
|
||||||
color: isDarkMode ? "#f8f9fa" : "#212529",
|
bgcolor: isDarkMode ? "rgba(52, 58, 64, 0.8)" : "rgba(255, 255, 255, 0.8)",
|
||||||
activecolor: isDarkMode ? "#6ea8fe" : "#007bff",
|
color: isDarkMode ? "#f8f9fa" : "#212529",
|
||||||
},
|
activecolor: isDarkMode ? "#6ea8fe" : "#007bff",
|
||||||
};
|
},
|
||||||
|
|
||||||
// Config for specific chart types
|
|
||||||
window.plotlyBarConfig = {
|
|
||||||
...window.plotlyDefaultLayout,
|
|
||||||
bargap: 0.1,
|
|
||||||
bargroupgap: 0.2,
|
|
||||||
};
|
|
||||||
|
|
||||||
window.plotlyPieConfig = {
|
|
||||||
...window.plotlyDefaultLayout,
|
|
||||||
showlegend: true,
|
|
||||||
legend: {
|
|
||||||
...window.plotlyDefaultLayout.legend,
|
|
||||||
xanchor: "center",
|
|
||||||
yanchor: "top",
|
|
||||||
y: -0.2,
|
|
||||||
x: 0.5,
|
|
||||||
orientation: "h",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme setting
|
|
||||||
updatePlotlyTheme();
|
|
||||||
|
|
||||||
// Listen for theme changes
|
|
||||||
const observer = new MutationObserver(function (mutations) {
|
|
||||||
mutations.forEach(function (mutation) {
|
|
||||||
if (mutation.attributeName === "data-bs-theme") {
|
|
||||||
console.log(
|
|
||||||
"Theme changed detected by observer:",
|
|
||||||
document.documentElement.getAttribute("data-bs-theme"),
|
|
||||||
);
|
|
||||||
updatePlotlyTheme();
|
|
||||||
// Use a small delay to ensure styles have been applied
|
|
||||||
setTimeout(refreshAllCharts, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, { attributes: true });
|
|
||||||
|
|
||||||
// Chart responsiveness
|
|
||||||
function resizeCharts() {
|
|
||||||
const charts = document.querySelectorAll(".chart-container");
|
|
||||||
charts.forEach((chart) => {
|
|
||||||
if (chart.id && window.Plotly) {
|
|
||||||
Plotly.relayout(chart.id, {
|
|
||||||
"xaxis.automargin": true,
|
|
||||||
"yaxis.automargin": true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh all charts with current theme
|
|
||||||
function refreshAllCharts() {
|
|
||||||
if (!window.Plotly) return;
|
|
||||||
|
|
||||||
const currentTheme = document.documentElement.getAttribute("data-bs-theme");
|
|
||||||
console.log("Refreshing charts with theme:", currentTheme);
|
|
||||||
|
|
||||||
// Update the theme settings
|
|
||||||
updatePlotlyTheme();
|
|
||||||
|
|
||||||
const charts = document.querySelectorAll(".chart-container");
|
|
||||||
charts.forEach(function (chart) {
|
|
||||||
if (chart.id) {
|
|
||||||
try {
|
|
||||||
// Safe way to check if element has a plot
|
|
||||||
const plotElement = document.getElementById(chart.id);
|
|
||||||
if (plotElement && plotElement._fullLayout) {
|
|
||||||
console.log("Updating chart theme for:", chart.id);
|
|
||||||
|
|
||||||
// Determine chart type to apply appropriate settings
|
|
||||||
let layoutUpdate = { ...window.plotlyDefaultLayout };
|
|
||||||
|
|
||||||
// Check if it's a bar chart
|
|
||||||
if (
|
|
||||||
plotElement.data &&
|
|
||||||
plotElement.data.some((trace) => trace.type === "bar")
|
|
||||||
) {
|
|
||||||
layoutUpdate = { ...window.plotlyBarConfig };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a pie chart
|
|
||||||
if (
|
|
||||||
plotElement.data &&
|
|
||||||
plotElement.data.some((trace) => trace.type === "pie")
|
|
||||||
) {
|
|
||||||
layoutUpdate = { ...window.plotlyPieConfig };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force paper and plot background colors based on current theme
|
|
||||||
// This ensures the chart background always matches the current theme
|
|
||||||
layoutUpdate.paper_bgcolor =
|
|
||||||
currentTheme === "dark" ? "#343a40" : "#ffffff";
|
|
||||||
layoutUpdate.plot_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
|
|
||||||
|
|
||||||
// Update font colors too
|
|
||||||
layoutUpdate.font.color = currentTheme === "dark" ? "#f8f9fa" : "#212529";
|
|
||||||
|
|
||||||
// Apply layout updates
|
|
||||||
Plotly.relayout(chart.id, layoutUpdate);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error updating chart theme:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make refreshAllCharts available globally
|
|
||||||
window.refreshAllCharts = refreshAllCharts;
|
|
||||||
|
|
||||||
// Handle window resize
|
|
||||||
window.addEventListener("resize", function () {
|
|
||||||
if (window.Plotly) {
|
|
||||||
resizeCharts();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call resizeCharts on initial load
|
|
||||||
if (window.Plotly) {
|
|
||||||
// Use a longer delay to ensure charts are fully loaded
|
|
||||||
setTimeout(function () {
|
|
||||||
updatePlotlyTheme();
|
|
||||||
refreshAllCharts();
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply theme to newly created charts
|
|
||||||
const originalPlotlyNewPlot = Plotly.newPlot;
|
|
||||||
Plotly.newPlot = function () {
|
|
||||||
const args = Array.from(arguments);
|
|
||||||
// Get the layout argument (3rd argument)
|
|
||||||
if (args.length >= 3 && typeof args[2] === "object") {
|
|
||||||
// Ensure plotlyDefaultLayout is up to date
|
|
||||||
updatePlotlyTheme();
|
|
||||||
// Apply current theme to new plot
|
|
||||||
args[2] = { ...window.plotlyDefaultLayout, ...args[2] };
|
|
||||||
}
|
|
||||||
return originalPlotlyNewPlot.apply(this, args);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Time range filtering
|
// Config for specific chart types
|
||||||
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
window.plotlyBarConfig = {
|
||||||
if (timeRangeDropdown) {
|
...window.plotlyDefaultLayout,
|
||||||
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
bargap: 0.1,
|
||||||
timeRangeLinks.forEach((link) => {
|
bargroupgap: 0.2,
|
||||||
link.addEventListener("click", function (e) {
|
};
|
||||||
const url = new URL(this.href);
|
|
||||||
const dashboardId = url.searchParams.get("dashboard_id");
|
|
||||||
const timeRange = url.searchParams.get("time_range");
|
|
||||||
|
|
||||||
// Fetch updated data via AJAX
|
window.plotlyPieConfig = {
|
||||||
if (dashboardId) {
|
...window.plotlyDefaultLayout,
|
||||||
fetchDashboardData(dashboardId, timeRange);
|
showlegend: true,
|
||||||
e.preventDefault();
|
legend: {
|
||||||
}
|
...window.plotlyDefaultLayout.legend,
|
||||||
});
|
xanchor: "center",
|
||||||
|
yanchor: "top",
|
||||||
|
y: -0.2,
|
||||||
|
x: 0.5,
|
||||||
|
orientation: "h",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme setting
|
||||||
|
updatePlotlyTheme();
|
||||||
|
|
||||||
|
// Listen for theme changes
|
||||||
|
const observer = new MutationObserver(function (mutations) {
|
||||||
|
mutations.forEach(function (mutation) {
|
||||||
|
if (mutation.attributeName === "data-bs-theme") {
|
||||||
|
console.log(
|
||||||
|
"Theme changed detected by observer:",
|
||||||
|
document.documentElement.getAttribute("data-bs-theme"),
|
||||||
|
);
|
||||||
|
updatePlotlyTheme();
|
||||||
|
// Use a small delay to ensure styles have been applied
|
||||||
|
setTimeout(refreshAllCharts, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.documentElement, { attributes: true });
|
||||||
|
|
||||||
|
// Chart responsiveness
|
||||||
|
function resizeCharts() {
|
||||||
|
const charts = document.querySelectorAll(".chart-container");
|
||||||
|
charts.forEach((chart) => {
|
||||||
|
if (chart.id && window.Plotly) {
|
||||||
|
Plotly.relayout(chart.id, {
|
||||||
|
"xaxis.automargin": true,
|
||||||
|
"yaxis.automargin": true,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh all charts with current theme
|
||||||
|
function refreshAllCharts() {
|
||||||
|
if (!window.Plotly) return;
|
||||||
|
|
||||||
|
const currentTheme = document.documentElement.getAttribute("data-bs-theme");
|
||||||
|
console.log("Refreshing charts with theme:", currentTheme);
|
||||||
|
|
||||||
|
// Update the theme settings
|
||||||
|
updatePlotlyTheme();
|
||||||
|
|
||||||
|
const charts = document.querySelectorAll(".chart-container");
|
||||||
|
charts.forEach(function (chart) {
|
||||||
|
if (chart.id) {
|
||||||
|
try {
|
||||||
|
// Safe way to check if element has a plot
|
||||||
|
const plotElement = document.getElementById(chart.id);
|
||||||
|
if (plotElement && plotElement._fullLayout) {
|
||||||
|
console.log("Updating chart theme for:", chart.id);
|
||||||
|
|
||||||
|
// Determine chart type to apply appropriate settings
|
||||||
|
let layoutUpdate = { ...window.plotlyDefaultLayout };
|
||||||
|
|
||||||
|
// Check if it's a bar chart
|
||||||
|
if (plotElement.data && plotElement.data.some((trace) => trace.type === "bar")) {
|
||||||
|
layoutUpdate = { ...window.plotlyBarConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a pie chart
|
||||||
|
if (plotElement.data && plotElement.data.some((trace) => trace.type === "pie")) {
|
||||||
|
layoutUpdate = { ...window.plotlyPieConfig };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force paper and plot background colors based on current theme
|
||||||
|
// This ensures the chart background always matches the current theme
|
||||||
|
layoutUpdate.paper_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
|
||||||
|
layoutUpdate.plot_bgcolor = currentTheme === "dark" ? "#343a40" : "#ffffff";
|
||||||
|
|
||||||
|
// Update font colors too
|
||||||
|
layoutUpdate.font.color = currentTheme === "dark" ? "#f8f9fa" : "#212529";
|
||||||
|
|
||||||
|
// Apply layout updates
|
||||||
|
Plotly.relayout(chart.id, layoutUpdate);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error updating chart theme:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make refreshAllCharts available globally
|
||||||
|
window.refreshAllCharts = refreshAllCharts;
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener("resize", function () {
|
||||||
|
if (window.Plotly) {
|
||||||
|
resizeCharts();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Function to fetch dashboard data
|
// Call resizeCharts on initial load
|
||||||
function fetchDashboardData(dashboardId, timeRange) {
|
if (window.Plotly) {
|
||||||
const loadingOverlay = document.createElement("div");
|
// Use a longer delay to ensure charts are fully loaded
|
||||||
loadingOverlay.className = "loading-overlay";
|
setTimeout(function () {
|
||||||
loadingOverlay.innerHTML =
|
updatePlotlyTheme();
|
||||||
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
refreshAllCharts();
|
||||||
document.querySelector("main").appendChild(loadingOverlay);
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
|
// Apply theme to newly created charts
|
||||||
.then((response) => {
|
const originalPlotlyNewPlot = Plotly.newPlot;
|
||||||
if (!response.ok) {
|
Plotly.newPlot = function () {
|
||||||
throw new Error(`Network response was not ok: ${response.status}`);
|
const args = Array.from(arguments);
|
||||||
}
|
// Get the layout argument (3rd argument)
|
||||||
return response.json();
|
if (args.length >= 3 && typeof args[2] === "object") {
|
||||||
})
|
// Ensure plotlyDefaultLayout is up to date
|
||||||
.then((data) => {
|
updatePlotlyTheme();
|
||||||
console.log("Dashboard API response:", data);
|
// Apply current theme to new plot
|
||||||
updateDashboardStats(data);
|
args[2] = { ...window.plotlyDefaultLayout, ...args[2] };
|
||||||
updateDashboardCharts(data);
|
}
|
||||||
|
return originalPlotlyNewPlot.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
// Update URL without page reload
|
// Time range filtering
|
||||||
const url = new URL(window.location.href);
|
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
||||||
url.searchParams.set("dashboard_id", dashboardId);
|
if (timeRangeDropdown) {
|
||||||
if (timeRange) {
|
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
||||||
url.searchParams.set("time_range", timeRange);
|
timeRangeLinks.forEach((link) => {
|
||||||
}
|
link.addEventListener("click", function (e) {
|
||||||
window.history.pushState({}, "", url);
|
const url = new URL(this.href);
|
||||||
|
const dashboardId = url.searchParams.get("dashboard_id");
|
||||||
|
const timeRange = url.searchParams.get("time_range");
|
||||||
|
|
||||||
document.querySelector(".loading-overlay").remove();
|
// Fetch updated data via AJAX
|
||||||
})
|
if (dashboardId) {
|
||||||
.catch((error) => {
|
fetchDashboardData(dashboardId, timeRange);
|
||||||
console.error("Error fetching dashboard data:", error);
|
e.preventDefault();
|
||||||
document.querySelector(".loading-overlay").remove();
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Show error message
|
// Function to fetch dashboard data
|
||||||
const alertElement = document.createElement("div");
|
function fetchDashboardData(dashboardId, timeRange) {
|
||||||
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
const loadingOverlay = document.createElement("div");
|
||||||
alertElement.setAttribute("role", "alert");
|
loadingOverlay.className = "loading-overlay";
|
||||||
alertElement.innerHTML = `
|
loadingOverlay.innerHTML =
|
||||||
|
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
||||||
|
document.querySelector("main").appendChild(loadingOverlay);
|
||||||
|
|
||||||
|
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Network response was not ok: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
console.log("Dashboard API response:", data);
|
||||||
|
updateDashboardStats(data);
|
||||||
|
updateDashboardCharts(data);
|
||||||
|
|
||||||
|
// Update URL without page reload
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set("dashboard_id", dashboardId);
|
||||||
|
if (timeRange) {
|
||||||
|
url.searchParams.set("time_range", timeRange);
|
||||||
|
}
|
||||||
|
window.history.pushState({}, "", url);
|
||||||
|
|
||||||
|
document.querySelector(".loading-overlay").remove();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching dashboard data:", error);
|
||||||
|
document.querySelector(".loading-overlay").remove();
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
const alertElement = document.createElement("div");
|
||||||
|
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
||||||
|
alertElement.setAttribute("role", "alert");
|
||||||
|
alertElement.innerHTML = `
|
||||||
Error loading dashboard data. Please try again.
|
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 updateDashboardStats(data) {
|
||||||
|
// Update total sessions
|
||||||
|
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
||||||
|
if (totalSessionsElement) {
|
||||||
|
totalSessionsElement.textContent = data.total_sessions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to update dashboard statistics
|
// Update average response time
|
||||||
function updateDashboardStats(data) {
|
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
||||||
// Update total sessions
|
if (avgResponseTimeElement) {
|
||||||
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
||||||
if (totalSessionsElement) {
|
|
||||||
totalSessionsElement.textContent = data.total_sessions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update average response time
|
|
||||||
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
|
||||||
if (avgResponseTimeElement) {
|
|
||||||
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update total tokens
|
|
||||||
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
|
||||||
if (totalTokensElement) {
|
|
||||||
totalTokensElement.textContent = data.total_tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update total cost
|
|
||||||
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
|
||||||
if (totalCostElement) {
|
|
||||||
totalCostElement.textContent = "€" + data.total_cost;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to update dashboard charts
|
// Update total tokens
|
||||||
function updateDashboardCharts(data) {
|
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
||||||
// Check if Plotly is available
|
if (totalTokensElement) {
|
||||||
if (!window.Plotly) {
|
totalTokensElement.textContent = data.total_tokens;
|
||||||
console.error("Plotly library not loaded!");
|
|
||||||
document.querySelectorAll(".chart-container").forEach((container) => {
|
|
||||||
container.innerHTML =
|
|
||||||
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update sessions over time chart
|
|
||||||
const timeSeriesData = data.time_series_data;
|
|
||||||
if (timeSeriesData && timeSeriesData.length > 0) {
|
|
||||||
try {
|
|
||||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
|
||||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
|
||||||
|
|
||||||
Plotly.react(
|
|
||||||
"sessions-time-chart",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
x: timeSeriesX,
|
|
||||||
y: timeSeriesY,
|
|
||||||
type: "scatter",
|
|
||||||
mode: "lines+markers",
|
|
||||||
line: {
|
|
||||||
color: "rgb(75, 192, 192)",
|
|
||||||
width: 2,
|
|
||||||
},
|
|
||||||
marker: {
|
|
||||||
color: "rgb(75, 192, 192)",
|
|
||||||
size: 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
...window.plotlyDefaultLayout,
|
|
||||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
|
||||||
xaxis: {
|
|
||||||
...window.plotlyDefaultLayout.xaxis,
|
|
||||||
title: "Date",
|
|
||||||
},
|
|
||||||
yaxis: {
|
|
||||||
...window.plotlyDefaultLayout.yaxis,
|
|
||||||
title: "Number of Sessions",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error rendering time series chart:", error);
|
|
||||||
document.getElementById("sessions-time-chart").innerHTML =
|
|
||||||
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.getElementById("sessions-time-chart").innerHTML =
|
|
||||||
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update sentiment chart
|
|
||||||
const sentimentData = data.sentiment_data;
|
|
||||||
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
|
||||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
|
||||||
const sentimentValues = sentimentData.map((item) => item.count);
|
|
||||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
|
||||||
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
|
||||||
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
|
||||||
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
|
||||||
return "rgb(201, 203, 207)";
|
|
||||||
});
|
|
||||||
|
|
||||||
Plotly.react(
|
|
||||||
"sentiment-chart",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
values: sentimentValues,
|
|
||||||
labels: sentimentLabels,
|
|
||||||
type: "pie",
|
|
||||||
marker: {
|
|
||||||
colors: sentimentColors,
|
|
||||||
},
|
|
||||||
hole: 0.4,
|
|
||||||
textinfo: "label+percent",
|
|
||||||
insidetextorientation: "radial",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
...window.plotlyDefaultLayout,
|
|
||||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update country chart
|
|
||||||
const countryData = data.country_data;
|
|
||||||
if (countryData && countryData.length > 0 && window.Plotly) {
|
|
||||||
const countryLabels = countryData.map((item) => item.country);
|
|
||||||
const countryValues = countryData.map((item) => item.count);
|
|
||||||
|
|
||||||
Plotly.react(
|
|
||||||
"country-chart",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
x: countryValues,
|
|
||||||
y: countryLabels,
|
|
||||||
type: "bar",
|
|
||||||
orientation: "h",
|
|
||||||
marker: {
|
|
||||||
color: "rgb(54, 162, 235)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
...window.plotlyDefaultLayout,
|
|
||||||
margin: { t: 10, r: 10, b: 40, l: 100 },
|
|
||||||
xaxis: {
|
|
||||||
...window.plotlyDefaultLayout.xaxis,
|
|
||||||
title: "Number of Sessions",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update category chart
|
|
||||||
const categoryData = data.category_data;
|
|
||||||
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
|
||||||
const categoryLabels = categoryData.map((item) => item.category);
|
|
||||||
const categoryValues = categoryData.map((item) => item.count);
|
|
||||||
|
|
||||||
Plotly.react(
|
|
||||||
"category-chart",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
labels: categoryLabels,
|
|
||||||
values: categoryValues,
|
|
||||||
type: "pie",
|
|
||||||
textinfo: "label+percent",
|
|
||||||
insidetextorientation: "radial",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
...window.plotlyDefaultLayout,
|
|
||||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dashboard selector
|
// Update total cost
|
||||||
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
||||||
dashboardSelector.forEach((link) => {
|
if (totalCostElement) {
|
||||||
link.addEventListener("click", function (e) {
|
totalCostElement.textContent = "€" + data.total_cost;
|
||||||
const url = new URL(this.href);
|
}
|
||||||
const dashboardId = url.searchParams.get("dashboard_id");
|
}
|
||||||
|
|
||||||
// Fetch updated data via AJAX
|
// Function to update dashboard charts
|
||||||
if (dashboardId) {
|
function updateDashboardCharts(data) {
|
||||||
fetchDashboardData(dashboardId);
|
// Check if Plotly is available
|
||||||
e.preventDefault();
|
if (!window.Plotly) {
|
||||||
}
|
console.error("Plotly library not loaded!");
|
||||||
});
|
document.querySelectorAll(".chart-container").forEach((container) => {
|
||||||
|
container.innerHTML =
|
||||||
|
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sessions over time chart
|
||||||
|
const timeSeriesData = data.time_series_data;
|
||||||
|
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||||
|
try {
|
||||||
|
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||||
|
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.react(
|
||||||
|
"sessions-time-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
x: timeSeriesX,
|
||||||
|
y: timeSeriesY,
|
||||||
|
type: "scatter",
|
||||||
|
mode: "lines+markers",
|
||||||
|
line: {
|
||||||
|
color: "rgb(75, 192, 192)",
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
color: "rgb(75, 192, 192)",
|
||||||
|
size: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
...window.plotlyDefaultLayout,
|
||||||
|
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||||
|
xaxis: {
|
||||||
|
...window.plotlyDefaultLayout.xaxis,
|
||||||
|
title: "Date",
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
...window.plotlyDefaultLayout.yaxis,
|
||||||
|
title: "Number of Sessions",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error rendering time series chart:", error);
|
||||||
|
document.getElementById("sessions-time-chart").innerHTML =
|
||||||
|
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById("sessions-time-chart").innerHTML =
|
||||||
|
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sentiment chart
|
||||||
|
const sentimentData = data.sentiment_data;
|
||||||
|
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
||||||
|
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||||
|
const sentimentValues = sentimentData.map((item) => item.count);
|
||||||
|
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||||
|
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||||
|
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||||
|
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||||
|
return "rgb(201, 203, 207)";
|
||||||
|
});
|
||||||
|
|
||||||
|
Plotly.react(
|
||||||
|
"sentiment-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
values: sentimentValues,
|
||||||
|
labels: sentimentLabels,
|
||||||
|
type: "pie",
|
||||||
|
marker: {
|
||||||
|
colors: sentimentColors,
|
||||||
|
},
|
||||||
|
hole: 0.4,
|
||||||
|
textinfo: "label+percent",
|
||||||
|
insidetextorientation: "radial",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
...window.plotlyDefaultLayout,
|
||||||
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update country chart
|
||||||
|
const countryData = data.country_data;
|
||||||
|
if (countryData && countryData.length > 0 && window.Plotly) {
|
||||||
|
const countryLabels = countryData.map((item) => item.country);
|
||||||
|
const countryValues = countryData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.react(
|
||||||
|
"country-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
x: countryValues,
|
||||||
|
y: countryLabels,
|
||||||
|
type: "bar",
|
||||||
|
orientation: "h",
|
||||||
|
marker: {
|
||||||
|
color: "rgb(54, 162, 235)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
...window.plotlyDefaultLayout,
|
||||||
|
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||||
|
xaxis: {
|
||||||
|
...window.plotlyDefaultLayout.xaxis,
|
||||||
|
title: "Number of Sessions",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update category chart
|
||||||
|
const categoryData = data.category_data;
|
||||||
|
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
||||||
|
const categoryLabels = categoryData.map((item) => item.category);
|
||||||
|
const categoryValues = categoryData.map((item) => item.count);
|
||||||
|
|
||||||
|
Plotly.react(
|
||||||
|
"category-chart",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
labels: categoryLabels,
|
||||||
|
values: categoryValues,
|
||||||
|
type: "pie",
|
||||||
|
textinfo: "label+percent",
|
||||||
|
insidetextorientation: "radial",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
...window.plotlyDefaultLayout,
|
||||||
|
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dashboard selector
|
||||||
|
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
||||||
|
dashboardSelector.forEach((link) => {
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
const url = new URL(this.href);
|
||||||
|
const dashboardId = url.searchParams.get("dashboard_id");
|
||||||
|
|
||||||
|
// Fetch updated data via AJAX
|
||||||
|
if (dashboardId) {
|
||||||
|
fetchDashboardData(dashboardId);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,241 +7,241 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
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
|
||||||
|
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||||
|
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||||
|
return new bootstrap.Popover(popoverTriggerEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle sidebar on mobile
|
||||||
|
const sidebarToggle = document.querySelector("#sidebarToggle");
|
||||||
|
if (sidebarToggle) {
|
||||||
|
sidebarToggle.addEventListener("click", function () {
|
||||||
|
document.querySelector(".sidebar").classList.toggle("show");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize popovers
|
// Auto-dismiss alerts after 5 seconds
|
||||||
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
setTimeout(function () {
|
||||||
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
||||||
return new bootstrap.Popover(popoverTriggerEl);
|
alerts.forEach(function (alert) {
|
||||||
|
if (alert && bootstrap.Alert.getInstance(alert)) {
|
||||||
|
bootstrap.Alert.getInstance(alert).close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
// Toggle sidebar on mobile
|
// Form validation
|
||||||
const sidebarToggle = document.querySelector("#sidebarToggle");
|
const forms = document.querySelectorAll(".needs-validation");
|
||||||
if (sidebarToggle) {
|
forms.forEach(function (form) {
|
||||||
sidebarToggle.addEventListener("click", function () {
|
form.addEventListener(
|
||||||
document.querySelector(".sidebar").classList.toggle("show");
|
"submit",
|
||||||
});
|
function (event) {
|
||||||
}
|
if (!form.checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
form.classList.add("was-validated");
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Auto-dismiss alerts after 5 seconds
|
// Confirm dialogs
|
||||||
setTimeout(function () {
|
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
||||||
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
confirmButtons.forEach(function (button) {
|
||||||
alerts.forEach(function (alert) {
|
button.addEventListener("click", function (event) {
|
||||||
if (alert && bootstrap.Alert.getInstance(alert)) {
|
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
||||||
bootstrap.Alert.getInstance(alert).close();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
// Form validation
|
|
||||||
const forms = document.querySelectorAll(".needs-validation");
|
|
||||||
forms.forEach(function (form) {
|
|
||||||
form.addEventListener(
|
|
||||||
"submit",
|
|
||||||
function (event) {
|
|
||||||
if (!form.checkValidity()) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
form.classList.add("was-validated");
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Confirm dialogs
|
// Back button
|
||||||
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
const backButtons = document.querySelectorAll(".btn-back");
|
||||||
confirmButtons.forEach(function (button) {
|
backButtons.forEach(function (button) {
|
||||||
button.addEventListener("click", function (event) {
|
button.addEventListener("click", function (event) {
|
||||||
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
event.preventDefault();
|
||||||
event.preventDefault();
|
window.history.back();
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Back button
|
// File input customization
|
||||||
const backButtons = document.querySelectorAll(".btn-back");
|
const fileInputs = document.querySelectorAll(".custom-file-input");
|
||||||
backButtons.forEach(function (button) {
|
fileInputs.forEach(function (input) {
|
||||||
button.addEventListener("click", function (event) {
|
input.addEventListener("change", function (e) {
|
||||||
event.preventDefault();
|
const fileName = this.files[0]?.name || "Choose file";
|
||||||
window.history.back();
|
const nextSibling = this.nextElementSibling;
|
||||||
});
|
if (nextSibling) {
|
||||||
|
nextSibling.innerText = fileName;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// File input customization
|
// Search form submit on enter
|
||||||
const fileInputs = document.querySelectorAll(".custom-file-input");
|
const searchInputs = document.querySelectorAll(".search-input");
|
||||||
fileInputs.forEach(function (input) {
|
searchInputs.forEach(function (input) {
|
||||||
input.addEventListener("change", function (e) {
|
input.addEventListener("keypress", function (e) {
|
||||||
const fileName = this.files[0]?.name || "Choose file";
|
if (e.key === "Enter") {
|
||||||
const nextSibling = this.nextElementSibling;
|
e.preventDefault();
|
||||||
if (nextSibling) {
|
this.closest("form").submit();
|
||||||
nextSibling.innerText = fileName;
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Search form submit on enter
|
// Toggle password visibility
|
||||||
const searchInputs = document.querySelectorAll(".search-input");
|
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
||||||
searchInputs.forEach(function (input) {
|
togglePasswordButtons.forEach(function (button) {
|
||||||
input.addEventListener("keypress", function (e) {
|
button.addEventListener("click", function () {
|
||||||
if (e.key === "Enter") {
|
const target = document.querySelector(this.dataset.target);
|
||||||
e.preventDefault();
|
if (target) {
|
||||||
this.closest("form").submit();
|
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
||||||
}
|
target.setAttribute("type", type);
|
||||||
});
|
this.querySelector("i").classList.toggle("fa-eye");
|
||||||
|
this.querySelector("i").classList.toggle("fa-eye-slash");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Toggle password visibility
|
// Dropdown menu positioning
|
||||||
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
||||||
togglePasswordButtons.forEach(function (button) {
|
dropdowns.forEach(function (dropdown) {
|
||||||
button.addEventListener("click", function () {
|
dropdown.addEventListener("click", function (e) {
|
||||||
const target = document.querySelector(this.dataset.target);
|
e.stopPropagation();
|
||||||
if (target) {
|
|
||||||
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
|
||||||
target.setAttribute("type", type);
|
|
||||||
this.querySelector("i").classList.toggle("fa-eye");
|
|
||||||
this.querySelector("i").classList.toggle("fa-eye-slash");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Dropdown menu positioning
|
// Responsive table handling
|
||||||
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
const tables = document.querySelectorAll(".table-responsive");
|
||||||
dropdowns.forEach(function (dropdown) {
|
if (window.innerWidth < 768) {
|
||||||
dropdown.addEventListener("click", function (e) {
|
tables.forEach(function (table) {
|
||||||
e.stopPropagation();
|
table.classList.add("table-responsive-force");
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Responsive table handling
|
// Handle special links (printable views, exports)
|
||||||
const tables = document.querySelectorAll(".table-responsive");
|
const printLinks = document.querySelectorAll(".print-link");
|
||||||
|
printLinks.forEach(function (link) {
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.print();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportLinks = document.querySelectorAll("[data-export]");
|
||||||
|
exportLinks.forEach(function (link) {
|
||||||
|
link.addEventListener("click", function (e) {
|
||||||
|
// Handle export functionality if needed
|
||||||
|
console.log("Export requested:", this.dataset.export);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle sidebar collapse on small screens
|
||||||
|
function handleSidebarOnResize() {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
tables.forEach(function (table) {
|
document.querySelector(".sidebar")?.classList.remove("show");
|
||||||
table.classList.add("table-responsive-force");
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleSidebarOnResize); // Theme toggling functionality
|
||||||
|
function setTheme(theme, isUserPreference = false) {
|
||||||
|
console.log("Setting theme to:", theme, "User preference:", isUserPreference);
|
||||||
|
|
||||||
|
// Update the HTML attribute that controls theme
|
||||||
|
document.documentElement.setAttribute("data-bs-theme", theme);
|
||||||
|
|
||||||
|
// Save the theme preference to localStorage
|
||||||
|
localStorage.setItem("theme", theme);
|
||||||
|
|
||||||
|
// If this was a user choice (from the toggle button), record that fact
|
||||||
|
if (isUserPreference) {
|
||||||
|
localStorage.setItem("userPreferredTheme", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle special links (printable views, exports)
|
// Update toggle button icon
|
||||||
const printLinks = document.querySelectorAll(".print-link");
|
|
||||||
printLinks.forEach(function (link) {
|
|
||||||
link.addEventListener("click", function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
window.print();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const exportLinks = document.querySelectorAll("[data-export]");
|
|
||||||
exportLinks.forEach(function (link) {
|
|
||||||
link.addEventListener("click", function (e) {
|
|
||||||
// Handle export functionality if needed
|
|
||||||
console.log("Export requested:", this.dataset.export);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle sidebar collapse on small screens
|
|
||||||
function handleSidebarOnResize() {
|
|
||||||
if (window.innerWidth < 768) {
|
|
||||||
document.querySelector(".sidebar")?.classList.remove("show");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("resize", handleSidebarOnResize); // Theme toggling functionality
|
|
||||||
function setTheme(theme, isUserPreference = false) {
|
|
||||||
console.log("Setting theme to:", theme, "User preference:", isUserPreference);
|
|
||||||
|
|
||||||
// Update the HTML attribute that controls theme
|
|
||||||
document.documentElement.setAttribute("data-bs-theme", theme);
|
|
||||||
|
|
||||||
// Save the theme preference to localStorage
|
|
||||||
localStorage.setItem("theme", theme);
|
|
||||||
|
|
||||||
// If this was a user choice (from the toggle button), record that fact
|
|
||||||
if (isUserPreference) {
|
|
||||||
localStorage.setItem("userPreferredTheme", "true");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update toggle button icon
|
|
||||||
const themeToggle = document.getElementById("theme-toggle");
|
|
||||||
if (themeToggle) {
|
|
||||||
const icon = themeToggle.querySelector("i");
|
|
||||||
if (theme === "dark") {
|
|
||||||
icon.classList.remove("fa-moon");
|
|
||||||
icon.classList.add("fa-sun");
|
|
||||||
themeToggle.setAttribute("title", "Switch to light mode");
|
|
||||||
themeToggle.setAttribute("aria-label", "Switch to light mode");
|
|
||||||
} else {
|
|
||||||
icon.classList.remove("fa-sun");
|
|
||||||
icon.classList.add("fa-moon");
|
|
||||||
themeToggle.setAttribute("title", "Switch to dark mode");
|
|
||||||
themeToggle.setAttribute("aria-label", "Switch to dark mode");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're on a page with charts, refresh them to match the theme
|
|
||||||
if (typeof window.refreshAllCharts === "function") {
|
|
||||||
console.log("Calling refresh charts from theme toggle");
|
|
||||||
// Add a small delay to ensure DOM updates have completed
|
|
||||||
setTimeout(window.refreshAllCharts, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the user has a system preference for dark mode
|
|
||||||
function getSystemPreference() {
|
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme based on saved preference or system setting
|
|
||||||
function initializeTheme() {
|
|
||||||
// Check if the user has explicitly set a preference
|
|
||||||
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
|
|
||||||
const savedTheme = localStorage.getItem("theme");
|
|
||||||
const systemTheme = getSystemPreference();
|
|
||||||
|
|
||||||
console.log("Theme initialization:", {
|
|
||||||
hasUserPreference,
|
|
||||||
savedTheme,
|
|
||||||
systemTheme,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use saved theme if it exists and was set by user
|
|
||||||
// Otherwise, use system preference
|
|
||||||
if (hasUserPreference && savedTheme) {
|
|
||||||
setTheme(savedTheme);
|
|
||||||
} else {
|
|
||||||
// No user preference, use system preference
|
|
||||||
setTheme(systemTheme);
|
|
||||||
// Clear any saved theme to ensure it uses system preference
|
|
||||||
localStorage.removeItem("userPreferredTheme");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme on page load
|
|
||||||
initializeTheme();
|
|
||||||
|
|
||||||
// Listen for system preference changes
|
|
||||||
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
||||||
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
|
||||||
// Only update theme based on system if user hasn't set a preference
|
|
||||||
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
|
|
||||||
console.log("System preference changed. Following system?", !hasUserPreference);
|
|
||||||
|
|
||||||
if (!hasUserPreference) {
|
|
||||||
setTheme(e.matches ? "dark" : "light");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Theme toggle button functionality
|
|
||||||
const themeToggle = document.getElementById("theme-toggle");
|
const themeToggle = document.getElementById("theme-toggle");
|
||||||
if (themeToggle) {
|
if (themeToggle) {
|
||||||
themeToggle.addEventListener("click", function () {
|
const icon = themeToggle.querySelector("i");
|
||||||
const currentTheme = document.documentElement.getAttribute("data-bs-theme") || "light";
|
if (theme === "dark") {
|
||||||
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
icon.classList.remove("fa-moon");
|
||||||
console.log("Manual theme toggle from", currentTheme, "to", newTheme);
|
icon.classList.add("fa-sun");
|
||||||
setTheme(newTheme, true); // true indicates this is a user preference
|
themeToggle.setAttribute("title", "Switch to light mode");
|
||||||
});
|
themeToggle.setAttribute("aria-label", "Switch to light mode");
|
||||||
|
} else {
|
||||||
|
icon.classList.remove("fa-sun");
|
||||||
|
icon.classList.add("fa-moon");
|
||||||
|
themeToggle.setAttribute("title", "Switch to dark mode");
|
||||||
|
themeToggle.setAttribute("aria-label", "Switch to dark mode");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're on a page with charts, refresh them to match the theme
|
||||||
|
if (typeof window.refreshAllCharts === "function") {
|
||||||
|
console.log("Calling refresh charts from theme toggle");
|
||||||
|
// Add a small delay to ensure DOM updates have completed
|
||||||
|
setTimeout(window.refreshAllCharts, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has a system preference for dark mode
|
||||||
|
function getSystemPreference() {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme based on saved preference or system setting
|
||||||
|
function initializeTheme() {
|
||||||
|
// Check if the user has explicitly set a preference
|
||||||
|
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
|
||||||
|
const savedTheme = localStorage.getItem("theme");
|
||||||
|
const systemTheme = getSystemPreference();
|
||||||
|
|
||||||
|
console.log("Theme initialization:", {
|
||||||
|
hasUserPreference,
|
||||||
|
savedTheme,
|
||||||
|
systemTheme,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use saved theme if it exists and was set by user
|
||||||
|
// Otherwise, use system preference
|
||||||
|
if (hasUserPreference && savedTheme) {
|
||||||
|
setTheme(savedTheme);
|
||||||
|
} else {
|
||||||
|
// No user preference, use system preference
|
||||||
|
setTheme(systemTheme);
|
||||||
|
// Clear any saved theme to ensure it uses system preference
|
||||||
|
localStorage.removeItem("userPreferredTheme");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme on page load
|
||||||
|
initializeTheme();
|
||||||
|
|
||||||
|
// Listen for system preference changes
|
||||||
|
const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
colorSchemeMediaQuery.addEventListener("change", (e) => {
|
||||||
|
// Only update theme based on system if user hasn't set a preference
|
||||||
|
const hasUserPreference = localStorage.getItem("userPreferredTheme") === "true";
|
||||||
|
console.log("System preference changed. Following system?", !hasUserPreference);
|
||||||
|
|
||||||
|
if (!hasUserPreference) {
|
||||||
|
setTheme(e.matches ? "dark" : "light");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme toggle button functionality
|
||||||
|
const themeToggle = document.getElementById("theme-toggle");
|
||||||
|
if (themeToggle) {
|
||||||
|
themeToggle.addEventListener("click", function () {
|
||||||
|
const currentTheme = document.documentElement.getAttribute("data-bs-theme") || "light";
|
||||||
|
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
||||||
|
console.log("Manual theme toggle from", currentTheme, "to", newTheme);
|
||||||
|
setTheme(newTheme, true); // true indicates this is a user preference
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
46
package.json
46
package.json
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
||||||
"lint:md:fix": "bun lint:md -- --fix"
|
"lint:md:fix": "bun lint:md -- --fix"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"markdownlint-cli2": "^0.18.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"prettier-plugin-jinja-template": "^2.1.0",
|
||||||
|
"prettier-plugin-packagejson": "^2.5.19"
|
||||||
|
},
|
||||||
|
"markdownlint-cli2": {
|
||||||
|
"config": {
|
||||||
|
"MD013": false,
|
||||||
|
"MD033": false
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"ignores": [
|
||||||
"markdownlint-cli2": "^0.18.1",
|
".git",
|
||||||
"prettier": "^3.6.2",
|
".trunk",
|
||||||
"prettier-plugin-jinja-template": "^2.1.0",
|
".venv",
|
||||||
"prettier-plugin-packagejson": "^2.5.19"
|
"node_modules"
|
||||||
},
|
]
|
||||||
"markdownlint-cli2": {
|
}
|
||||||
"config": {
|
|
||||||
"MD013": false,
|
|
||||||
"MD033": false
|
|
||||||
},
|
|
||||||
"ignores": [
|
|
||||||
".git",
|
|
||||||
".trunk",
|
|
||||||
".venv",
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ dependencies = [
|
|||||||
"Documentation" = "https://github.com/kjanat/livegraphsdjango#readme"
|
"Documentation" = "https://github.com/kjanat/livegraphsdjango#readme"
|
||||||
"Source" = "https://github.com/kjanat/livegraphsdjango"
|
"Source" = "https://github.com/kjanat/livegraphsdjango"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
# Django management commands
|
||||||
|
livegraphs-manage = "dashboard_project.manage:main"
|
||||||
|
livegraphs-migrate = "dashboard_project.__main__:migrate"
|
||||||
|
livegraphs-server = "dashboard_project.__main__:runserver"
|
||||||
|
livegraphs-shell = "dashboard_project.__main__:shell"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"bandit>=1.8.3",
|
"bandit>=1.8.3",
|
||||||
@@ -52,6 +59,7 @@ dev = [
|
|||||||
"pytest>=8.3.5",
|
"pytest>=8.3.5",
|
||||||
"pytest-django>=4.11.1",
|
"pytest-django>=4.11.1",
|
||||||
"ruff>=0.11.10",
|
"ruff>=0.11.10",
|
||||||
|
"ty>=0.0.1a25",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
@@ -165,4 +173,9 @@ line-ending = "lf"
|
|||||||
packages = ["dashboard_project"]
|
packages = ["dashboard_project"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"dashboard_project" = ["static/__/*", "templates/__/*", "media/**/*"]
|
"dashboard_project" = [
|
||||||
|
"static/**/*",
|
||||||
|
"templates/**/*",
|
||||||
|
"media/**/*",
|
||||||
|
"py.typed"
|
||||||
|
]
|
||||||
|
|||||||
@@ -547,6 +547,25 @@ tinycss2==1.4.0 \
|
|||||||
# via
|
# via
|
||||||
# bleach
|
# bleach
|
||||||
# livegraphsdjango
|
# livegraphsdjango
|
||||||
|
ty==0.0.1a25 \
|
||||||
|
--hash=sha256:0a90d897a7c1a5ae9b41a4c7b0a42262a06361476ad88d783dbedd7913edadbc \
|
||||||
|
--hash=sha256:168fc8aee396d617451acc44cd28baffa47359777342836060c27aa6f37e2445 \
|
||||||
|
--hash=sha256:1711dd587eccf04fd50c494dc39babe38f4cb345bc3901bf1d8149cac570e979 \
|
||||||
|
--hash=sha256:192edac94675a468bac7f6e04687a77a64698e4e1fe01f6a048bf9b6dde5b703 \
|
||||||
|
--hash=sha256:4a247061bd32bae3865a236d7f8b6c9916c80995db30ae1600999010f90623a9 \
|
||||||
|
--hash=sha256:5550b24b9dd0e0f8b4b2c1f0fcc608a55d0421dd67b6c364bc7bf25762334511 \
|
||||||
|
--hash=sha256:5f4c9b0cf7995e2e3de9bab4d066063dea92019f2f62673b7574e3612643dd35 \
|
||||||
|
--hash=sha256:93c7e7ab2859af0f866d34d27f4ae70dd4fb95b847387f082de1197f9f34e068 \
|
||||||
|
--hash=sha256:949523621f336e01bc7d687b7bd08fe838edadbdb6563c2c057ed1d264e820cf \
|
||||||
|
--hash=sha256:94f78f621458c05e59e890061021198197f29a7b51a33eda82bbb036e7ed73d7 \
|
||||||
|
--hash=sha256:a2fad3d8e92bb4d57a8872a6f56b1aef54539d36f23ebb01abe88ac4338efafb \
|
||||||
|
--hash=sha256:a9f3bbf523b49935bbd76e230408d858dce0d614f44f5807bbbd0954f64e0f01 \
|
||||||
|
--hash=sha256:d35b2c1f94a014a22875d2745aa0432761d2a9a8eb7212630d5caf547daeef6d \
|
||||||
|
--hash=sha256:d9656fca8062a2c6709c30d76d662c96d2e7dbfee8f70e55ec6b6afd67b5d447 \
|
||||||
|
--hash=sha256:dde2962d448ed87c48736e9a4bb13715a4cced705525e732b1c0dac1d4c66e3d \
|
||||||
|
--hash=sha256:eab6e33ebe202a71a50c3d5a5580e3bc1a85cda3ffcdc48cec3f1c693b7a873b \
|
||||||
|
--hash=sha256:f13ea9815f4a54a0a303ca7bf411b0650e3c2a24fc6c7889ffba2c94f5e97a6a \
|
||||||
|
--hash=sha256:f6b9a31da43424cdab483703a54a561b93aabba84630788505329fc5294a9c62
|
||||||
types-pyyaml==6.0.12.20250915 \
|
types-pyyaml==6.0.12.20250915 \
|
||||||
--hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \
|
--hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \
|
||||||
--hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6
|
--hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6
|
||||||
|
|||||||
26
ty.toml
Normal file
26
ty.toml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# ty Type Checker Configuration
|
||||||
|
|
||||||
|
[environment]
|
||||||
|
# Django project root for first-party module resolution
|
||||||
|
root = ["dashboard_project"]
|
||||||
|
# Python version (matches pyproject.toml requires-python)
|
||||||
|
python-version = "3.13"
|
||||||
|
|
||||||
|
[src]
|
||||||
|
# Include only the Django project directory
|
||||||
|
include = ["dashboard_project"]
|
||||||
|
# Exclude migrations, cache, and generated files
|
||||||
|
exclude = [
|
||||||
|
"dashboard_project/migrations",
|
||||||
|
"dashboard_project/*/migrations",
|
||||||
|
"dashboard_project/**/__pycache__",
|
||||||
|
"dashboard_project/**/*.pyc"
|
||||||
|
]
|
||||||
|
# Respect .gitignore files
|
||||||
|
respect-ignore-files = true
|
||||||
|
|
||||||
|
[terminal]
|
||||||
|
# Use concise output for cleaner CI/CD logs
|
||||||
|
output-format = "concise"
|
||||||
|
# Treat warnings as errors in CI
|
||||||
|
error-on-warning = false
|
||||||
27
uv.lock
generated
27
uv.lock
generated
@@ -563,6 +563,7 @@ dev = [
|
|||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-django" },
|
{ name = "pytest-django" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
|
{ name = "ty" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -599,6 +600,7 @@ dev = [
|
|||||||
{ name = "pytest", specifier = ">=8.3.5" },
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
{ name = "pytest-django", specifier = ">=4.11.1" },
|
{ name = "pytest-django", specifier = ">=4.11.1" },
|
||||||
{ name = "ruff", specifier = ">=0.11.10" },
|
{ name = "ruff", specifier = ">=0.11.10" },
|
||||||
|
{ name = "ty", specifier = ">=0.0.1a25" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1088,6 +1090,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
|
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ty"
|
||||||
|
version = "0.0.1a25"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/6b/e73bc3c1039ea72936158a08313155a49e5aa5e7db5205a149fe516a4660/ty-0.0.1a25.tar.gz", hash = "sha256:5550b24b9dd0e0f8b4b2c1f0fcc608a55d0421dd67b6c364bc7bf25762334511", size = 4403670, upload-time = "2025-10-29T19:40:23.647Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/3b/4457231238a2eeb04cba4ba7cc33d735be68ee46ca40a98ae30e187de864/ty-0.0.1a25-py3-none-linux_armv6l.whl", hash = "sha256:d35b2c1f94a014a22875d2745aa0432761d2a9a8eb7212630d5caf547daeef6d", size = 8878803, upload-time = "2025-10-29T19:39:42.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/fa/a328713dd310018fc7a381693d8588185baa2fdae913e01a6839187215df/ty-0.0.1a25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:192edac94675a468bac7f6e04687a77a64698e4e1fe01f6a048bf9b6dde5b703", size = 8695667, upload-time = "2025-10-29T19:39:45.179Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/e8/5707939118992ced2bf5385adc3ede7723c1b717b07ad14c495eea1e47b4/ty-0.0.1a25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:949523621f336e01bc7d687b7bd08fe838edadbdb6563c2c057ed1d264e820cf", size = 8159012, upload-time = "2025-10-29T19:39:47.011Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/fb/ff313aa71602225cd78f1bce3017713d6d1b1c1e0fa8101ead4594a60d95/ty-0.0.1a25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f78f621458c05e59e890061021198197f29a7b51a33eda82bbb036e7ed73d7", size = 8433675, upload-time = "2025-10-29T19:39:48.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/8d/cc7e7fb57215a15b575a43ed042bdd92971871e0decec1b26d2e7d969465/ty-0.0.1a25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9656fca8062a2c6709c30d76d662c96d2e7dbfee8f70e55ec6b6afd67b5d447", size = 8668456, upload-time = "2025-10-29T19:39:50.412Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/6d/d7bf5909ed2dcdcbc1e2ca7eea80929893e2d188d9c36b3fcb2b36532ff6/ty-0.0.1a25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9f3bbf523b49935bbd76e230408d858dce0d614f44f5807bbbd0954f64e0f01", size = 9023543, upload-time = "2025-10-29T19:39:52.292Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/b8/72bcefb4be32e5a84f0b21de2552f16cdb4cae3eb271ac891c8199c26b1a/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f13ea9815f4a54a0a303ca7bf411b0650e3c2a24fc6c7889ffba2c94f5e97a6a", size = 9700013, upload-time = "2025-10-29T19:39:57.283Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/0d/cf7e794b840cf6b0bbecb022e593c543f85abad27a582241cf2095048cb1/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eab6e33ebe202a71a50c3d5a5580e3bc1a85cda3ffcdc48cec3f1c693b7a873b", size = 9372574, upload-time = "2025-10-29T19:40:04.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/71/2d35e7d51b48eabd330e2f7b7e0bce541cbd95950c4d2f780e85f3366af1/ty-0.0.1a25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6b9a31da43424cdab483703a54a561b93aabba84630788505329fc5294a9c62", size = 9535726, upload-time = "2025-10-29T19:40:06.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/d3/01ecc23bbd8f3e0dfbcf9172d06d84e88155c5f416f1491137e8066fd859/ty-0.0.1a25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a90d897a7c1a5ae9b41a4c7b0a42262a06361476ad88d783dbedd7913edadbc", size = 9003380, upload-time = "2025-10-29T19:40:08.683Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/f9/cde9380d8a1a6ca61baeb9aecb12cbec90d489aa929be55cd78ad5c2ccd9/ty-0.0.1a25-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:93c7e7ab2859af0f866d34d27f4ae70dd4fb95b847387f082de1197f9f34e068", size = 8401833, upload-time = "2025-10-29T19:40:10.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/39/0acf3625b0c495011795a391016b572f97a812aca1d67f7a76621fdb9ebf/ty-0.0.1a25-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a247061bd32bae3865a236d7f8b6c9916c80995db30ae1600999010f90623a9", size = 8706761, upload-time = "2025-10-29T19:40:12.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/73/7de1648f3563dd9d416d36ab5f1649bfd7b47a179135027f31d44b89a246/ty-0.0.1a25-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1711dd587eccf04fd50c494dc39babe38f4cb345bc3901bf1d8149cac570e979", size = 8792426, upload-time = "2025-10-29T19:40:14.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/8a/b6e761a65eac7acd10b2e452f49b2d8ae0ea163ca36bb6b18b2dadae251b/ty-0.0.1a25-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f4c9b0cf7995e2e3de9bab4d066063dea92019f2f62673b7574e3612643dd35", size = 9103991, upload-time = "2025-10-29T19:40:16.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/25/9324ae947fcc4322470326cf8276a3fc2f08dc82adec1de79d963fdf7af5/ty-0.0.1a25-py3-none-win32.whl", hash = "sha256:168fc8aee396d617451acc44cd28baffa47359777342836060c27aa6f37e2445", size = 8387095, upload-time = "2025-10-29T19:40:18.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/2b/cb12cbc7db1ba310aa7b1de9b4e018576f653105993736c086ee67d2ec02/ty-0.0.1a25-py3-none-win_amd64.whl", hash = "sha256:a2fad3d8e92bb4d57a8872a6f56b1aef54539d36f23ebb01abe88ac4338efafb", size = 9059225, upload-time = "2025-10-29T19:40:20.278Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/c1/f6be8cdd0bf387c1d8ee9d14bb299b7b5d2c0532f550a6693216a32ec0c5/ty-0.0.1a25-py3-none-win_arm64.whl", hash = "sha256:dde2962d448ed87c48736e9a4bb13715a4cced705525e732b1c0dac1d4c66e3d", size = 8536832, upload-time = "2025-10-29T19:40:22.014Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-pyyaml"
|
name = "types-pyyaml"
|
||||||
version = "6.0.12.20250915"
|
version = "6.0.12.20250915"
|
||||||
|
|||||||
Reference in New Issue
Block a user