mirror of
https://github.com/kjanat/livegraphs-django.git
synced 2026-01-16 10:52:08 +01:00
Add configuration and scripts for linting, testing, and dependency management
- Introduced .pre-commit-config.yaml for pre-commit hooks using uv-pre-commit. - Created lint.sh script to run Ruff and Black for linting and formatting. - Added test.sh script to execute tests with coverage reporting. - Configured .uv file for uv settings including lockfile management and dependency resolution. - Updated Makefile with targets for virtual environment setup, dependency installation, linting, testing, formatting, and database migrations. - Established requirements.txt with main and development dependencies for the project.
This commit is contained in:
@ -6,268 +6,269 @@
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Only initialize if AJAX navigation is enabled
|
||||
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
|
||||
setupAjaxNavigation();
|
||||
}
|
||||
// Only initialize if AJAX navigation is enabled
|
||||
if (typeof ENABLE_AJAX_NAVIGATION !== "undefined" && ENABLE_AJAX_NAVIGATION) {
|
||||
setupAjaxNavigation();
|
||||
}
|
||||
|
||||
// Function to set up AJAX navigation for the application
|
||||
function setupAjaxNavigation() {
|
||||
// Configuration
|
||||
const config = {
|
||||
mainContentSelector: "#main-content", // Selector for the main content area
|
||||
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
|
||||
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
|
||||
excludePatterns: [
|
||||
// URL patterns to exclude from AJAX navigation
|
||||
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
|
||||
/\/admin\//, // Admin pages
|
||||
/\/accounts\/logout\//, // Logout page
|
||||
/\/api\//, // API endpoints
|
||||
],
|
||||
};
|
||||
// Function to set up AJAX navigation for the application
|
||||
function setupAjaxNavigation() {
|
||||
// Configuration
|
||||
const config = {
|
||||
mainContentSelector: "#main-content", // Selector for the main content area
|
||||
navLinkSelector: ".ajax-nav-link", // Selector for links to handle with AJAX
|
||||
loadingIndicatorId: "nav-loading-indicator", // ID of the loading indicator
|
||||
excludePatterns: [
|
||||
// URL patterns to exclude from AJAX navigation
|
||||
/\.(pdf|xlsx?|docx?|csv|zip|png|jpe?g|gif|svg)$/i, // File downloads
|
||||
/\/admin\//, // Admin pages
|
||||
/\/accounts\/logout\//, // Logout page
|
||||
/\/api\//, // API endpoints
|
||||
],
|
||||
};
|
||||
|
||||
// Create and insert the loading indicator
|
||||
if (!document.getElementById(config.loadingIndicatorId)) {
|
||||
const loadingIndicator = document.createElement("div");
|
||||
loadingIndicator.id = config.loadingIndicatorId;
|
||||
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
|
||||
loadingIndicator.innerHTML =
|
||||
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
|
||||
loadingIndicator.style.display = "none";
|
||||
loadingIndicator.style.zIndex = "9999";
|
||||
document.body.appendChild(loadingIndicator);
|
||||
}
|
||||
// Create and insert the loading indicator
|
||||
if (!document.getElementById(config.loadingIndicatorId)) {
|
||||
const loadingIndicator = document.createElement("div");
|
||||
loadingIndicator.id = config.loadingIndicatorId;
|
||||
loadingIndicator.className = "position-fixed top-0 start-0 end-0";
|
||||
loadingIndicator.innerHTML =
|
||||
'<div class="progress" style="height: 3px; border-radius: 0;"><div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" style="width: 100%"></div></div>';
|
||||
loadingIndicator.style.display = "none";
|
||||
loadingIndicator.style.zIndex = "9999";
|
||||
document.body.appendChild(loadingIndicator);
|
||||
}
|
||||
|
||||
// Get the loading indicator element
|
||||
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
|
||||
// Get the loading indicator element
|
||||
const loadingIndicator = document.getElementById(config.loadingIndicatorId);
|
||||
|
||||
// Get the main content container
|
||||
const mainContent = document.querySelector(config.mainContentSelector);
|
||||
if (!mainContent) {
|
||||
console.warn("Main content container not found. AJAX navigation disabled.");
|
||||
return;
|
||||
}
|
||||
// Get the main content container
|
||||
const mainContent = document.querySelector(config.mainContentSelector);
|
||||
if (!mainContent) {
|
||||
console.warn("Main content container not found. AJAX navigation disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Function to check if a URL should be excluded from AJAX navigation
|
||||
function shouldExcludeUrl(url) {
|
||||
for (const pattern of config.excludePatterns) {
|
||||
if (pattern.test(url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Function to check if a URL should be excluded from AJAX navigation
|
||||
function shouldExcludeUrl(url) {
|
||||
for (const pattern of config.excludePatterns) {
|
||||
if (pattern.test(url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to show the loading indicator
|
||||
function showLoading() {
|
||||
loadingIndicator.style.display = "block";
|
||||
}
|
||||
// Function to show the loading indicator
|
||||
function showLoading() {
|
||||
loadingIndicator.style.display = "block";
|
||||
}
|
||||
|
||||
// Function to hide the loading indicator
|
||||
function hideLoading() {
|
||||
loadingIndicator.style.display = "none";
|
||||
}
|
||||
// Function to hide the loading indicator
|
||||
function hideLoading() {
|
||||
loadingIndicator.style.display = "none";
|
||||
}
|
||||
|
||||
// Function to handle AJAX page navigation
|
||||
function handlePageNavigation(url, pushState = true) {
|
||||
if (shouldExcludeUrl(url)) {
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
showLoading();
|
||||
const currentScrollPos = window.scrollY;
|
||||
fetch(url, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-AJAX-Navigation": "true",
|
||||
Accept: "text/html",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
|
||||
return response.text();
|
||||
})
|
||||
.then((html) => {
|
||||
// Parse the HTML and extract #main-content
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = html;
|
||||
const newContent = tempDiv.querySelector(config.mainContentSelector);
|
||||
if (!newContent) throw new Error("Could not find main content in the response");
|
||||
mainContent.innerHTML = newContent.innerHTML;
|
||||
// Update the page title
|
||||
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
||||
if (titleMatch) document.title = titleMatch[1];
|
||||
// Re-initialize dynamic content
|
||||
reloadScripts(mainContent);
|
||||
attachEventListeners();
|
||||
initializePageScripts();
|
||||
if (pushState) {
|
||||
history.pushState(
|
||||
{ url: url, title: document.title, scrollPos: currentScrollPos },
|
||||
document.title,
|
||||
url
|
||||
);
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
} else if (window.history.state && window.history.state.scrollPos) {
|
||||
window.scrollTo({ top: window.history.state.scrollPos });
|
||||
}
|
||||
hideLoading();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error during AJAX navigation:", error);
|
||||
hideLoading();
|
||||
window.location.href = url;
|
||||
});
|
||||
}
|
||||
// Function to handle AJAX page navigation
|
||||
function handlePageNavigation(url, pushState = true) {
|
||||
if (shouldExcludeUrl(url)) {
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
showLoading();
|
||||
const currentScrollPos = window.scrollY;
|
||||
fetch(url, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-AJAX-Navigation": "true",
|
||||
Accept: "text/html",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok)
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
return response.text();
|
||||
})
|
||||
.then((html) => {
|
||||
// Parse the HTML and extract #main-content
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = html;
|
||||
const newContent = tempDiv.querySelector(config.mainContentSelector);
|
||||
if (!newContent) throw new Error("Could not find main content in the response");
|
||||
mainContent.innerHTML = newContent.innerHTML;
|
||||
// Update the page title
|
||||
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
||||
if (titleMatch) document.title = titleMatch[1];
|
||||
// Re-initialize dynamic content
|
||||
reloadScripts(mainContent);
|
||||
attachEventListeners();
|
||||
initializePageScripts();
|
||||
if (pushState) {
|
||||
history.pushState(
|
||||
{ url: url, title: document.title, scrollPos: currentScrollPos },
|
||||
document.title,
|
||||
url,
|
||||
);
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
} else if (window.history.state && window.history.state.scrollPos) {
|
||||
window.scrollTo({ top: window.history.state.scrollPos });
|
||||
}
|
||||
hideLoading();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error during AJAX navigation:", error);
|
||||
hideLoading();
|
||||
window.location.href = url;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to reload and execute scripts in new content
|
||||
function reloadScripts(container) {
|
||||
const scripts = container.getElementsByTagName("script");
|
||||
for (let script of scripts) {
|
||||
const newScript = document.createElement("script");
|
||||
// Function to reload and execute scripts in new content
|
||||
function reloadScripts(container) {
|
||||
const scripts = container.getElementsByTagName("script");
|
||||
for (let script of scripts) {
|
||||
const newScript = document.createElement("script");
|
||||
|
||||
// Copy all attributes
|
||||
Array.from(script.attributes).forEach((attr) => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
// Copy all attributes
|
||||
Array.from(script.attributes).forEach((attr) => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
|
||||
// Copy inline script content
|
||||
newScript.textContent = script.textContent;
|
||||
// Copy inline script content
|
||||
newScript.textContent = script.textContent;
|
||||
|
||||
// Replace old script with new one
|
||||
script.parentNode.replaceChild(newScript, script);
|
||||
}
|
||||
}
|
||||
// Replace old script with new one
|
||||
script.parentNode.replaceChild(newScript, script);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle form submissions
|
||||
function handleFormSubmission(form, e) {
|
||||
e.preventDefault();
|
||||
// Function to handle form submissions
|
||||
function handleFormSubmission(form, e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show loading indicator
|
||||
showLoading();
|
||||
// Show loading indicator
|
||||
showLoading();
|
||||
|
||||
// Get form data
|
||||
const formData = new FormData(form);
|
||||
const method = form.method.toLowerCase();
|
||||
const url = form.action || window.location.href;
|
||||
// Get form data
|
||||
const formData = new FormData(form);
|
||||
const method = form.method.toLowerCase();
|
||||
const url = form.action || window.location.href;
|
||||
|
||||
// Configure fetch options
|
||||
const fetchOptions = {
|
||||
method: method,
|
||||
headers: {
|
||||
"X-AJAX-Navigation": "true",
|
||||
},
|
||||
};
|
||||
// Configure fetch options
|
||||
const fetchOptions = {
|
||||
method: method,
|
||||
headers: {
|
||||
"X-AJAX-Navigation": "true",
|
||||
},
|
||||
};
|
||||
|
||||
// Handle different HTTP methods
|
||||
if (method === "get") {
|
||||
const queryParams = new URLSearchParams(formData).toString();
|
||||
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
|
||||
} else {
|
||||
fetchOptions.body = formData;
|
||||
// Handle different HTTP methods
|
||||
if (method === "get") {
|
||||
const queryParams = new URLSearchParams(formData).toString();
|
||||
handlePageNavigation(url + (queryParams ? "?" + queryParams : ""));
|
||||
} else {
|
||||
fetchOptions.body = formData;
|
||||
|
||||
fetch(url, fetchOptions)
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.redirect) {
|
||||
// Handle server-side redirects
|
||||
handlePageNavigation(data.redirect, true);
|
||||
} else {
|
||||
// Update page content
|
||||
mainContent.innerHTML = data.html;
|
||||
document.title = data.title || document.title;
|
||||
fetch(url, fetchOptions)
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.redirect) {
|
||||
// Handle server-side redirects
|
||||
handlePageNavigation(data.redirect, true);
|
||||
} else {
|
||||
// Update page content
|
||||
mainContent.innerHTML = data.html;
|
||||
document.title = data.title || document.title;
|
||||
|
||||
// Re-initialize dynamic content
|
||||
reloadScripts(mainContent);
|
||||
attachEventListeners();
|
||||
initializePageScripts();
|
||||
// Re-initialize dynamic content
|
||||
reloadScripts(mainContent);
|
||||
attachEventListeners();
|
||||
initializePageScripts();
|
||||
|
||||
// Update URL if needed
|
||||
if (data.url) {
|
||||
history.pushState({ url: data.url }, document.title, data.url);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Form submission error:", error);
|
||||
// Fallback to traditional form submission
|
||||
form.submit();
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
}
|
||||
// Update URL if needed
|
||||
if (data.url) {
|
||||
history.pushState({ url: data.url }, document.title, data.url);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Form submission error:", error);
|
||||
// Fallback to traditional form submission
|
||||
form.submit();
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoading();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to initialize scripts needed for the new page content
|
||||
function initializePageScripts() {
|
||||
// Re-initialize any custom scripts that might be needed
|
||||
if (typeof setupAjaxPagination === "function") {
|
||||
setupAjaxPagination();
|
||||
}
|
||||
// Function to initialize scripts needed for the new page content
|
||||
function initializePageScripts() {
|
||||
// Re-initialize any custom scripts that might be needed
|
||||
if (typeof setupAjaxPagination === "function") {
|
||||
setupAjaxPagination();
|
||||
}
|
||||
|
||||
// Initialize Bootstrap tooltips, popovers, etc.
|
||||
if (typeof bootstrap !== "undefined") {
|
||||
// Initialize tooltips
|
||||
const tooltipTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
);
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
// Initialize Bootstrap tooltips, popovers, etc.
|
||||
if (typeof bootstrap !== "undefined") {
|
||||
// Initialize tooltips
|
||||
const tooltipTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
|
||||
);
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize popovers
|
||||
const popoverTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="popover"]')
|
||||
);
|
||||
popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Initialize popovers
|
||||
const popoverTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="popover"]'),
|
||||
);
|
||||
popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to attach event listeners to forms and links
|
||||
function attachEventListeners() {
|
||||
// Handle AJAX navigation links
|
||||
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
|
||||
if (!link.dataset.ajaxNavInitialized) {
|
||||
link.addEventListener("click", function (e) {
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
|
||||
return; // Let the browser handle these cases
|
||||
}
|
||||
e.preventDefault();
|
||||
handlePageNavigation(this.href);
|
||||
});
|
||||
link.dataset.ajaxNavInitialized = "true";
|
||||
}
|
||||
});
|
||||
// Function to attach event listeners to forms and links
|
||||
function attachEventListeners() {
|
||||
// Handle AJAX navigation links
|
||||
document.querySelectorAll(config.navLinkSelector).forEach((link) => {
|
||||
if (!link.dataset.ajaxNavInitialized) {
|
||||
link.addEventListener("click", function (e) {
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey || shouldExcludeUrl(this.href)) {
|
||||
return; // Let the browser handle these cases
|
||||
}
|
||||
e.preventDefault();
|
||||
handlePageNavigation(this.href);
|
||||
});
|
||||
link.dataset.ajaxNavInitialized = "true";
|
||||
}
|
||||
});
|
||||
|
||||
// Handle forms with AJAX
|
||||
document
|
||||
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
|
||||
.forEach((form) => {
|
||||
if (!form.dataset.ajaxFormInitialized) {
|
||||
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
|
||||
form.dataset.ajaxFormInitialized = "true";
|
||||
}
|
||||
});
|
||||
}
|
||||
// Handle forms with AJAX
|
||||
document
|
||||
.querySelectorAll("form.ajax-form, form.search-form, form.filter-form")
|
||||
.forEach((form) => {
|
||||
if (!form.dataset.ajaxFormInitialized) {
|
||||
form.addEventListener("submit", (e) => handleFormSubmission(form, e));
|
||||
form.dataset.ajaxFormInitialized = "true";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial attachment of event listeners
|
||||
attachEventListeners();
|
||||
// Initial attachment of event listeners
|
||||
attachEventListeners();
|
||||
|
||||
// Handle browser back/forward buttons
|
||||
window.addEventListener("popstate", function (event) {
|
||||
if (event.state && event.state.url) {
|
||||
handlePageNavigation(event.state.url, false);
|
||||
} else {
|
||||
// Fallback to current URL if no state
|
||||
handlePageNavigation(window.location.href, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Handle browser back/forward buttons
|
||||
window.addEventListener("popstate", function (event) {
|
||||
if (event.state && event.state.url) {
|
||||
handlePageNavigation(event.state.url, false);
|
||||
} else {
|
||||
// Fallback to current URL if no state
|
||||
handlePageNavigation(window.location.href, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,101 +6,101 @@
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize AJAX pagination
|
||||
setupAjaxPagination();
|
||||
// Initialize AJAX pagination
|
||||
setupAjaxPagination();
|
||||
|
||||
// Function to set up AJAX pagination for the entire application
|
||||
function setupAjaxPagination() {
|
||||
// Configuration - can be customized per page if needed
|
||||
const config = {
|
||||
contentContainerId: "ajax-content-container", // ID of the container to update
|
||||
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
|
||||
paginationLinkClass: "pagination-link", // Class for pagination links
|
||||
retryMessage: "An error occurred while loading data. Please try again.",
|
||||
};
|
||||
// Function to set up AJAX pagination for the entire application
|
||||
function setupAjaxPagination() {
|
||||
// Configuration - can be customized per page if needed
|
||||
const config = {
|
||||
contentContainerId: "ajax-content-container", // ID of the container to update
|
||||
loadingSpinnerId: "ajax-loading-spinner", // ID of the loading spinner
|
||||
paginationLinkClass: "pagination-link", // Class for pagination links
|
||||
retryMessage: "An error occurred while loading data. Please try again.",
|
||||
};
|
||||
|
||||
// Get container elements
|
||||
const contentContainer = document.getElementById(config.contentContainerId);
|
||||
const loadingSpinner = document.getElementById(config.loadingSpinnerId);
|
||||
// Get container elements
|
||||
const contentContainer = document.getElementById(config.contentContainerId);
|
||||
const loadingSpinner = document.getElementById(config.loadingSpinnerId);
|
||||
|
||||
// Exit if the page doesn't have the required elements
|
||||
if (!contentContainer || !loadingSpinner) return;
|
||||
// Exit if the page doesn't have the required elements
|
||||
if (!contentContainer || !loadingSpinner) return;
|
||||
|
||||
// Function to handle pagination clicks
|
||||
function setupPaginationListeners() {
|
||||
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
handleAjaxNavigation(this.href);
|
||||
// Function to handle pagination clicks
|
||||
function setupPaginationListeners() {
|
||||
document.querySelectorAll("." + config.paginationLinkClass).forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
handleAjaxNavigation(this.href);
|
||||
|
||||
// Get the page number if available
|
||||
const page = this.getAttribute("data-page");
|
||||
// Get the page number if available
|
||||
const page = this.getAttribute("data-page");
|
||||
|
||||
// Update browser URL without refreshing
|
||||
const newUrl = this.href;
|
||||
history.pushState({ url: newUrl, page: page }, "", newUrl);
|
||||
});
|
||||
});
|
||||
}
|
||||
// Update browser URL without refreshing
|
||||
const newUrl = this.href;
|
||||
history.pushState({ url: newUrl, page: page }, "", newUrl);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to handle AJAX navigation
|
||||
function handleAjaxNavigation(url) {
|
||||
// Show loading spinner
|
||||
contentContainer.classList.add("d-none");
|
||||
loadingSpinner.classList.remove("d-none");
|
||||
// Function to handle AJAX navigation
|
||||
function handleAjaxNavigation(url) {
|
||||
// Show loading spinner
|
||||
contentContainer.classList.add("d-none");
|
||||
loadingSpinner.classList.remove("d-none");
|
||||
|
||||
// Fetch data via AJAX
|
||||
fetch(url, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.status === "success") {
|
||||
// Update the content
|
||||
contentContainer.innerHTML = data.html_data;
|
||||
// Fetch data via AJAX
|
||||
fetch(url, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.status === "success") {
|
||||
// Update the content
|
||||
contentContainer.innerHTML = data.html_data;
|
||||
|
||||
// Re-attach event listeners to new pagination links
|
||||
setupPaginationListeners();
|
||||
// Re-attach event listeners to new pagination links
|
||||
setupPaginationListeners();
|
||||
|
||||
// Update any summary data if present and the page provides it
|
||||
if (typeof updateSummary === "function" && data.summary) {
|
||||
updateSummary(data);
|
||||
}
|
||||
// Update any summary data if present and the page provides it
|
||||
if (typeof updateSummary === "function" && data.summary) {
|
||||
updateSummary(data);
|
||||
}
|
||||
|
||||
// Hide loading spinner, show content
|
||||
loadingSpinner.classList.add("d-none");
|
||||
contentContainer.classList.remove("d-none");
|
||||
// Hide loading spinner, show content
|
||||
loadingSpinner.classList.add("d-none");
|
||||
contentContainer.classList.remove("d-none");
|
||||
|
||||
// Scroll to top of the content container
|
||||
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching data:", error);
|
||||
loadingSpinner.classList.add("d-none");
|
||||
contentContainer.classList.remove("d-none");
|
||||
alert(config.retryMessage);
|
||||
});
|
||||
}
|
||||
// Scroll to top of the content container
|
||||
contentContainer.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching data:", error);
|
||||
loadingSpinner.classList.add("d-none");
|
||||
contentContainer.classList.remove("d-none");
|
||||
alert(config.retryMessage);
|
||||
});
|
||||
}
|
||||
|
||||
// Initial setup of event listeners
|
||||
setupPaginationListeners();
|
||||
// Initial setup of event listeners
|
||||
setupPaginationListeners();
|
||||
|
||||
// Handle browser back/forward buttons
|
||||
window.addEventListener("popstate", function (event) {
|
||||
if (event.state && event.state.url) {
|
||||
handleAjaxNavigation(event.state.url);
|
||||
} else {
|
||||
// If no state, fetch current URL
|
||||
handleAjaxNavigation(window.location.href);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Handle browser back/forward buttons
|
||||
window.addEventListener("popstate", function (event) {
|
||||
if (event.state && event.state.url) {
|
||||
handleAjaxNavigation(event.state.url);
|
||||
} else {
|
||||
// If no state, fetch current URL
|
||||
handleAjaxNavigation(window.location.href);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -7,272 +7,272 @@
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Chart responsiveness
|
||||
function resizeCharts() {
|
||||
const charts = document.querySelectorAll(".chart-container");
|
||||
charts.forEach((chart) => {
|
||||
if (chart.id && window.Plotly) {
|
||||
Plotly.relayout(chart.id, {
|
||||
"xaxis.automargin": true,
|
||||
"yaxis.automargin": true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Chart responsiveness
|
||||
function resizeCharts() {
|
||||
const charts = document.querySelectorAll(".chart-container");
|
||||
charts.forEach((chart) => {
|
||||
if (chart.id && window.Plotly) {
|
||||
Plotly.relayout(chart.id, {
|
||||
"xaxis.automargin": true,
|
||||
"yaxis.automargin": true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.Plotly) {
|
||||
resizeCharts();
|
||||
}
|
||||
});
|
||||
// Handle window resize
|
||||
window.addEventListener("resize", function () {
|
||||
if (window.Plotly) {
|
||||
resizeCharts();
|
||||
}
|
||||
});
|
||||
|
||||
// Time range filtering
|
||||
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
||||
if (timeRangeDropdown) {
|
||||
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
||||
timeRangeLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
const timeRange = url.searchParams.get("time_range");
|
||||
// Time range filtering
|
||||
const timeRangeDropdown = document.getElementById("timeRangeDropdown");
|
||||
if (timeRangeDropdown) {
|
||||
const timeRangeLinks = timeRangeDropdown.querySelectorAll(".dropdown-item");
|
||||
timeRangeLinks.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
const timeRange = url.searchParams.get("time_range");
|
||||
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId, timeRange);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId, timeRange);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to fetch dashboard data
|
||||
function fetchDashboardData(dashboardId, timeRange) {
|
||||
const loadingOverlay = document.createElement("div");
|
||||
loadingOverlay.className = "loading-overlay";
|
||||
loadingOverlay.innerHTML =
|
||||
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
||||
document.querySelector("main").appendChild(loadingOverlay);
|
||||
// Function to fetch dashboard data
|
||||
function fetchDashboardData(dashboardId, timeRange) {
|
||||
const loadingOverlay = document.createElement("div");
|
||||
loadingOverlay.className = "loading-overlay";
|
||||
loadingOverlay.innerHTML =
|
||||
'<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>';
|
||||
document.querySelector("main").appendChild(loadingOverlay);
|
||||
|
||||
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("Dashboard API response:", data);
|
||||
updateDashboardStats(data);
|
||||
updateDashboardCharts(data);
|
||||
fetch(`/dashboard/api/dashboard/${dashboardId}/data/?time_range=${timeRange || "all"}`)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("Dashboard API response:", data);
|
||||
updateDashboardStats(data);
|
||||
updateDashboardCharts(data);
|
||||
|
||||
// Update URL without page reload
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("dashboard_id", dashboardId);
|
||||
if (timeRange) {
|
||||
url.searchParams.set("time_range", timeRange);
|
||||
}
|
||||
window.history.pushState({}, "", url);
|
||||
// Update URL without page reload
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("dashboard_id", dashboardId);
|
||||
if (timeRange) {
|
||||
url.searchParams.set("time_range", timeRange);
|
||||
}
|
||||
window.history.pushState({}, "", url);
|
||||
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching dashboard data:", error);
|
||||
document.querySelector(".loading-overlay").remove();
|
||||
|
||||
// Show error message
|
||||
const alertElement = document.createElement("div");
|
||||
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
||||
alertElement.setAttribute("role", "alert");
|
||||
alertElement.innerHTML = `
|
||||
// Show error message
|
||||
const alertElement = document.createElement("div");
|
||||
alertElement.className = "alert alert-danger alert-dismissible fade show";
|
||||
alertElement.setAttribute("role", "alert");
|
||||
alertElement.innerHTML = `
|
||||
Error loading dashboard data. Please try again.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
`;
|
||||
document.querySelector("main").prepend(alertElement);
|
||||
});
|
||||
}
|
||||
document.querySelector("main").prepend(alertElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to update dashboard statistics
|
||||
function updateDashboardStats(data) {
|
||||
// Update total sessions
|
||||
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
||||
if (totalSessionsElement) {
|
||||
totalSessionsElement.textContent = data.total_sessions;
|
||||
}
|
||||
// Function to update dashboard statistics
|
||||
function updateDashboardStats(data) {
|
||||
// Update total sessions
|
||||
const totalSessionsElement = document.querySelector(".stats-card:nth-child(1) h3");
|
||||
if (totalSessionsElement) {
|
||||
totalSessionsElement.textContent = data.total_sessions;
|
||||
}
|
||||
|
||||
// Update average response time
|
||||
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
||||
if (avgResponseTimeElement) {
|
||||
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
||||
}
|
||||
// Update average response time
|
||||
const avgResponseTimeElement = document.querySelector(".stats-card:nth-child(2) h3");
|
||||
if (avgResponseTimeElement) {
|
||||
avgResponseTimeElement.textContent = data.avg_response_time + "s";
|
||||
}
|
||||
|
||||
// Update total tokens
|
||||
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
||||
if (totalTokensElement) {
|
||||
totalTokensElement.textContent = data.total_tokens;
|
||||
}
|
||||
// Update total tokens
|
||||
const totalTokensElement = document.querySelector(".stats-card:nth-child(3) h3");
|
||||
if (totalTokensElement) {
|
||||
totalTokensElement.textContent = data.total_tokens;
|
||||
}
|
||||
|
||||
// Update total cost
|
||||
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
||||
if (totalCostElement) {
|
||||
totalCostElement.textContent = "€" + data.total_cost;
|
||||
}
|
||||
}
|
||||
// Update total cost
|
||||
const totalCostElement = document.querySelector(".stats-card:nth-child(4) h3");
|
||||
if (totalCostElement) {
|
||||
totalCostElement.textContent = "€" + data.total_cost;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update dashboard charts
|
||||
function updateDashboardCharts(data) {
|
||||
// Check if Plotly is available
|
||||
if (!window.Plotly) {
|
||||
console.error("Plotly library not loaded!");
|
||||
document.querySelectorAll(".chart-container").forEach((container) => {
|
||||
container.innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Function to update dashboard charts
|
||||
function updateDashboardCharts(data) {
|
||||
// Check if Plotly is available
|
||||
if (!window.Plotly) {
|
||||
console.error("Plotly library not loaded!");
|
||||
document.querySelectorAll(".chart-container").forEach((container) => {
|
||||
container.innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-danger">Chart library not available. Please refresh the page.</p></div>';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update sessions over time chart
|
||||
const timeSeriesData = data.time_series_data;
|
||||
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||
try {
|
||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||
// Update sessions over time chart
|
||||
const timeSeriesData = data.time_series_data;
|
||||
if (timeSeriesData && timeSeriesData.length > 0) {
|
||||
try {
|
||||
const timeSeriesX = timeSeriesData.map((item) => item.date);
|
||||
const timeSeriesY = timeSeriesData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"sessions-time-chart",
|
||||
[
|
||||
{
|
||||
x: timeSeriesX,
|
||||
y: timeSeriesY,
|
||||
type: "scatter",
|
||||
mode: "lines+markers",
|
||||
line: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
width: 2,
|
||||
},
|
||||
marker: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||
xaxis: {
|
||||
title: "Date",
|
||||
},
|
||||
yaxis: {
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error rendering time series chart:", error);
|
||||
document.getElementById("sessions-time-chart").innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
|
||||
}
|
||||
} else {
|
||||
document.getElementById("sessions-time-chart").innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
||||
}
|
||||
Plotly.react(
|
||||
"sessions-time-chart",
|
||||
[
|
||||
{
|
||||
x: timeSeriesX,
|
||||
y: timeSeriesY,
|
||||
type: "scatter",
|
||||
mode: "lines+markers",
|
||||
line: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
width: 2,
|
||||
},
|
||||
marker: {
|
||||
color: "rgb(75, 192, 192)",
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 40, l: 40 },
|
||||
xaxis: {
|
||||
title: "Date",
|
||||
},
|
||||
yaxis: {
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error rendering time series chart:", error);
|
||||
document.getElementById("sessions-time-chart").innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-danger">Error rendering chart.</p></div>';
|
||||
}
|
||||
} else {
|
||||
document.getElementById("sessions-time-chart").innerHTML =
|
||||
'<div class="text-center py-5"><p class="text-muted">No time series data available</p></div>';
|
||||
}
|
||||
|
||||
// Update sentiment chart
|
||||
const sentimentData = data.sentiment_data;
|
||||
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||
const sentimentValues = sentimentData.map((item) => item.count);
|
||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||
return "rgb(201, 203, 207)";
|
||||
});
|
||||
// Update sentiment chart
|
||||
const sentimentData = data.sentiment_data;
|
||||
if (sentimentData && sentimentData.length > 0 && window.Plotly) {
|
||||
const sentimentLabels = sentimentData.map((item) => item.sentiment);
|
||||
const sentimentValues = sentimentData.map((item) => item.count);
|
||||
const sentimentColors = sentimentLabels.map((sentiment) => {
|
||||
if (sentiment.toLowerCase().includes("positive")) return "rgb(75, 192, 92)";
|
||||
if (sentiment.toLowerCase().includes("negative")) return "rgb(255, 99, 132)";
|
||||
if (sentiment.toLowerCase().includes("neutral")) return "rgb(255, 205, 86)";
|
||||
return "rgb(201, 203, 207)";
|
||||
});
|
||||
|
||||
Plotly.react(
|
||||
"sentiment-chart",
|
||||
[
|
||||
{
|
||||
values: sentimentValues,
|
||||
labels: sentimentLabels,
|
||||
type: "pie",
|
||||
marker: {
|
||||
colors: sentimentColors,
|
||||
},
|
||||
hole: 0.4,
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
}
|
||||
);
|
||||
}
|
||||
Plotly.react(
|
||||
"sentiment-chart",
|
||||
[
|
||||
{
|
||||
values: sentimentValues,
|
||||
labels: sentimentLabels,
|
||||
type: "pie",
|
||||
marker: {
|
||||
colors: sentimentColors,
|
||||
},
|
||||
hole: 0.4,
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Update country chart
|
||||
const countryData = data.country_data;
|
||||
if (countryData && countryData.length > 0 && window.Plotly) {
|
||||
const countryLabels = countryData.map((item) => item.country);
|
||||
const countryValues = countryData.map((item) => item.count);
|
||||
// Update country chart
|
||||
const countryData = data.country_data;
|
||||
if (countryData && countryData.length > 0 && window.Plotly) {
|
||||
const countryLabels = countryData.map((item) => item.country);
|
||||
const countryValues = countryData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"country-chart",
|
||||
[
|
||||
{
|
||||
x: countryValues,
|
||||
y: countryLabels,
|
||||
type: "bar",
|
||||
orientation: "h",
|
||||
marker: {
|
||||
color: "rgb(54, 162, 235)",
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||
xaxis: {
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
Plotly.react(
|
||||
"country-chart",
|
||||
[
|
||||
{
|
||||
x: countryValues,
|
||||
y: countryLabels,
|
||||
type: "bar",
|
||||
orientation: "h",
|
||||
marker: {
|
||||
color: "rgb(54, 162, 235)",
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 40, l: 100 },
|
||||
xaxis: {
|
||||
title: "Number of Sessions",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Update category chart
|
||||
const categoryData = data.category_data;
|
||||
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
||||
const categoryLabels = categoryData.map((item) => item.category);
|
||||
const categoryValues = categoryData.map((item) => item.count);
|
||||
// Update category chart
|
||||
const categoryData = data.category_data;
|
||||
if (categoryData && categoryData.length > 0 && window.Plotly) {
|
||||
const categoryLabels = categoryData.map((item) => item.category);
|
||||
const categoryValues = categoryData.map((item) => item.count);
|
||||
|
||||
Plotly.react(
|
||||
"category-chart",
|
||||
[
|
||||
{
|
||||
labels: categoryLabels,
|
||||
values: categoryValues,
|
||||
type: "pie",
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Plotly.react(
|
||||
"category-chart",
|
||||
[
|
||||
{
|
||||
labels: categoryLabels,
|
||||
values: categoryValues,
|
||||
type: "pie",
|
||||
textinfo: "label+percent",
|
||||
insidetextorientation: "radial",
|
||||
},
|
||||
],
|
||||
{
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard selector
|
||||
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
||||
dashboardSelector.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
// Dashboard selector
|
||||
const dashboardSelector = document.querySelectorAll('a[href^="?dashboard_id="]');
|
||||
dashboardSelector.forEach((link) => {
|
||||
link.addEventListener("click", function (e) {
|
||||
const url = new URL(this.href);
|
||||
const dashboardId = url.searchParams.get("dashboard_id");
|
||||
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
// Fetch updated data via AJAX
|
||||
if (dashboardId) {
|
||||
fetchDashboardData(dashboardId);
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -6,147 +6,147 @@
|
||||
*/
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Initialize tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
// Initialize tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize popovers
|
||||
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
// Initialize popovers
|
||||
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
|
||||
// Toggle sidebar on mobile
|
||||
const sidebarToggle = document.querySelector("#sidebarToggle");
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener("click", function () {
|
||||
document.querySelector(".sidebar").classList.toggle("show");
|
||||
});
|
||||
}
|
||||
// Toggle sidebar on mobile
|
||||
const sidebarToggle = document.querySelector("#sidebarToggle");
|
||||
if (sidebarToggle) {
|
||||
sidebarToggle.addEventListener("click", function () {
|
||||
document.querySelector(".sidebar").classList.toggle("show");
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-dismiss alerts after 5 seconds
|
||||
setTimeout(function () {
|
||||
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
||||
alerts.forEach(function (alert) {
|
||||
if (alert && bootstrap.Alert.getInstance(alert)) {
|
||||
bootstrap.Alert.getInstance(alert).close();
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
// Auto-dismiss alerts after 5 seconds
|
||||
setTimeout(function () {
|
||||
var alerts = document.querySelectorAll(".alert:not(.alert-important)");
|
||||
alerts.forEach(function (alert) {
|
||||
if (alert && bootstrap.Alert.getInstance(alert)) {
|
||||
bootstrap.Alert.getInstance(alert).close();
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Form validation
|
||||
const forms = document.querySelectorAll(".needs-validation");
|
||||
forms.forEach(function (form) {
|
||||
form.addEventListener(
|
||||
"submit",
|
||||
function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add("was-validated");
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
// Form validation
|
||||
const forms = document.querySelectorAll(".needs-validation");
|
||||
forms.forEach(function (form) {
|
||||
form.addEventListener(
|
||||
"submit",
|
||||
function (event) {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
form.classList.add("was-validated");
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
// Confirm dialogs
|
||||
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
||||
confirmButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
// Confirm dialogs
|
||||
const confirmButtons = document.querySelectorAll("[data-confirm]");
|
||||
confirmButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
if (!confirm(this.dataset.confirm || "Are you sure?")) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Back button
|
||||
const backButtons = document.querySelectorAll(".btn-back");
|
||||
backButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
window.history.back();
|
||||
});
|
||||
});
|
||||
// Back button
|
||||
const backButtons = document.querySelectorAll(".btn-back");
|
||||
backButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
window.history.back();
|
||||
});
|
||||
});
|
||||
|
||||
// File input customization
|
||||
const fileInputs = document.querySelectorAll(".custom-file-input");
|
||||
fileInputs.forEach(function (input) {
|
||||
input.addEventListener("change", function (e) {
|
||||
const fileName = this.files[0]?.name || "Choose file";
|
||||
const nextSibling = this.nextElementSibling;
|
||||
if (nextSibling) {
|
||||
nextSibling.innerText = fileName;
|
||||
}
|
||||
});
|
||||
});
|
||||
// File input customization
|
||||
const fileInputs = document.querySelectorAll(".custom-file-input");
|
||||
fileInputs.forEach(function (input) {
|
||||
input.addEventListener("change", function (e) {
|
||||
const fileName = this.files[0]?.name || "Choose file";
|
||||
const nextSibling = this.nextElementSibling;
|
||||
if (nextSibling) {
|
||||
nextSibling.innerText = fileName;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Search form submit on enter
|
||||
const searchInputs = document.querySelectorAll(".search-input");
|
||||
searchInputs.forEach(function (input) {
|
||||
input.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.closest("form").submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
// Search form submit on enter
|
||||
const searchInputs = document.querySelectorAll(".search-input");
|
||||
searchInputs.forEach(function (input) {
|
||||
input.addEventListener("keypress", function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.closest("form").submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle password visibility
|
||||
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
||||
togglePasswordButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
const target = document.querySelector(this.dataset.target);
|
||||
if (target) {
|
||||
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
||||
target.setAttribute("type", type);
|
||||
this.querySelector("i").classList.toggle("fa-eye");
|
||||
this.querySelector("i").classList.toggle("fa-eye-slash");
|
||||
}
|
||||
});
|
||||
});
|
||||
// Toggle password visibility
|
||||
const togglePasswordButtons = document.querySelectorAll(".toggle-password");
|
||||
togglePasswordButtons.forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
const target = document.querySelector(this.dataset.target);
|
||||
if (target) {
|
||||
const type = target.getAttribute("type") === "password" ? "text" : "password";
|
||||
target.setAttribute("type", type);
|
||||
this.querySelector("i").classList.toggle("fa-eye");
|
||||
this.querySelector("i").classList.toggle("fa-eye-slash");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Dropdown menu positioning
|
||||
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
||||
dropdowns.forEach(function (dropdown) {
|
||||
dropdown.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
// Dropdown menu positioning
|
||||
const dropdowns = document.querySelectorAll(".dropdown-menu");
|
||||
dropdowns.forEach(function (dropdown) {
|
||||
dropdown.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
// Responsive table handling
|
||||
const tables = document.querySelectorAll(".table-responsive");
|
||||
if (window.innerWidth < 768) {
|
||||
tables.forEach(function (table) {
|
||||
table.classList.add("table-responsive-force");
|
||||
});
|
||||
}
|
||||
// Responsive table handling
|
||||
const tables = document.querySelectorAll(".table-responsive");
|
||||
if (window.innerWidth < 768) {
|
||||
tables.forEach(function (table) {
|
||||
table.classList.add("table-responsive-force");
|
||||
});
|
||||
}
|
||||
|
||||
// Handle special links (printable views, exports)
|
||||
const printLinks = document.querySelectorAll(".print-link");
|
||||
printLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
window.print();
|
||||
});
|
||||
});
|
||||
// Handle special links (printable views, exports)
|
||||
const printLinks = document.querySelectorAll(".print-link");
|
||||
printLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
window.print();
|
||||
});
|
||||
});
|
||||
|
||||
const exportLinks = document.querySelectorAll("[data-export]");
|
||||
exportLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
// Handle export functionality if needed
|
||||
console.log("Export requested:", this.dataset.export);
|
||||
});
|
||||
});
|
||||
const exportLinks = document.querySelectorAll("[data-export]");
|
||||
exportLinks.forEach(function (link) {
|
||||
link.addEventListener("click", function (e) {
|
||||
// Handle export functionality if needed
|
||||
console.log("Export requested:", this.dataset.export);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle sidebar collapse on small screens
|
||||
function handleSidebarOnResize() {
|
||||
if (window.innerWidth < 768) {
|
||||
document.querySelector(".sidebar")?.classList.remove("show");
|
||||
}
|
||||
}
|
||||
// Handle sidebar collapse on small screens
|
||||
function handleSidebarOnResize() {
|
||||
if (window.innerWidth < 768) {
|
||||
document.querySelector(".sidebar")?.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", handleSidebarOnResize);
|
||||
window.addEventListener("resize", handleSidebarOnResize);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user