huge updates
This commit is contained in:
57 files changed
+10143
-4185
No files matched your search
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
// Initialize Bootstrap components
|
||||
function initializeBootstrapComponents() {
|
||||
// Initialize tooltips if available
|
||||
if (typeof bootstrap !== "undefined" && bootstrap.Tooltip) {
|
||||
const tooltipTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]'),
|
||||
);
|
||||
tooltipTriggerList.map(function(tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize popovers if available
|
||||
if (typeof bootstrap !== "undefined" && bootstrap.Popover) {
|
||||
const popoverTriggerList = [].slice.call(
|
||||
document.querySelectorAll('[data-bs-toggle="popover"]'),
|
||||
);
|
||||
popoverTriggerList.map(function(popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Make function globally accessible
|
||||
window.initializeBootstrapComponents = initializeBootstrapComponents;
|
||||
+757
-535
File diff suppressed because it is too large.
Load diff
@@ -1,113 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Handle star rating selection for teacher feedback
|
||||
const teacherStars = document.querySelectorAll('[id^="teacherStar"]');
|
||||
const teacherLabels = document.querySelectorAll('label[for^="teacherStar"]');
|
||||
|
||||
teacherLabels.forEach(label => {
|
||||
label.addEventListener('click', function() {
|
||||
const starId = this.getAttribute('for');
|
||||
const starNumber = parseInt(starId.replace('teacherStar', ''));
|
||||
|
||||
// Update visual appearance
|
||||
teacherLabels.forEach((lbl, index) => {
|
||||
const star = lbl.querySelector('i');
|
||||
if (index < starNumber) {
|
||||
star.className = 'fas fa-star fs-4 text-warning';
|
||||
} else {
|
||||
star.className = 'far fa-star fs-4 text-warning';
|
||||
}
|
||||
});
|
||||
|
||||
// Set the correct radio button
|
||||
document.getElementById(starId).checked = true;
|
||||
});
|
||||
|
||||
// Add hover effects
|
||||
label.addEventListener('mouseenter', function() {
|
||||
const starId = this.getAttribute('for');
|
||||
const starNumber = parseInt(starId.replace('teacherStar', ''));
|
||||
|
||||
teacherLabels.forEach((lbl, index) => {
|
||||
const star = lbl.querySelector('i');
|
||||
if (index < starNumber) {
|
||||
star.className = 'fas fa-star fs-4 text-warning';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
label.addEventListener('mouseleave', function() {
|
||||
// Reset to selected state
|
||||
let selectedValue = 0;
|
||||
teacherStars.forEach((radio, index) => {
|
||||
if (radio.checked) {
|
||||
selectedValue = index + 1;
|
||||
}
|
||||
});
|
||||
|
||||
teacherLabels.forEach((lbl, index) => {
|
||||
const star = lbl.querySelector('i');
|
||||
if (index < selectedValue) {
|
||||
star.className = 'fas fa-star fs-4 text-warning';
|
||||
} else {
|
||||
star.className = 'far fa-star fs-4 text-warning';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle star rating selection for institute feedback
|
||||
const instituteStars = document.querySelectorAll('[id^="instituteStar"]');
|
||||
const instituteLabels = document.querySelectorAll('label[for^="instituteStar"]');
|
||||
|
||||
instituteLabels.forEach(label => {
|
||||
label.addEventListener('click', function() {
|
||||
const starId = this.getAttribute('for');
|
||||
const starNumber = parseInt(starId.replace('instituteStar', ''));
|
||||
|
||||
// Update visual appearance
|
||||
instituteLabels.forEach((lbl, index) => {
|
||||
const star = lbl.querySelector('i');
|
||||
if (index < starNumber) {
|
||||
star.className = 'fas fa-star fs-4 text-warning';
|
||||
} else {
|
||||
star.className = 'far fa-star fs-4 text-warning';
|
||||
}
|
||||
});
|
||||
|
||||
// Set the correct radio button
|
||||
document.getElementById(starId).checked = true;
|
||||
});
|
||||
|
||||
// Add hover effects
|
||||
label.addEventListener('mouseenter', function() {
|
||||
const starId = this.getAttribute('for');
|
||||
const starNumber = parseInt(starId.replace('instituteStar', ''));
|
||||
|
||||
instituteLabels.forEach((lbl, index) => {
|
||||
const star = lbl.querySelector('i');
|
||||
if (index < starNumber) {
|
||||
star.className = 'fas fa-star fs-4 text-warning';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
label.addEventListener('mouseleave', function() {
|
||||
// Reset to selected state
|
||||
let selectedValue = 0;
|
||||
instituteStars.forEach((radio, index) => {
|
||||
if (radio.checked) {
|
||||
selectedValue = index + 1;
|
||||
}
|
||||
});
|
||||
|
||||
instituteLabels.forEach((lbl, index) => {
|
||||
const star = lbl.querySelector('i');
|
||||
if (index < selectedValue) {
|
||||
star.className = 'fas fa-star fs-4 text-warning';
|
||||
} else {
|
||||
star.className = 'far fa-star fs-4 text-warning';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// Firebase configuration
|
||||
const firebaseConfig = {
|
||||
apiKey: "YOUR_API_KEY",
|
||||
authDomain: "YOUR_AUTH_DOMAIN",
|
||||
projectId: "YOUR_PROJECT_ID",
|
||||
storageBucket: "YOUR_STORAGE_BUCKET",
|
||||
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
|
||||
appId: "YOUR_APP_ID",
|
||||
measurementId: "YOUR_MEASUREMENT_ID"
|
||||
};
|
||||
|
||||
// VAPID key for push notifications
|
||||
const vapidKey = "YOUR_VAPID_KEY";
|
||||
@@ -0,0 +1,160 @@
|
||||
// Get CSRF token from cookie
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
// Initialize Firebase
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
const messaging = firebase.messaging();
|
||||
|
||||
async function registerServiceWorker() {
|
||||
try {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
throw new Error('Service Worker is not supported in this browser');
|
||||
}
|
||||
|
||||
// Unregister any existing service workers
|
||||
const existingRegistrations = await navigator.serviceWorker.getRegistrations();
|
||||
for (let registration of existingRegistrations) {
|
||||
await registration.unregister();
|
||||
}
|
||||
|
||||
// Register the new service worker
|
||||
const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js', {
|
||||
scope: '/',
|
||||
updateViaCache: 'none'
|
||||
});
|
||||
|
||||
console.log('Service Worker registered with scope:', registration.scope);
|
||||
await navigator.serviceWorker.ready;
|
||||
return registration;
|
||||
} catch (error) {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function getDeviceToken() {
|
||||
try {
|
||||
const registration = await registerServiceWorker();
|
||||
|
||||
// Request notification permission if not already granted
|
||||
let permission = Notification.permission;
|
||||
if (permission === 'default') {
|
||||
permission = await Notification.requestPermission();
|
||||
}
|
||||
|
||||
if (permission !== 'granted') {
|
||||
throw new Error('Notification permission not granted');
|
||||
}
|
||||
|
||||
// Get FCM token
|
||||
const token = await messaging.getToken({
|
||||
vapidKey: vapidKey,
|
||||
serviceWorkerRegistration: registration
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Failed to get FCM token');
|
||||
}
|
||||
|
||||
console.log('Got FCM token:', token);
|
||||
|
||||
// Get CSRF token
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
if (!csrftoken) {
|
||||
throw new Error('CSRF token not found');
|
||||
}
|
||||
|
||||
// Send the token to your backend for storage
|
||||
const response = await fetch('/app/saveFCMToken/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrftoken
|
||||
},
|
||||
body: JSON.stringify({ token: token })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save token to server');
|
||||
}
|
||||
|
||||
console.log('FCM Token saved successfully');
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error('Error in getDeviceToken:', error);
|
||||
try {
|
||||
// Clean up and retry on failure
|
||||
await messaging.deleteToken().catch(console.error);
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
for (let registration of registrations) {
|
||||
await registration.unregister();
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return getDeviceToken();
|
||||
} catch (retryError) {
|
||||
console.error('Error during cleanup and retry:', retryError);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make getDeviceToken available globally
|
||||
window.getDeviceToken = getDeviceToken;
|
||||
|
||||
// Handle token refresh
|
||||
messaging.onTokenRefresh = async () => {
|
||||
try {
|
||||
await getDeviceToken();
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle foreground messages
|
||||
messaging.onMessage((payload) => {
|
||||
console.log('Foreground message received:', payload);
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
const notificationTitle = payload.notification.title;
|
||||
const notificationOptions = {
|
||||
body: payload.notification.body,
|
||||
icon: payload.notification.icon || '/static/img/logo.png',
|
||||
badge: '/static/img/logo.png',
|
||||
timestamp: Date.now(),
|
||||
requireInteraction: true,
|
||||
vibrate: [200, 100, 200],
|
||||
tag: 'notification-' + Date.now(),
|
||||
data: payload.data || {},
|
||||
actions: [
|
||||
{
|
||||
action: 'open',
|
||||
title: 'Open'
|
||||
},
|
||||
{
|
||||
action: 'close',
|
||||
title: 'Close'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.showNotification(notificationTitle, notificationOptions)
|
||||
.catch(error => {
|
||||
console.error('Error showing notification:', error);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// Setup feedback form validation
|
||||
function setupRatingFormValidation() {
|
||||
const ratingForm = document.querySelector(".feedback-form");
|
||||
const ratingInput = document.getElementById("selected_rating");
|
||||
const ratingError = document.getElementById("rating-error");
|
||||
|
||||
if (ratingForm && ratingInput && ratingError) {
|
||||
// Add form validation before submission
|
||||
ratingForm.addEventListener("submit", function(e) {
|
||||
// Check if rating is selected
|
||||
if (!ratingInput.value) {
|
||||
e.preventDefault(); // Stop form submission
|
||||
if (ratingError) {
|
||||
ratingError.style.display = "block";
|
||||
ratingError.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Make function globally accessible
|
||||
window.setupRatingFormValidation = setupRatingFormValidation;
|
||||
@@ -0,0 +1,88 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
// Get all navigation items
|
||||
const navItems = document.querySelectorAll('.nav-item[data-section]');
|
||||
|
||||
// Clear active state from all navigation items immediately
|
||||
navItems.forEach(nav => nav.classList.remove('active'));
|
||||
|
||||
// Hide all sections initially to prevent flickering
|
||||
document.querySelectorAll('.content-section').forEach(section => {
|
||||
section.classList.remove('active');
|
||||
section.style.display = 'none';
|
||||
section.style.visibility = 'hidden';
|
||||
});
|
||||
|
||||
// Function to switch to a section
|
||||
function switchToSection(targetSection, skipStorage = false) {
|
||||
if (!targetSection) return;
|
||||
|
||||
if (!skipStorage) {
|
||||
sessionStorage.setItem('activeSection', targetSection);
|
||||
}
|
||||
|
||||
// Remove active class from all nav items
|
||||
navItems.forEach(nav => nav.classList.remove('active'));
|
||||
|
||||
// Add active class to corresponding nav item
|
||||
const activeNav = document.querySelector(`.nav-item[data-section="${targetSection}"]`);
|
||||
if (activeNav) {
|
||||
activeNav.classList.add('active');
|
||||
}
|
||||
|
||||
// Hide all sections first
|
||||
document.querySelectorAll('.content-section').forEach(section => {
|
||||
section.classList.remove('active');
|
||||
section.style.display = 'none';
|
||||
section.style.visibility = 'hidden';
|
||||
});
|
||||
|
||||
// Show the target section
|
||||
const targetElement = document.getElementById(`${targetSection}Section`);
|
||||
|
||||
if (targetElement) {
|
||||
targetElement.classList.add('active');
|
||||
targetElement.style.display = 'block';
|
||||
targetElement.style.visibility = 'visible';
|
||||
|
||||
// Update URL hash without scrolling
|
||||
history.replaceState(null, null, `#${targetSection}`);
|
||||
}
|
||||
|
||||
// Make the page visible now that everything is ready
|
||||
document.body.style.opacity = "1";
|
||||
}
|
||||
|
||||
// Handle initial page load before showing content
|
||||
function handleInitialLoad() {
|
||||
let targetSection = 'dashboard'; // Default section
|
||||
|
||||
if (window.location.hash) {
|
||||
targetSection = window.location.hash.substring(1);
|
||||
} else if (sessionStorage.getItem('activeSection')) {
|
||||
targetSection = sessionStorage.getItem('activeSection');
|
||||
}
|
||||
|
||||
// Switch to the target section without saving to sessionStorage again
|
||||
switchToSection(targetSection, true);
|
||||
}
|
||||
|
||||
// Add event listeners to navigation items
|
||||
navItems.forEach(item => {
|
||||
const section = item.getAttribute('data-section');
|
||||
|
||||
item.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
switchToSection(section);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle hash changes (browser back/forward)
|
||||
window.addEventListener('hashchange', function () {
|
||||
const targetSection = window.location.hash.substring(1) || 'dashboard';
|
||||
switchToSection(targetSection);
|
||||
});
|
||||
|
||||
// Run initial load before rendering content
|
||||
handleInitialLoad();
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
// Function to show notification toast
|
||||
function showNotification(title, message, type = "success") {
|
||||
const toast = document.getElementById("notificationToast");
|
||||
if (!toast) return;
|
||||
|
||||
const toastTitle = document.getElementById("toastTitle");
|
||||
const toastMessage = document.getElementById("toastMessage");
|
||||
|
||||
toastTitle.innerText = title;
|
||||
toastMessage.innerText = message;
|
||||
|
||||
// Set toast color based on type
|
||||
toast.className = "toast";
|
||||
if (type === "success") {
|
||||
toast.classList.add("bg-success", "text-white");
|
||||
} else if (type === "error") {
|
||||
toast.classList.add("bg-danger", "text-white");
|
||||
} else if (type === "warning") {
|
||||
toast.classList.add("bg-warning");
|
||||
} else if (type === "info") {
|
||||
toast.classList.add("bg-info", "text-white");
|
||||
}
|
||||
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
}
|
||||
|
||||
// Make function globally accessible
|
||||
window.showNotification = showNotification;
|
||||
@@ -0,0 +1,163 @@
|
||||
// Function to set up star rating
|
||||
function setupStarRating() {
|
||||
console.log("Setting up star rating...");
|
||||
|
||||
// Handle display of existing ratings (read-only stars)
|
||||
setupExistingRatings();
|
||||
}
|
||||
|
||||
// Function to set up existing ratings (read-only stars)
|
||||
function setupExistingRatings() {
|
||||
// Find all rating divs (not in forms)
|
||||
document.querySelectorAll('.rating').forEach(container => {
|
||||
if (!container) return;
|
||||
|
||||
// Make sure the stars have the right color
|
||||
const stars = container.querySelectorAll('i.fas, i.far');
|
||||
stars.forEach(star => {
|
||||
if (star.classList.contains('fas')) {
|
||||
star.style.color = '#FFC107';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add CSS styles for rating stars
|
||||
(function() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.rating-star {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.rating-star.hover,
|
||||
.rating-star:hover,
|
||||
.rating-star.active,
|
||||
.rating-star.selected {
|
||||
color: #FFD700 !important;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
|
||||
// Global function to select rating stars
|
||||
function selectRating(star, inputId) {
|
||||
console.log('selectRating called with star value:', star.getAttribute('data-value'), 'for input:', inputId);
|
||||
|
||||
// Get the value
|
||||
const value = parseInt(star.getAttribute('data-value'));
|
||||
|
||||
// Find and update the input
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
input.value = value;
|
||||
console.log('Updated input value to:', value);
|
||||
} else {
|
||||
console.error('Input element not found with ID:', inputId);
|
||||
}
|
||||
|
||||
// Update the stars
|
||||
const container = star.closest('.rating-stars');
|
||||
if (container) {
|
||||
const stars = container.querySelectorAll('.rating-star');
|
||||
stars.forEach(s => {
|
||||
const starValue = parseInt(s.getAttribute('data-value'));
|
||||
if (starValue <= value) {
|
||||
s.classList.remove('far');
|
||||
s.classList.add('fas');
|
||||
s.classList.add('selected');
|
||||
} else {
|
||||
s.classList.remove('fas');
|
||||
s.classList.add('far');
|
||||
s.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Rating container not found');
|
||||
}
|
||||
|
||||
// Hide error message if any
|
||||
let errorId;
|
||||
if (inputId === 'teacher_rating') {
|
||||
errorId = 'teacher-rating-error';
|
||||
} else if (inputId === 'institute_rating') {
|
||||
errorId = 'institute-rating-error';
|
||||
} else if (inputId === 'teacher_modal_rating') {
|
||||
errorId = 'teacher-modal-rating-error';
|
||||
}
|
||||
|
||||
if (errorId) {
|
||||
const errorElement = document.getElementById(errorId);
|
||||
if (errorElement) {
|
||||
errorElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize rating functionality when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log("DOM fully loaded");
|
||||
setupStarRating();
|
||||
|
||||
// Add an additional check after a short delay to catch dynamically loaded content
|
||||
setTimeout(setupStarRating, 500);
|
||||
});
|
||||
|
||||
// Initialize on document load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Rating.js loaded');
|
||||
|
||||
// Apply rating stars to static ratings (read-only)
|
||||
const ratingContainers = document.querySelectorAll('.rating');
|
||||
console.log('Found', ratingContainers.length, 'static rating containers');
|
||||
|
||||
ratingContainers.forEach(container => {
|
||||
const stars = container.querySelectorAll('i');
|
||||
stars.forEach(star => {
|
||||
if (star.classList.contains('fas')) {
|
||||
star.style.color = '#FFC107';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Apply hover effects to rating stars
|
||||
const ratingStars = document.querySelectorAll('.rating-star');
|
||||
console.log('Found', ratingStars.length, 'interactive rating stars');
|
||||
|
||||
ratingStars.forEach(star => {
|
||||
star.addEventListener('mouseover', function() {
|
||||
// Find all stars in the same container
|
||||
const container = this.closest('.rating-stars');
|
||||
if (container) {
|
||||
const stars = container.querySelectorAll('.rating-star');
|
||||
const value = parseInt(this.getAttribute('data-value'));
|
||||
|
||||
stars.forEach(s => {
|
||||
const starValue = parseInt(s.getAttribute('data-value'));
|
||||
if (starValue <= value) {
|
||||
s.classList.add('hover');
|
||||
} else {
|
||||
s.classList.remove('hover');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
star.addEventListener('mouseout', function() {
|
||||
// Find all stars in the same container
|
||||
const container = this.closest('.rating-stars');
|
||||
if (container) {
|
||||
const stars = container.querySelectorAll('.rating-star');
|
||||
stars.forEach(s => {
|
||||
s.classList.remove('hover');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Make function globally available
|
||||
window.selectRating = selectRating;
|
||||
@@ -0,0 +1,169 @@
|
||||
// Functions for handling student modals
|
||||
|
||||
// Close PDF viewer modal
|
||||
function closePdfViewerModal() {
|
||||
document.getElementById('pdfViewerModal').style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Close subject files modal
|
||||
function closeSubjectFilesModal() {
|
||||
document.getElementById('subjectFilesModal').style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Open subject files modal and load content
|
||||
function openSubjectFilesModal(subjectId) {
|
||||
const modal = document.getElementById('subjectFilesModal');
|
||||
const filesList = document.getElementById('subjectFilesList');
|
||||
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
filesList.innerHTML = `
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading subject files...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch(`/app/subject/${subjectId}/files/`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
filesList.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i> ${data.message || 'No files available for this subject'}
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.files || data.files.length === 0) {
|
||||
filesList.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i> No study materials available for this subject.
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filesList.innerHTML = data.files.map(file => {
|
||||
const fileExtension = file.file_url ? file.file_url.split('.').pop().toLowerCase() : '';
|
||||
const iconClass = getFileIcon(fileExtension);
|
||||
const fileIcon = `<i class="fas ${iconClass} ${fileExtension === 'pdf' ? 'text-danger' : 'text-primary'} me-2"></i>`;
|
||||
|
||||
return `
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-1">
|
||||
${fileIcon}
|
||||
${file.title}
|
||||
</h6>
|
||||
<p class="mb-1 small text-muted">${file.description || 'No description'}</p>
|
||||
<small class="text-muted">
|
||||
${file.file_type === 'syllabus'
|
||||
? '<span class="badge bg-info me-1">Syllabus</span>'
|
||||
: '<span class="badge bg-primary me-1">Study Material</span>'}
|
||||
${file.uploaded_by ? `<span>Uploaded by: ${file.uploaded_by}</span>` : ''}
|
||||
${file.uploaded_at ? `<span class="ms-2">• ${formatDate(file.uploaded_at)}</span>` : ''}
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
${file.file_url ? `
|
||||
<div class="btn-group" role="group">
|
||||
<a href="${file.file_url}" class="btn btn-primary btn-sm" target="_blank">
|
||||
<i class="fas fa-download me-1"></i> Download
|
||||
</a>
|
||||
<a href="${file.file_url}" class="btn btn-secondary btn-sm" target="_blank">
|
||||
<i class="fas fa-eye me-1"></i> View
|
||||
</a>
|
||||
</div>
|
||||
` : `
|
||||
<span class="text-danger">
|
||||
<i class="fas fa-exclamation-circle me-1"></i> File not available
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching subject files:', error);
|
||||
filesList.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i> Failed to load files. Please try again.
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// Get appropriate icon based on file type
|
||||
function getFileIcon(fileType) {
|
||||
const iconMap = {
|
||||
'pdf': 'fa-file-pdf',
|
||||
'doc': 'fa-file-word',
|
||||
'docx': 'fa-file-word',
|
||||
'xls': 'fa-file-excel',
|
||||
'xlsx': 'fa-file-excel',
|
||||
'ppt': 'fa-file-powerpoint',
|
||||
'pptx': 'fa-file-powerpoint',
|
||||
'txt': 'fa-file-alt',
|
||||
'zip': 'fa-file-archive',
|
||||
'rar': 'fa-file-archive',
|
||||
'image': 'fa-file-image',
|
||||
'video': 'fa-file-video',
|
||||
'audio': 'fa-file-audio'
|
||||
};
|
||||
return iconMap[fileType] || 'fa-file';
|
||||
}
|
||||
|
||||
// Format file size for display
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Format date for display
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize event listeners when the DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Handle Escape key to close modals
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
// Close all open modals
|
||||
const pdfModal = document.getElementById('pdfViewerModal');
|
||||
const filesModal = document.getElementById('subjectFilesModal');
|
||||
|
||||
if (pdfModal && pdfModal.style.display === 'block') {
|
||||
closePdfViewerModal();
|
||||
}
|
||||
|
||||
if (filesModal && filesModal.style.display === 'block') {
|
||||
closeSubjectFilesModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
// Setup custom tabs
|
||||
function setupCustomTabs() {
|
||||
const tabButtons = document.querySelectorAll(".custom-tab-btn");
|
||||
if (tabButtons.length > 0) {
|
||||
tabButtons.forEach((button) => {
|
||||
button.addEventListener("click", function() {
|
||||
// Remove active class from all buttons
|
||||
tabButtons.forEach((btn) => btn.classList.remove("active"));
|
||||
|
||||
// Add active class to clicked button
|
||||
this.classList.add("active");
|
||||
|
||||
// Hide all content
|
||||
document.querySelectorAll(".custom-tab-content").forEach((content) => {
|
||||
content.classList.remove("active");
|
||||
});
|
||||
|
||||
// Show target content
|
||||
const target = this.getAttribute("data-target");
|
||||
document.getElementById(target).classList.add("active");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Setup feedback tabs
|
||||
function setupFeedbackTabs() {
|
||||
const teacherTabBtn = document.getElementById("teacher-tab-btn");
|
||||
const instituteTabBtn = document.getElementById("institute-tab-btn");
|
||||
|
||||
if (teacherTabBtn && instituteTabBtn) {
|
||||
teacherTabBtn.addEventListener("click", function() {
|
||||
switchFeedbackTab("teacher");
|
||||
});
|
||||
|
||||
instituteTabBtn.addEventListener("click", function() {
|
||||
switchFeedbackTab("institute");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Tab switching function
|
||||
function switchFeedbackTab(tabId) {
|
||||
// Hide all tab contents
|
||||
document.querySelectorAll(".tab-pane").forEach((tab) => {
|
||||
tab.classList.remove("active");
|
||||
});
|
||||
|
||||
// Deactivate all tab buttons
|
||||
document.querySelectorAll(".feedback-tab-btn").forEach((btn) => {
|
||||
btn.classList.remove("active");
|
||||
});
|
||||
|
||||
// Show selected tab content and activate button
|
||||
document.getElementById(`${tabId}-tab-content`).classList.add("active");
|
||||
document.getElementById(`${tabId}-tab-btn`).classList.add("active");
|
||||
}
|
||||
|
||||
// Make functions globally accessible
|
||||
window.setupCustomTabs = setupCustomTabs;
|
||||
window.setupFeedbackTabs = setupFeedbackTabs;
|
||||
window.switchFeedbackTab = switchFeedbackTab;
|
||||
@@ -0,0 +1,379 @@
|
||||
// Attendance Functions for Teacher Dashboard
|
||||
|
||||
// Constants
|
||||
const ATTENDANCE_MODAL_ID = 'attendanceModal';
|
||||
const QUICK_ATTENDANCE_FORM_ID = 'quickAttendanceForm';
|
||||
const ATTENDANCE_FORM_ID = 'attendanceForm';
|
||||
|
||||
// Attendance Modal Functions
|
||||
async function openAttendanceModal(routineId, date) {
|
||||
try {
|
||||
console.log('Opening attendance modal for routine:', routineId, 'date:', date);
|
||||
|
||||
// Validate and clean inputs
|
||||
routineId = routineId?.trim().replace(/['"]+/g, '');
|
||||
date = date || getCurrentDate();
|
||||
|
||||
if (!routineId) {
|
||||
throw new Error('Invalid routine ID');
|
||||
}
|
||||
|
||||
// Get required elements
|
||||
const modal = document.getElementById(ATTENDANCE_MODAL_ID);
|
||||
const modalRoutineId = document.getElementById('modalRoutineId');
|
||||
const modalDate = document.getElementById('modalDate');
|
||||
const modalStudentList = document.getElementById('modalStudentList');
|
||||
|
||||
if (!modal || !modalRoutineId || !modalDate || !modalStudentList) {
|
||||
throw new Error('Required modal elements not found');
|
||||
}
|
||||
|
||||
// Set form values
|
||||
modalRoutineId.value = routineId;
|
||||
modalDate.value = date;
|
||||
|
||||
// Show loading state
|
||||
showLoadingSpinner(modalStudentList, 'Loading students...');
|
||||
|
||||
// Show modal
|
||||
showModal(ATTENDANCE_MODAL_ID);
|
||||
|
||||
// Fetch students
|
||||
const data = await apiRequest(`${API_BASE_URL}/get-students/?routine_id=${routineId}`);
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
// Render student list with enhanced UI
|
||||
renderStudentList(modalStudentList, data.students);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in openAttendanceModal:', error);
|
||||
showError(document.getElementById('modalStudentList'), error);
|
||||
hideModal(ATTENDANCE_MODAL_ID);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStudentList(container, students) {
|
||||
if (!container || !Array.isArray(students)) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<tbody>
|
||||
${students.map((student, index) => `
|
||||
<tr>
|
||||
<td>${index + 1}</td>
|
||||
<td>${student.name}</td>
|
||||
<td>${student.id}</td>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="student_${student.id}"
|
||||
name="student_${student.id}"
|
||||
aria-label="Mark ${student.name} as present">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
function closeAttendanceModal() {
|
||||
hideModal(ATTENDANCE_MODAL_ID);
|
||||
resetForm(QUICK_ATTENDANCE_FORM_ID);
|
||||
}
|
||||
|
||||
// Attendance View Functions
|
||||
function toggleAttendanceView(view) {
|
||||
const headerSection = document.getElementById('attendanceHeader');
|
||||
const manageView = document.getElementById('manageAttendanceView');
|
||||
const routineSelectionForm = document.getElementById('routineSelectionForm');
|
||||
const attendanceFormContainer = document.getElementById('attendanceFormContainer');
|
||||
|
||||
if (view === 'recent') {
|
||||
// Show recent view
|
||||
headerSection.style.display = 'block';
|
||||
manageView.style.display = 'none';
|
||||
|
||||
// Reset form and container
|
||||
if (attendanceFormContainer) {
|
||||
attendanceFormContainer.style.display = 'none';
|
||||
attendanceFormContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// Safely reset routine selection form
|
||||
if (routineSelectionForm) {
|
||||
routineSelectionForm.style.display = 'block';
|
||||
// Reset form elements individually to avoid potential issues
|
||||
const routineSelect = routineSelectionForm.querySelector('#routineSelect');
|
||||
const dateSelect = routineSelectionForm.querySelector('#dateSelect');
|
||||
|
||||
if (routineSelect) {
|
||||
routineSelect.selectedIndex = 0;
|
||||
}
|
||||
|
||||
if (dateSelect) {
|
||||
dateSelect.value = '';
|
||||
}
|
||||
|
||||
// Clear any error messages
|
||||
routineSelectionForm.querySelectorAll('.alert').forEach(alert => alert.remove());
|
||||
}
|
||||
} else {
|
||||
// Show manage view
|
||||
headerSection.style.display = 'none';
|
||||
manageView.style.display = 'block';
|
||||
|
||||
// Load data if routine and date are selected
|
||||
const routineSelect = document.getElementById('routineSelect');
|
||||
const dateSelect = document.getElementById('dateSelect');
|
||||
|
||||
if (routineSelect?.value && dateSelect?.value) {
|
||||
loadAttendanceData();
|
||||
} else if (attendanceFormContainer) {
|
||||
attendanceFormContainer.style.display = 'none';
|
||||
attendanceFormContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attendance Data Functions
|
||||
async function editAttendance(routineId, date) {
|
||||
try {
|
||||
console.log('Editing attendance for routine:', routineId, 'date:', date);
|
||||
|
||||
// Switch to manage view
|
||||
toggleAttendanceView('manage');
|
||||
|
||||
// Hide routine selection form
|
||||
const routineSelectionForm = document.getElementById('routineSelectionForm');
|
||||
if (routineSelectionForm) {
|
||||
routineSelectionForm.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update form inputs
|
||||
const attendanceForm = document.getElementById(ATTENDANCE_FORM_ID);
|
||||
if (attendanceForm) {
|
||||
const routineInput = attendanceForm.querySelector('input[name="routine_id"]');
|
||||
const dateInput = attendanceForm.querySelector('input[name="date"]');
|
||||
if (routineInput) routineInput.value = routineId;
|
||||
if (dateInput) dateInput.value = date;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const container = document.getElementById('attendanceFormContainer');
|
||||
if (!container) return;
|
||||
|
||||
container.style.display = 'block';
|
||||
showLoadingSpinner(container, 'Loading attendance data...');
|
||||
|
||||
// Fetch attendance data
|
||||
const [attendanceData, studentsData] = await Promise.all([
|
||||
apiRequest(`${API_BASE_URL}/get-attendance-form/?routine_id=${routineId}&date=${date}`),
|
||||
apiRequest(`${API_BASE_URL}/get-students/?routine_id=${routineId}`)
|
||||
]);
|
||||
|
||||
if (attendanceData.error || studentsData.error) {
|
||||
throw new Error(attendanceData.error || studentsData.error);
|
||||
}
|
||||
|
||||
// Render attendance form
|
||||
renderAttendanceForm(container, routineId, date, attendanceData, studentsData.students);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error editing attendance:', error);
|
||||
if (container) {
|
||||
showError(container, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderAttendanceForm(container, routineId, date, attendanceData, students) {
|
||||
if (!container || !Array.isArray(students)) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<form method="post" action="${API_BASE_URL}/save-attendance/" id="${ATTENDANCE_FORM_ID}">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${getCSRFToken()}">
|
||||
<input type="hidden" name="routine_id" value="${routineId}">
|
||||
<input type="hidden" name="date" value="${date}">
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="teacherAttend"
|
||||
name="teacher_attend" ${attendanceData.teacher_attend ? 'checked' : ''}>
|
||||
<label class="form-check-label" for="teacherAttend">Teacher Present</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<select class="form-select" id="classStatus" name="class_status">
|
||||
<option value="True" ${attendanceData.class_status ? 'selected' : ''}>Conducted</option>
|
||||
<option value="False" ${!attendanceData.class_status ? 'selected' : ''}>Not Conducted</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th width="5%">#</th>
|
||||
<th width="40%">Name</th>
|
||||
<th width="40%">Student ID</th>
|
||||
<th width="15%">Present</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="studentList">
|
||||
${students.map((student, index) => {
|
||||
const isPresent = attendanceData.records && attendanceData.records[student.id] === true;
|
||||
return `
|
||||
<tr>
|
||||
<td>${index + 1}</td>
|
||||
<td>${student.name}</td>
|
||||
<td>${student.id}</td>
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="student_${student.id}"
|
||||
name="student_${student.id}"
|
||||
${isPresent ? 'checked' : ''}
|
||||
aria-label="Mark ${student.name} as present">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex justify-content-between">
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-success me-2" onclick="markAllPresent()">
|
||||
<i class="fas fa-check-double me-1"></i> Mark All Present
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="markAllAbsent()">
|
||||
<i class="fas fa-times-circle me-1"></i> Mark All Absent
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i> Save Attendance
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
// Attendance Marking Functions
|
||||
function markAllPresent() {
|
||||
const checkboxes = document.querySelectorAll(`#${ATTENDANCE_FORM_ID} input[type="checkbox"], #${QUICK_ATTENDANCE_FORM_ID} input[type="checkbox"]`);
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (checkbox.id !== 'teacherAttend' && checkbox.id !== 'modalTeacherAttend') {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function markAllAbsent() {
|
||||
const checkboxes = document.querySelectorAll(`#${ATTENDANCE_FORM_ID} input[type="checkbox"], #${QUICK_ATTENDANCE_FORM_ID} input[type="checkbox"]`);
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (checkbox.id !== 'teacherAttend' && checkbox.id !== 'modalTeacherAttend') {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAttendanceData() {
|
||||
try {
|
||||
console.log('Loading attendance data...');
|
||||
const routineId = document.getElementById('routineSelect')?.value;
|
||||
const date = document.getElementById('dateSelect')?.value;
|
||||
const container = document.getElementById('attendanceFormContainer');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
if (!routineId || !date) {
|
||||
container.style.display = 'none';
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
container.style.display = 'block';
|
||||
showLoadingSpinner(container, 'Loading attendance data...');
|
||||
|
||||
const [attendanceData, studentsData] = await Promise.all([
|
||||
apiRequest(`${API_BASE_URL}/get-attendance-form/?routine_id=${routineId}&date=${date}`),
|
||||
apiRequest(`${API_BASE_URL}/get-students/?routine_id=${routineId}`)
|
||||
]);
|
||||
|
||||
if (attendanceData.error || studentsData.error) {
|
||||
throw new Error(attendanceData.error || studentsData.error);
|
||||
}
|
||||
|
||||
renderAttendanceForm(container, routineId, date, attendanceData, studentsData.students);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading attendance data:', error);
|
||||
if (container) {
|
||||
showError(container, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form Submission Handler
|
||||
async function saveQuickAttendance() {
|
||||
try {
|
||||
const form = document.getElementById(QUICK_ATTENDANCE_FORM_ID);
|
||||
if (!form) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
// Validate required fields
|
||||
if (!data.routine_id || !data.date) {
|
||||
throw new Error('Missing required fields');
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
if (submitButton) {
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Saving...';
|
||||
}
|
||||
|
||||
// Submit attendance
|
||||
const response = await apiRequest(`${API_BASE_URL}/save-attendance/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
showSuccess(form, 'Attendance saved successfully');
|
||||
|
||||
// Close modal and reset form
|
||||
setTimeout(() => {
|
||||
closeAttendanceModal();
|
||||
}, 1500);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving attendance:', error);
|
||||
showError(form, error);
|
||||
|
||||
// Reset submit button
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
if (submitButton) {
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = '<i class="fas fa-save me-1"></i> Save Attendance';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Main teacher dashboard entry point - imports all modules
|
||||
|
||||
// Load utility functions first
|
||||
document.write('<script src="/static/js/teacher-utils.js"></script>');
|
||||
|
||||
// Load feature modules
|
||||
document.write('<script src="/static/js/teacher-attendance.js"></script>');
|
||||
document.write('<script src="/static/js/teacher-materials.js"></script>');
|
||||
document.write('<script src="/static/js/teacher-init.js"></script>');
|
||||
@@ -0,0 +1,243 @@
|
||||
// Initialization code for the teacher dashboard
|
||||
|
||||
// Initialize when document is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
try {
|
||||
console.log('Initializing teacher dashboard and attendance functions...');
|
||||
|
||||
// Handle hash navigation with priority
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
// Remove the # and get the section name
|
||||
const sectionId = hash.substring(1);
|
||||
console.log('Found hash navigation target:', sectionId);
|
||||
|
||||
// Make all sections inactive and hidden first
|
||||
document.querySelectorAll('.content-section').forEach(section => {
|
||||
section.classList.remove('active');
|
||||
section.style.display = 'none';
|
||||
});
|
||||
|
||||
// Then activate just the target section
|
||||
const targetSection = document.getElementById(sectionId);
|
||||
if (targetSection) {
|
||||
console.log('Activating section from hash:', sectionId);
|
||||
targetSection.classList.add('active');
|
||||
targetSection.style.display = 'block';
|
||||
|
||||
// Also activate the correct nav item
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
const navSection = item.getAttribute('data-section');
|
||||
if (navSection === sectionId) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// If it's subjects, make sure the subjects nav is active
|
||||
if (sectionId === 'subjectsSection') {
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
const navSection = item.getAttribute('data-section');
|
||||
if (navSection === 'subjects') {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide attendance form container by default
|
||||
const attendanceFormContainer = document.getElementById('attendanceFormContainer');
|
||||
if (attendanceFormContainer) {
|
||||
attendanceFormContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Set today's date as default for date inputs
|
||||
const dateInputs = document.querySelectorAll('input[type="date"]');
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
dateInputs.forEach(input => {
|
||||
// Don't set a default value for the attendance date input
|
||||
if (input.id !== 'dateSelect' && !input.value) {
|
||||
input.value = today;
|
||||
console.log('Set date for input:', input.id);
|
||||
// Trigger any change event listeners
|
||||
const event = new Event('change');
|
||||
input.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listeners for routine selection form
|
||||
const routineSelect = document.getElementById('routineSelect');
|
||||
const dateSelect = document.getElementById('dateSelect');
|
||||
|
||||
if (routineSelect) {
|
||||
// Remove auto-selection of first routine option
|
||||
// We want nothing selected by default
|
||||
|
||||
routineSelect.addEventListener('change', function() {
|
||||
console.log('Routine changed:', this.value);
|
||||
loadAttendanceData();
|
||||
});
|
||||
}
|
||||
|
||||
if (dateSelect) {
|
||||
dateSelect.addEventListener('change', function() {
|
||||
console.log('Date changed:', this.value);
|
||||
loadAttendanceData();
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listeners for edit buttons
|
||||
document.querySelectorAll('.edit-attendance-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const routineId = this.getAttribute('data-routine-id');
|
||||
const date = this.getAttribute('data-date');
|
||||
console.log('Edit button clicked:', { routineId, date });
|
||||
editAttendance(routineId, date);
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners for today's classes attendance buttons
|
||||
document.querySelectorAll('.take-attendance-btn, .attendance-btn, .today-attendance-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
console.log('Today attendance button clicked');
|
||||
const routineId = this.getAttribute('data-routine-id');
|
||||
const date = this.getAttribute('data-date') || today;
|
||||
|
||||
if (routineId) {
|
||||
console.log('Taking attendance for routine:', routineId, 'date:', date);
|
||||
|
||||
// If this is a quick attendance button, open the modal
|
||||
if (this.classList.contains('quick-attendance')) {
|
||||
openAttendanceModal(routineId, date);
|
||||
} else {
|
||||
// Otherwise go to manage attendance view
|
||||
editAttendance(routineId, date);
|
||||
}
|
||||
} else {
|
||||
console.error('Missing routine ID for attendance button');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Load initial data if values are present
|
||||
if (routineSelect && dateSelect && routineSelect.value && dateSelect.value) {
|
||||
loadAttendanceData();
|
||||
}
|
||||
|
||||
// Initialize file upload form submission
|
||||
const uploadForm = document.getElementById('uploadFileForm');
|
||||
if (uploadForm) {
|
||||
uploadForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(uploadForm);
|
||||
const submitBtn = uploadForm.querySelector('button[type="submit"]');
|
||||
const responseDiv = document.getElementById('uploadResponse');
|
||||
|
||||
// Disable the submit button and show loading state
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Uploading...';
|
||||
}
|
||||
|
||||
fetch('/app/manage-subject-files/', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (responseDiv) {
|
||||
responseDiv.style.display = 'block';
|
||||
responseDiv.className = data.success ? 'alert alert-success' : 'alert alert-danger';
|
||||
responseDiv.innerHTML = data.message;
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
// Reload materials after successful upload
|
||||
setTimeout(() => {
|
||||
// Get the subject ID that was selected
|
||||
const subjectId = document.getElementById('subject_id').value;
|
||||
loadSubjectMaterials(subjectId);
|
||||
closeUploadModal();
|
||||
}, 1500);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
if (responseDiv) {
|
||||
responseDiv.style.display = 'block';
|
||||
responseDiv.className = 'alert alert-danger';
|
||||
responseDiv.innerHTML = 'An error occurred while uploading the file. Please try again.';
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-upload me-1"></i> Upload';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize delete confirmation button
|
||||
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
|
||||
if (confirmDeleteBtn) {
|
||||
confirmDeleteBtn.addEventListener('click', function() {
|
||||
if (!fileIdToDelete) {
|
||||
closeConfirmationModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable the button and show loading state
|
||||
confirmDeleteBtn.disabled = true;
|
||||
confirmDeleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Deleting...';
|
||||
|
||||
fetch('/app/delete-subject-file/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
},
|
||||
body: JSON.stringify({ file_id: fileIdToDelete })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
closeConfirmationModal();
|
||||
|
||||
if (data.success) {
|
||||
// Store the current subject ID if displayed in the UI
|
||||
const currentSubjectHeading = document.querySelector('#subjectMaterialsContent .card-header h5');
|
||||
let currentSubjectId = null;
|
||||
|
||||
if (currentSubjectHeading) {
|
||||
// Find the subject ID from the upload button
|
||||
const uploadBtn = currentSubjectHeading.closest('.card-header').querySelector('[data-subject-id]');
|
||||
if (uploadBtn) {
|
||||
currentSubjectId = uploadBtn.getAttribute('data-subject-id');
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the materials section with the same subject if we had one
|
||||
loadSubjectMaterials(currentSubjectId);
|
||||
} else {
|
||||
alert(data.message || 'Error deleting file.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
closeConfirmationModal();
|
||||
alert('An error occurred while deleting the file. Please try again.');
|
||||
})
|
||||
.finally(() => {
|
||||
confirmDeleteBtn.disabled = false;
|
||||
confirmDeleteBtn.innerHTML = '<i class="fas fa-trash me-1"></i> Delete';
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing teacher dashboard:', error);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,941 @@
|
||||
// Subject Materials Section Functions
|
||||
|
||||
function showSection(sectionId, forceOrSubjectId = null) {
|
||||
console.log('Showing section:', sectionId, 'force/subjectId:', forceOrSubjectId);
|
||||
|
||||
// Special handling for subject materials section using the toggle function
|
||||
if (sectionId === 'subjectMaterialsSection') {
|
||||
const subjectId = forceOrSubjectId;
|
||||
toggleSubjectView('materials');
|
||||
|
||||
// Get the section
|
||||
const section = document.getElementById(sectionId);
|
||||
if (section) {
|
||||
// Make sure the header div is visible and properly styled
|
||||
const headerDiv = section.querySelector('.d-flex.justify-content-between.align-items-center');
|
||||
if (headerDiv) {
|
||||
// Ensure header is visible with !important
|
||||
headerDiv.style.cssText = "display: flex !important; opacity: 1; visibility: visible;";
|
||||
|
||||
// Make the heading more prominent
|
||||
const heading = headerDiv.querySelector('h3');
|
||||
if (heading) {
|
||||
heading.style.fontWeight = 'bold';
|
||||
heading.style.color = '#3F51B5';
|
||||
}
|
||||
}
|
||||
|
||||
// Now load the content
|
||||
loadSubjectMaterials(subjectId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for subjects section using the toggle function
|
||||
if (sectionId === 'subjectsSection') {
|
||||
toggleSubjectView('subjects');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the case where we're being called from the back button
|
||||
const forceDisplay = forceOrSubjectId === true;
|
||||
// If not a boolean, treat as subjectId
|
||||
const subjectId = typeof forceOrSubjectId === 'boolean' ? null : forceOrSubjectId;
|
||||
|
||||
// Special handling for forced navigation to subjects
|
||||
if (sectionId === 'subjectsSection' && forceDisplay) {
|
||||
console.log('FORCE NAVIGATION to subjects section');
|
||||
|
||||
// Hide all other sections first
|
||||
document.querySelectorAll('.content-section').forEach(sect => {
|
||||
if (sect.id !== 'subjectsSection') {
|
||||
sect.style.display = 'none';
|
||||
sect.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Activate subjects section with high specificity
|
||||
const subjectsSection = document.getElementById('subjectsSection');
|
||||
if (subjectsSection) {
|
||||
// Force display using multiple methods
|
||||
subjectsSection.style.cssText = 'display: block !important; opacity: 1; visibility: visible;';
|
||||
subjectsSection.classList.add('active');
|
||||
|
||||
// Force a redraw
|
||||
void subjectsSection.offsetHeight;
|
||||
|
||||
// Add active class to subjects nav
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
const section = item.getAttribute('data-section');
|
||||
if (section === 'subjects') {
|
||||
item.classList.add('active');
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update URL hash
|
||||
window.location.hash = 'subjectsSection';
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
|
||||
console.log('Force navigation complete.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for subjects section to correctly set the active nav item
|
||||
if (sectionId === 'subjectsSection') {
|
||||
// Update URL hash to track the section
|
||||
window.location.hash = 'subjectsSection';
|
||||
console.log('Setting hash to subjectsSection');
|
||||
}
|
||||
|
||||
// Debug output - list all sections in DOM
|
||||
const allSections = document.querySelectorAll('.content-section');
|
||||
console.log('Available sections:');
|
||||
allSections.forEach(section => {
|
||||
console.log('- Section ID:', section.id, 'Display:', section.style.display, 'Classes:', section.className);
|
||||
});
|
||||
|
||||
// Hide all sections
|
||||
document.querySelectorAll('.content-section').forEach(section => {
|
||||
section.classList.remove('active');
|
||||
section.style.display = 'none';
|
||||
console.log('Hiding section:', section.id);
|
||||
});
|
||||
|
||||
// First remove active class from all nav items
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// Then add active class to the correct nav item
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
const navSection = item.getAttribute('data-section');
|
||||
console.log('Nav item section:', navSection, 'comparing with:', sectionId);
|
||||
|
||||
if (navSection === sectionId ||
|
||||
(sectionId === 'subjectMaterialsSection' && navSection === 'subjects') ||
|
||||
(sectionId === 'subjectsSection' && navSection === 'subjects')) {
|
||||
item.classList.add('active');
|
||||
console.log('Activated nav item:', navSection);
|
||||
}
|
||||
});
|
||||
|
||||
// Show the selected section
|
||||
const selectedSection = document.getElementById(sectionId);
|
||||
if (selectedSection) {
|
||||
console.log('Found section to show:', sectionId);
|
||||
selectedSection.classList.add('active');
|
||||
selectedSection.style.display = 'block';
|
||||
|
||||
// Force browser to recognize the display change
|
||||
setTimeout(() => {
|
||||
selectedSection.style.opacity = '0.99';
|
||||
setTimeout(() => {
|
||||
selectedSection.style.opacity = '1';
|
||||
}, 10);
|
||||
}, 10);
|
||||
} else {
|
||||
console.error('Could not find section with ID:', sectionId);
|
||||
}
|
||||
|
||||
// Special handling for subjects section to ensure it's visible
|
||||
if (sectionId === 'subjectsSection') {
|
||||
const subjectsSection = document.getElementById('subjectsSection');
|
||||
if (subjectsSection) {
|
||||
console.log('Forcing subjects section to display');
|
||||
subjectsSection.style.display = 'block';
|
||||
subjectsSection.classList.add('active');
|
||||
|
||||
// The following forces a reflow
|
||||
void subjectsSection.offsetWidth;
|
||||
} else {
|
||||
console.error('Subjects section element not found in DOM');
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
// Custom API function to get subject materials directly
|
||||
function getSubjectMaterials(subjectId = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Get the CSRF token
|
||||
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
// Build the URL for the API endpoint
|
||||
let url = '/app/get-teacher-subjects/';
|
||||
|
||||
console.log('Fetching teacher subjects from:', url);
|
||||
|
||||
// First fetch the subjects
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'X-CSRFToken': csrftoken
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(subjectsData => {
|
||||
console.log('Teacher subjects data:', subjectsData);
|
||||
|
||||
if (!subjectsData.success || !subjectsData.subjects || subjectsData.subjects.length === 0) {
|
||||
console.log('No subjects found or empty response');
|
||||
resolve({ subjects: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
let subjects = subjectsData.subjects;
|
||||
|
||||
// If a specific subject is requested, filter the subjects
|
||||
if (subjectId) {
|
||||
console.log('Filtering subjects by ID:', subjectId);
|
||||
subjects = subjects.filter(subject => subject.id.toString() === subjectId.toString());
|
||||
|
||||
if (subjects.length === 0) {
|
||||
console.log('No matching subject found for ID:', subjectId);
|
||||
resolve({ subjects: [] });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Now we need to get the files for each subject
|
||||
// We'll use the manage_subject_files endpoint with a GET request
|
||||
// and parse the HTML response to extract the files
|
||||
|
||||
// Create a result object to store the subjects with their files
|
||||
const result = {
|
||||
subjects: []
|
||||
};
|
||||
|
||||
// Process each subject to get its files
|
||||
const promises = subjects.map(subject => {
|
||||
const filesUrl = `/app/manage-subject-files/?subject=${subject.id}`;
|
||||
console.log(`Fetching files for subject ${subject.name} (ID: ${subject.id}) from: ${filesUrl}`);
|
||||
|
||||
return fetch(filesUrl, {
|
||||
headers: {
|
||||
'X-CSRFToken': csrftoken
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
// Parse the HTML to extract the files
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
console.log(`Parsing HTML response for subject ${subject.name}`);
|
||||
|
||||
// Try different ways to find the subject card
|
||||
let subjectCard = null;
|
||||
let cardBody = null;
|
||||
|
||||
// Method 1: Find by exact heading text
|
||||
const headings = Array.from(doc.querySelectorAll('.card-header h5, .card-title'));
|
||||
subjectCard = headings.find(h5 => h5.textContent.trim() === subject.name);
|
||||
|
||||
if (subjectCard) {
|
||||
console.log(`Found subject card for ${subject.name} using heading match`);
|
||||
cardBody = subjectCard.closest('.card')?.querySelector('.card-body');
|
||||
} else {
|
||||
// Method 2: Find by card with subject ID in attributes
|
||||
const cards = doc.querySelectorAll('.card[data-subject-id="' + subject.id + '"], .card[id*="subject-' + subject.id + '"]');
|
||||
if (cards.length > 0) {
|
||||
console.log(`Found subject card for ${subject.name} using ID attribute`);
|
||||
subjectCard = cards[0];
|
||||
cardBody = subjectCard.querySelector('.card-body');
|
||||
} else {
|
||||
// Method 3: Just take the first card if there's only one subject requested
|
||||
if (subjectId) {
|
||||
const allCards = doc.querySelectorAll('.card');
|
||||
if (allCards.length > 0) {
|
||||
console.log(`Using first card for ${subject.name} as fallback`);
|
||||
subjectCard = allCards[0];
|
||||
cardBody = subjectCard.querySelector('.card-body');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const files = [];
|
||||
let hasSyllabus = false;
|
||||
|
||||
// If we found a card body with a table, extract the files
|
||||
if (cardBody) {
|
||||
// Look for table rows
|
||||
const fileRows = cardBody.querySelectorAll('table tbody tr');
|
||||
console.log(`Found ${fileRows.length} file rows for subject ${subject.name}`);
|
||||
|
||||
fileRows.forEach(row => {
|
||||
const columns = row.querySelectorAll('td');
|
||||
if (columns.length >= 3) { // Need at least 3 columns
|
||||
const title = columns[0].textContent.trim();
|
||||
const description = columns.length > 1 ? columns[1].textContent.trim() : '';
|
||||
const date = columns.length > 2 ? columns[2].textContent.trim() : '';
|
||||
|
||||
// Extract file ID from the delete button
|
||||
const deleteBtn = row.querySelector('button[onclick*="deleteFile"], button[data-file-id]');
|
||||
let fileId = null;
|
||||
|
||||
if (deleteBtn) {
|
||||
// Try onclick attribute
|
||||
const onClickAttr = deleteBtn.getAttribute('onclick');
|
||||
if (onClickAttr) {
|
||||
const match = onClickAttr.match(/deleteFile\(['"]([^'"]*)['"]\)/);
|
||||
if (match && match[1]) {
|
||||
fileId = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Try data attribute if onclick failed
|
||||
if (!fileId) {
|
||||
fileId = deleteBtn.getAttribute('data-file-id');
|
||||
}
|
||||
}
|
||||
|
||||
// Extract file URL from the download link
|
||||
const actionCell = columns[columns.length - 1]; // Last column usually has actions
|
||||
const downloadLink = actionCell.querySelector('a[href]:not([href="#"])');
|
||||
let fileUrl = null;
|
||||
|
||||
if (downloadLink) {
|
||||
fileUrl = downloadLink.getAttribute('href');
|
||||
}
|
||||
|
||||
// Check if this is a syllabus file based on title or other indicators
|
||||
const isSyllabus = title.toLowerCase().includes('syllabus');
|
||||
|
||||
// Set the syllabus flag for the subject if this file is a syllabus
|
||||
if (isSyllabus) {
|
||||
hasSyllabus = true;
|
||||
}
|
||||
|
||||
files.push({
|
||||
id: fileId,
|
||||
title: title,
|
||||
description: description,
|
||||
uploaded_on: date,
|
||||
file_url: fileUrl,
|
||||
file_type: isSyllabus ? 'syllabus' : 'material'
|
||||
});
|
||||
|
||||
console.log(`Added file: ${title} for subject ${subject.name}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(`Could not find card body for subject ${subject.name}`);
|
||||
}
|
||||
|
||||
// Check if the subject has a syllabus PDF using the View Syllabus button existence
|
||||
const syllabusBtn = doc.querySelector(`a[href*="/subject/${subject.id}/syllabus/"], button[onclick*="viewSyllabus('${subject.id}')"]`);
|
||||
if (syllabusBtn) {
|
||||
console.log(`Found syllabus button for subject ${subject.name}`);
|
||||
hasSyllabus = true;
|
||||
}
|
||||
|
||||
// Add this subject with its files to the result
|
||||
result.subjects.push({
|
||||
...subject,
|
||||
materials: files,
|
||||
has_syllabus: hasSyllabus
|
||||
});
|
||||
|
||||
console.log(`Finished processing subject ${subject.name}, found ${files.length} files`);
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for all promises to resolve
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
console.log('Final result:', result);
|
||||
resolve(result);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error processing subjects:', error);
|
||||
reject(error);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching subjects:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadSubjectMaterials(subjectId = null) {
|
||||
const contentDiv = document.getElementById('subjectMaterialsContent');
|
||||
if (!contentDiv) {
|
||||
console.error('Subject materials content container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state in content area only
|
||||
contentDiv.innerHTML = `
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading subject materials...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
console.log('==== LOADING SUBJECT MATERIALS ====');
|
||||
console.log('Subject ID:', subjectId);
|
||||
|
||||
// Build the URL with optional subject filter
|
||||
let url = '/app/manage-subject-files/';
|
||||
if (subjectId) {
|
||||
url += `?subject=${subjectId}`;
|
||||
}
|
||||
|
||||
console.log('Fetching from URL:', url);
|
||||
|
||||
// Create a direct link to the materials as a backup
|
||||
const directLink = document.createElement('div');
|
||||
directLink.className = 'text-center mt-3';
|
||||
directLink.innerHTML = `
|
||||
<p>If materials don't load automatically, try:</p>
|
||||
<a href="${url}" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i> Open Subject Materials in New Tab
|
||||
</a>
|
||||
`;
|
||||
|
||||
// Fetch the materials page directly
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'text/html',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response type:', response.headers.get('content-type'));
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
console.log('Received HTML content of length:', html.length);
|
||||
|
||||
if (!html || html.trim().length === 0) {
|
||||
console.error('Empty response received');
|
||||
contentDiv.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<strong>Empty response received from server</strong>
|
||||
</div>
|
||||
`;
|
||||
contentDiv.appendChild(directLink);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a temporary div to hold the HTML content
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
console.log('HTML parsed, searching for content...');
|
||||
|
||||
// Clear the content div
|
||||
contentDiv.innerHTML = '';
|
||||
|
||||
// Add back button at the top of the content for mobile
|
||||
const mobileBackNav = document.createElement('div');
|
||||
mobileBackNav.className = 'd-block d-md-none text-center mb-3';
|
||||
mobileBackNav.innerHTML = `
|
||||
<button class="btn btn-secondary w-100" onclick="showSection('subjectsSection'); return false;">
|
||||
<i class="fas fa-arrow-left me-2"></i> Back to Subjects
|
||||
</button>
|
||||
`;
|
||||
contentDiv.appendChild(mobileBackNav);
|
||||
|
||||
// Look for subjects and display them, trying multiple selectors
|
||||
let contentFound = false;
|
||||
|
||||
// First approach: find the row of cards
|
||||
const subjectsRow = tempDiv.querySelector('.content-section .row, .content-card .row, .row');
|
||||
if (subjectsRow) {
|
||||
console.log('Found subjects row with', subjectsRow.children.length, 'children');
|
||||
contentDiv.innerHTML = '';
|
||||
const newRow = document.createElement('div');
|
||||
newRow.className = 'row';
|
||||
newRow.style.display = 'flex';
|
||||
newRow.style.flexWrap = 'wrap';
|
||||
newRow.style.margin = '0 -12px'; // Negative margin to offset the padding
|
||||
|
||||
// Copy each card individually to avoid any parent context issues
|
||||
Array.from(subjectsRow.querySelectorAll('.col-md-6, .col-lg-4, .col')).forEach(col => {
|
||||
newRow.appendChild(col.cloneNode(true));
|
||||
});
|
||||
|
||||
if (newRow.children.length > 0) {
|
||||
contentDiv.appendChild(newRow);
|
||||
contentFound = true;
|
||||
console.log('Added', newRow.children.length, 'subject cards to the content');
|
||||
}
|
||||
}
|
||||
|
||||
// Second approach: find individual cards if no row found
|
||||
if (!contentFound) {
|
||||
const cards = tempDiv.querySelectorAll('.card');
|
||||
if (cards && cards.length > 0) {
|
||||
console.log('Found', cards.length, 'individual cards');
|
||||
contentDiv.innerHTML = '';
|
||||
const newRow = document.createElement('div');
|
||||
newRow.className = 'row';
|
||||
newRow.style.display = 'flex';
|
||||
newRow.style.flexWrap = 'wrap';
|
||||
newRow.style.margin = '0 -12px'; // Negative margin to offset the padding
|
||||
|
||||
cards.forEach(card => {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-md-6 col-lg-4 mb-4';
|
||||
// Add explicit styling to ensure visibility
|
||||
col.style.display = 'block';
|
||||
col.style.padding = '0 12px';
|
||||
col.style.marginBottom = '24px';
|
||||
col.style.width = '33.333%';
|
||||
col.style.minWidth = '300px';
|
||||
|
||||
// Clone the card and add explicit styles
|
||||
const cardClone = card.cloneNode(true);
|
||||
cardClone.style.display = 'flex';
|
||||
cardClone.style.flexDirection = 'column';
|
||||
cardClone.style.height = '100%';
|
||||
cardClone.style.border = '1px solid rgba(0,0,0,0.125)';
|
||||
cardClone.style.borderRadius = '0.25rem';
|
||||
cardClone.style.overflow = 'hidden';
|
||||
cardClone.style.backgroundColor = '#fff';
|
||||
|
||||
col.appendChild(cardClone);
|
||||
newRow.appendChild(col);
|
||||
});
|
||||
|
||||
// Clear and append with explicit styles
|
||||
contentDiv.style.display = 'block';
|
||||
contentDiv.style.width = '100%';
|
||||
contentDiv.style.visibility = 'visible';
|
||||
contentDiv.appendChild(newRow);
|
||||
contentFound = true;
|
||||
console.log('Added cards to content as individual items with explicit styles');
|
||||
|
||||
// Force redraw
|
||||
setTimeout(() => {
|
||||
contentDiv.style.opacity = '0.99';
|
||||
setTimeout(() => {
|
||||
contentDiv.style.opacity = '1';
|
||||
}, 50);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
// Third approach: try to find any content card
|
||||
if (!contentFound) {
|
||||
const contentCard = tempDiv.querySelector('.content-card');
|
||||
if (contentCard) {
|
||||
console.log('Found content card, using its content');
|
||||
// Remove header if present to avoid duplicate headers
|
||||
const header = contentCard.querySelector('.d-flex.justify-content-between.align-items-center');
|
||||
if (header) {
|
||||
console.log('Removing header from content card');
|
||||
header.remove();
|
||||
}
|
||||
|
||||
contentDiv.innerHTML = contentCard.innerHTML;
|
||||
contentFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fourth approach: direct render of subjects
|
||||
if (!contentFound) {
|
||||
console.log('No content found in HTML, trying to manually render subjects');
|
||||
// Directly call the API to get subjects
|
||||
fetch('/app/get-teacher-subjects/')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.subjects && data.subjects.length > 0) {
|
||||
console.log('Got', data.subjects.length, 'subjects from API');
|
||||
|
||||
// Filter by subject ID if provided
|
||||
let subjects = data.subjects;
|
||||
if (subjectId) {
|
||||
subjects = subjects.filter(s => s.id == subjectId);
|
||||
}
|
||||
|
||||
if (subjects.length === 0) {
|
||||
contentDiv.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
No subjects found matching your criteria.
|
||||
</div>
|
||||
`;
|
||||
contentDiv.appendChild(directLink);
|
||||
return;
|
||||
}
|
||||
|
||||
// Manually render subjects
|
||||
contentDiv.innerHTML = '';
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
row.style.display = 'flex';
|
||||
row.style.flexWrap = 'wrap';
|
||||
row.style.margin = '0 -12px';
|
||||
|
||||
subjects.forEach(subject => {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-md-6 col-lg-4 mb-4';
|
||||
col.style.display = 'block';
|
||||
col.style.padding = '0 12px';
|
||||
col.style.marginBottom = '24px';
|
||||
col.style.width = '33.333%';
|
||||
col.style.minWidth = '300px';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card border-0 shadow-sm h-100';
|
||||
card.style.display = 'flex';
|
||||
card.style.flexDirection = 'column';
|
||||
card.style.height = '100%';
|
||||
card.style.border = '1px solid rgba(0,0,0,0.125)';
|
||||
card.style.borderRadius = '0.25rem';
|
||||
card.style.overflow = 'hidden';
|
||||
card.style.backgroundColor = '#fff';
|
||||
|
||||
const cardHeader = document.createElement('div');
|
||||
cardHeader.className = 'card-header bg-light py-3';
|
||||
cardHeader.style.backgroundColor = '#f8f9fa';
|
||||
cardHeader.style.padding = '1rem';
|
||||
|
||||
const title = document.createElement('h5');
|
||||
title.className = 'card-title mb-0';
|
||||
title.textContent = subject.name;
|
||||
|
||||
const subtitle = document.createElement('p');
|
||||
subtitle.className = 'text-muted mb-0 small';
|
||||
subtitle.textContent = `${subject.course__name || ''} - ${subject.semester_or_year || 'N/A'}`;
|
||||
|
||||
cardHeader.appendChild(title);
|
||||
cardHeader.appendChild(subtitle);
|
||||
|
||||
const cardBody = document.createElement('div');
|
||||
cardBody.className = 'card-body';
|
||||
cardBody.style.padding = '1rem';
|
||||
cardBody.style.flexGrow = '1';
|
||||
|
||||
const fileStatus = document.createElement('p');
|
||||
fileStatus.className = 'text-muted small mb-3';
|
||||
fileStatus.innerHTML = '<i class="fas fa-file me-1"></i> Loading files...';
|
||||
|
||||
const btnContainer = document.createElement('div');
|
||||
btnContainer.className = 'mb-3';
|
||||
|
||||
const uploadBtn = document.createElement('button');
|
||||
uploadBtn.className = 'btn btn-primary w-100';
|
||||
uploadBtn.innerHTML = `<i class="fas fa-file-upload me-1"></i> Upload to ${subject.name}`;
|
||||
uploadBtn.onclick = function() { openUploadModal(subject.id); };
|
||||
|
||||
btnContainer.appendChild(uploadBtn);
|
||||
cardBody.appendChild(fileStatus);
|
||||
cardBody.appendChild(btnContainer);
|
||||
|
||||
card.appendChild(cardHeader);
|
||||
card.appendChild(cardBody);
|
||||
col.appendChild(card);
|
||||
row.appendChild(col);
|
||||
});
|
||||
|
||||
contentDiv.appendChild(row);
|
||||
contentFound = true;
|
||||
} else {
|
||||
console.log('No subjects returned from API');
|
||||
contentDiv.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
No subjects assigned to you. Please contact administrator.
|
||||
</div>
|
||||
`;
|
||||
contentDiv.appendChild(directLink);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching subjects:', error);
|
||||
contentDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
Error fetching subjects: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
contentDiv.appendChild(directLink);
|
||||
});
|
||||
}
|
||||
|
||||
// If all else fails, show a simple message
|
||||
if (!contentFound) {
|
||||
console.log('All content extraction methods failed');
|
||||
contentDiv.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<span>Unable to display subject materials automatically.</span>
|
||||
</div>
|
||||
`;
|
||||
contentDiv.appendChild(directLink);
|
||||
}
|
||||
|
||||
// Re-attach event handlers for the buttons after a short delay
|
||||
setTimeout(() => {
|
||||
console.log('Attaching event handlers to buttons');
|
||||
// Add event listeners for upload buttons
|
||||
contentDiv.querySelectorAll('[onclick*="openUploadModal"]').forEach(button => {
|
||||
const match = button.getAttribute('onclick')?.match(/openUploadModal\(['"]([^'"]*)['"]\)/);
|
||||
if (match) {
|
||||
const id = match[1];
|
||||
console.log('Found upload button for subject ID:', id);
|
||||
button.setAttribute('onclick', `openUploadModal('${id}')`);
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listeners for delete buttons
|
||||
contentDiv.querySelectorAll('[onclick*="deleteFile"]').forEach(button => {
|
||||
const match = button.getAttribute('onclick')?.match(/deleteFile\(['"]([^'"]*)['"]\)/);
|
||||
if (match) {
|
||||
const id = match[1];
|
||||
console.log('Found delete button for file ID:', id);
|
||||
button.setAttribute('onclick', `deleteFile('${id}')`);
|
||||
}
|
||||
});
|
||||
|
||||
// Add upload button if none exists
|
||||
if (!contentDiv.querySelector('[onclick*="openUploadModal"]')) {
|
||||
console.log('No upload buttons found, adding one');
|
||||
const uploadBtn = document.createElement('div');
|
||||
uploadBtn.className = 'text-center mt-4';
|
||||
uploadBtn.innerHTML = `
|
||||
<button class="btn btn-primary" onclick="openUploadModal()">
|
||||
<i class="fas fa-file-upload me-1"></i> Upload New Material
|
||||
</button>
|
||||
`;
|
||||
contentDiv.appendChild(uploadBtn);
|
||||
}
|
||||
}, 200);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading subject materials:', error);
|
||||
contentDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
<strong>Error loading subject materials: ${error.message}</strong>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<button class="btn btn-primary" onclick="openUploadModal()">
|
||||
<i class="fas fa-file-upload me-1"></i> Upload New Material
|
||||
</button>
|
||||
<a href="${url}" class="btn btn-outline-secondary ms-2" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i> Open in New Tab
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function openUploadModal(subjectId = '') {
|
||||
console.log('Opening upload modal with subject ID:', subjectId);
|
||||
const modal = document.getElementById('uploadFileModal');
|
||||
const subjectSelect = document.getElementById('subject_id');
|
||||
const responseDiv = document.getElementById('uploadResponse');
|
||||
|
||||
if (!modal || !subjectSelect) {
|
||||
console.error('Modal or subject select not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the form
|
||||
const uploadForm = document.getElementById('uploadFileForm');
|
||||
if (uploadForm) {
|
||||
uploadForm.reset();
|
||||
}
|
||||
|
||||
if (responseDiv) {
|
||||
responseDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// If a subject ID was provided, pre-select it
|
||||
if (subjectId && subjectSelect) {
|
||||
subjectSelect.value = subjectId;
|
||||
}
|
||||
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeUploadModal() {
|
||||
const modal = document.getElementById('uploadFileModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
function deleteFile(fileId) {
|
||||
fileIdToDelete = fileId;
|
||||
const modal = document.getElementById('confirmationModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
function closeConfirmationModal() {
|
||||
const modal = document.getElementById('confirmationModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
fileIdToDelete = null;
|
||||
}
|
||||
|
||||
// Function to view subject syllabus
|
||||
function viewSyllabus(subjectId) {
|
||||
if (!subjectId) return;
|
||||
|
||||
// Get the button element that was clicked
|
||||
const syllabusButton = document.querySelector(`.syllabus-button[data-subject-id="${subjectId}"]`);
|
||||
|
||||
// Check if button was hidden - if so, there's no syllabus available
|
||||
if (syllabusButton && syllabusButton.style.display === 'none') {
|
||||
alert('No syllabus available for this subject.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
const loadingModal = document.createElement('div');
|
||||
loadingModal.style.position = 'fixed';
|
||||
loadingModal.style.top = '0';
|
||||
loadingModal.style.left = '0';
|
||||
loadingModal.style.width = '100%';
|
||||
loadingModal.style.height = '100%';
|
||||
loadingModal.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
||||
loadingModal.style.display = 'flex';
|
||||
loadingModal.style.justifyContent = 'center';
|
||||
loadingModal.style.alignItems = 'center';
|
||||
loadingModal.style.zIndex = '10000';
|
||||
|
||||
loadingModal.innerHTML = `
|
||||
<div class="bg-white p-4 rounded shadow-sm text-center">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mb-0">Loading syllabus...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(loadingModal);
|
||||
|
||||
// Get the CSRF token
|
||||
const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
// Fetch the syllabus URL
|
||||
fetch(`/app/subject/${subjectId}/syllabus/`, {
|
||||
headers: {
|
||||
'X-CSRFToken': csrftoken
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
// If response is not OK (e.g., 404), throw an error with status
|
||||
if (response.status === 404) {
|
||||
throw new Error('Syllabus not found');
|
||||
}
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Remove loading indicator
|
||||
document.body.removeChild(loadingModal);
|
||||
|
||||
if (data.success) {
|
||||
// Open the syllabus in a new tab
|
||||
window.open(data.file_url, '_blank');
|
||||
} else {
|
||||
// Show error message
|
||||
alert(data.message || 'No syllabus available for this subject.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching syllabus:', error);
|
||||
document.body.removeChild(loadingModal);
|
||||
|
||||
// Show a more user-friendly message
|
||||
if (error.message === 'Syllabus not found') {
|
||||
alert('No syllabus available for this subject.');
|
||||
|
||||
// Hide the button to prevent future attempts
|
||||
if (syllabusButton) {
|
||||
syllabusButton.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
alert('Error fetching syllabus. Please try again.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to toggle between subjects list and subject materials
|
||||
function toggleSubjectView(view) {
|
||||
const subjectsSection = document.getElementById('subjectsSection');
|
||||
const materialsSection = document.getElementById('subjectMaterialsSection');
|
||||
|
||||
console.log('Toggling subject view to:', view);
|
||||
|
||||
if (view === 'subjects') {
|
||||
// Show subjects section
|
||||
subjectsSection.style.display = 'block';
|
||||
subjectsSection.classList.add('active');
|
||||
|
||||
// Hide materials section
|
||||
materialsSection.style.display = 'none';
|
||||
materialsSection.classList.remove('active');
|
||||
|
||||
// Activate the correct nav item
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
const navSection = item.getAttribute('data-section');
|
||||
if (navSection === 'subjects') {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Switched to subjects view');
|
||||
} else if (view === 'materials') {
|
||||
// Show materials section
|
||||
materialsSection.style.display = 'block';
|
||||
materialsSection.classList.add('active');
|
||||
|
||||
// Hide subjects section
|
||||
subjectsSection.style.display = 'none';
|
||||
subjectsSection.classList.remove('active');
|
||||
|
||||
// Keep the subjects nav item active
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
const navSection = item.getAttribute('data-section');
|
||||
if (navSection === 'subjects') {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Switched to materials view');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// Utility functions for the teacher dashboard
|
||||
|
||||
// Global variables and constants
|
||||
let fileIdToDelete = null;
|
||||
const API_BASE_URL = '/app';
|
||||
const DEFAULT_ERROR_MESSAGE = 'An error occurred. Please try again.';
|
||||
|
||||
// Type checking utilities
|
||||
const isString = (value) => typeof value === 'string';
|
||||
const isObject = (value) => value !== null && typeof value === 'object';
|
||||
const isEmpty = (value) => value === null || value === undefined || value === '';
|
||||
|
||||
// CSRF token management
|
||||
function getCSRFToken() {
|
||||
try {
|
||||
const cookies = document.cookie.split(';');
|
||||
const csrfCookie = cookies.find(cookie => cookie.trim().startsWith('csrftoken='));
|
||||
return csrfCookie ? decodeURIComponent(csrfCookie.split('=')[1]) : null;
|
||||
} catch (error) {
|
||||
console.error('Error getting CSRF token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// API request helper with better error handling and retries
|
||||
async function apiRequest(url, options = {}, retries = 3) {
|
||||
const defaultOptions = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCSRFToken(),
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const response = await fetch(url, { ...defaultOptions, ...options });
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
if (attempt === retries) {
|
||||
console.error(`API Request Error (attempt ${attempt}/${retries}):`, error);
|
||||
throw error;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI Helpers with enhanced styling and accessibility
|
||||
function showLoadingSpinner(container, message = 'Loading...') {
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-4" role="status" aria-live="polite">
|
||||
<div class="spinner-border text-primary" role="presentation">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2 text-muted">${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function showError(container, error, withRetryButton = false) {
|
||||
if (!container) return;
|
||||
|
||||
const errorMessage = error?.message || DEFAULT_ERROR_MESSAGE;
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
<strong>${errorMessage}</strong>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
${withRetryButton ? `
|
||||
<div class="text-center mt-3">
|
||||
<button class="btn btn-primary" onclick="window.location.reload()">
|
||||
<i class="fas fa-sync me-1"></i> Retry
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function showSuccess(container, message, autoHide = true) {
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (autoHide) {
|
||||
setTimeout(() => {
|
||||
const alert = container.querySelector('.alert');
|
||||
if (alert) {
|
||||
alert.classList.remove('show');
|
||||
setTimeout(() => alert.remove(), 150);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Helpers with enhanced functionality
|
||||
function showModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (!modal) return;
|
||||
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Focus trap for accessibility
|
||||
const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusableElements.length > 0) {
|
||||
focusableElements[0].focus();
|
||||
}
|
||||
}
|
||||
|
||||
function hideModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (!modal) return;
|
||||
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Form Helpers with validation
|
||||
function resetForm(formOrId) {
|
||||
let form;
|
||||
|
||||
// Handle both form element and form ID
|
||||
if (typeof formOrId === 'string') {
|
||||
form = document.getElementById(formOrId);
|
||||
} else if (formOrId instanceof HTMLFormElement) {
|
||||
form = formOrId;
|
||||
} else {
|
||||
console.warn('Invalid form reference:', formOrId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!form) {
|
||||
console.warn('Form not found:', formOrId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset form values
|
||||
form.reset();
|
||||
|
||||
// Clear all alerts and validation states
|
||||
form.querySelectorAll('.alert, .is-invalid, .is-valid').forEach(el => {
|
||||
el.classList.remove('alert', 'is-invalid', 'is-valid');
|
||||
});
|
||||
|
||||
// Reset all checkboxes to unchecked state
|
||||
form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
// Reset all select elements to first option
|
||||
form.querySelectorAll('select').forEach(select => {
|
||||
if (select.options.length > 0) {
|
||||
select.selectedIndex = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Date Helpers with enhanced formatting options
|
||||
function formatDate(date, format = 'YYYY-MM-DD') {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day);
|
||||
}
|
||||
|
||||
function getCurrentDate() {
|
||||
return formatDate(new Date());
|
||||
}
|
||||
|
||||
// Validation Helpers
|
||||
function validateEmail(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
function validatePhone(phone) {
|
||||
const re = /^\+?[\d\s-]{10,}$/;
|
||||
return re.test(phone);
|
||||
}
|
||||
|
||||
// Export all functions
|
||||
window.getCSRFToken = getCSRFToken;
|
||||
window.apiRequest = apiRequest;
|
||||
window.showLoadingSpinner = showLoadingSpinner;
|
||||
window.showError = showError;
|
||||
window.showSuccess = showSuccess;
|
||||
window.showModal = showModal;
|
||||
window.hideModal = hideModal;
|
||||
window.resetForm = resetForm;
|
||||
window.formatDate = formatDate;
|
||||
window.getCurrentDate = getCurrentDate;
|
||||
window.validateEmail = validateEmail;
|
||||
window.validatePhone = validatePhone;
|
||||
Reference in new issue
Block a user