huge updates

This commit is contained in:
sankalp committed 2025-04-03 16:30:28 +05:45
1 parent 2c62f212cf
commit e729cd615c
57 files changed
+10143 -4185

No files matched your search

+25
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
-113
View File
@@ -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';
}
});
});
});
});
+13
View File
@@ -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";
+160
View File
@@ -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);
});
});
}
});
+25
View File
@@ -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;
+88
View File
@@ -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();
});
+29
View File
@@ -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;
+163
View File
@@ -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;
+169
View File
@@ -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();
}
}
});
});
+62
View File
@@ -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;
+379
View File
@@ -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';
}
}
}
+9
View File
@@ -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>');
+243
View File
@@ -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);
}
});
+941
View File
@@ -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');
}
}
+217
View File
@@ -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;