huge updates
This commit is contained in:
82 files changed
+14911
-4509
No files matched your search
@@ -574,4 +574,101 @@ body {
|
||||
a[href]:after {
|
||||
content: " (" attr(href) ")";
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Form Styles */
|
||||
@media (max-width: 768px) {
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
font-size: 16px; /* Prevents iOS zoom */
|
||||
height: auto;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.input-group > .form-control,
|
||||
.input-group > .form-select {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.form-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-row > .col,
|
||||
.form-row > [class*="col-"] {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.form-control, .form-select {
|
||||
font-size: 16px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Responsive Fixes */
|
||||
@media (max-width: 576px) {
|
||||
.modal-header {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 0.75rem 1rem;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.modal-footer > * {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
/* Fix for iOS modals */
|
||||
.ios-modal-fix {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix for notched phones */
|
||||
@supports (padding: max(0px)) {
|
||||
.fixed-bottom {
|
||||
padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,3 @@ img[src*="/static/img/logo.png"] {
|
||||
margin-right: .5rem;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
@@ -1152,4 +1152,106 @@ img.img-fluid {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Meeting Section Styles */
|
||||
.meeting-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: var(--transition);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meeting-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--hover-shadow);
|
||||
}
|
||||
|
||||
.meeting-card .card-header {
|
||||
background-color: var(--hover-bg);
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.meeting-card .card-header h5 {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.meeting-card .card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.meeting-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meeting-status.scheduled {
|
||||
background-color: var(--primary-light);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.meeting-status.completed {
|
||||
background-color: var(--success-light);
|
||||
color: var(--success-dark);
|
||||
}
|
||||
|
||||
.meeting-status.cancelled {
|
||||
background-color: var(--danger-light);
|
||||
color: var(--danger-dark);
|
||||
}
|
||||
|
||||
.meeting-status.rescheduled {
|
||||
background-color: var(--warning-light);
|
||||
color: var(--warning-dark);
|
||||
}
|
||||
|
||||
.meeting-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.meeting-actions .btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.meeting-actions .btn i {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* Responsive Meeting Section */
|
||||
@media (max-width: 768px) {
|
||||
.meeting-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.meeting-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meeting-actions .btn {
|
||||
flex: 1 1 calc(50% - 0.5rem);
|
||||
min-width: calc(50% - 0.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.meeting-actions .btn {
|
||||
flex: 1 1 100%;
|
||||
min-width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +1,143 @@
|
||||
importScripts(
|
||||
"https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js",
|
||||
);
|
||||
importScripts(
|
||||
"https://www.gstatic.com/firebasejs/9.22.0/firebase-messaging-compat.js",
|
||||
);
|
||||
// Version identifier for cache busting
|
||||
const SW_VERSION = '1.0.2';
|
||||
console.log(`Firebase Messaging SW Version ${SW_VERSION} starting...`);
|
||||
|
||||
firebase.initializeApp({
|
||||
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",
|
||||
});
|
||||
// Import Firebase scripts
|
||||
try {
|
||||
importScripts('https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js');
|
||||
importScripts('https://www.gstatic.com/firebasejs/9.22.0/firebase-messaging-compat.js');
|
||||
console.log('Firebase scripts loaded successfully');
|
||||
} catch (e) {
|
||||
console.error('Error loading Firebase scripts:', e);
|
||||
}
|
||||
|
||||
const messaging = firebase.messaging();
|
||||
// Firebase configuration - this must match exactly with your main app 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"
|
||||
};
|
||||
|
||||
// Handle background messages
|
||||
messaging.onBackgroundMessage((payload) => {
|
||||
console.log("Received background message:", payload);
|
||||
// Handle any errors during initialization
|
||||
try {
|
||||
// Initialize Firebase
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
console.log('Firebase initialized in service worker');
|
||||
|
||||
const messaging = firebase.messaging();
|
||||
console.log('Firebase messaging initialized');
|
||||
|
||||
// Default notification options
|
||||
const notificationOptions = {
|
||||
body: "You have a new message.",
|
||||
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",
|
||||
},
|
||||
],
|
||||
};
|
||||
// Handle background messages
|
||||
messaging.onBackgroundMessage(function(payload) {
|
||||
console.log('Received background message:', payload);
|
||||
|
||||
// Generate a unique ID for this notification
|
||||
const notificationId = payload.data?.notificationId || `notification-${Date.now()}`;
|
||||
|
||||
// Check if we already have a notification with this ID
|
||||
return self.registration.getNotifications({tag: notificationId})
|
||||
.then(function(notifications) {
|
||||
// Only show if no matching notification exists
|
||||
if (notifications.length === 0) {
|
||||
const notificationTitle = payload.notification?.title || 'New Notification';
|
||||
const notificationOptions = {
|
||||
body: payload.notification?.body || 'You have a new message.',
|
||||
icon: '/static/img/logo.png',
|
||||
badge: '/static/img/logo.png',
|
||||
tag: notificationId, // Tag helps prevent duplicates
|
||||
data: payload.data || {}
|
||||
};
|
||||
|
||||
// Set notification title and body based on payload
|
||||
let notificationTitle = "New Notification";
|
||||
if (payload.notification) {
|
||||
notificationTitle = payload.notification.title || notificationTitle;
|
||||
notificationOptions.body =
|
||||
payload.notification.body || notificationOptions.body;
|
||||
if (payload.notification.icon) {
|
||||
notificationOptions.icon = payload.notification.icon;
|
||||
}
|
||||
} else if (payload.data) {
|
||||
notificationTitle = payload.data.title || notificationTitle;
|
||||
notificationOptions.body = payload.data.body || notificationOptions.body;
|
||||
}
|
||||
|
||||
// Show the notification
|
||||
return self.registration
|
||||
.showNotification(notificationTitle, notificationOptions)
|
||||
.catch((error) => {
|
||||
console.error("Error showing notification:", error);
|
||||
return self.registration.showNotification(notificationTitle, notificationOptions);
|
||||
} else {
|
||||
console.log('Prevented duplicate background notification', notificationId);
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Firebase messaging initialization error:', error);
|
||||
}
|
||||
|
||||
// Improve service worker lifecycle for better compatibility
|
||||
self.addEventListener('install', function(event) {
|
||||
console.log('Service Worker installed');
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// Handle notification click
|
||||
self.addEventListener("notificationclick", (event) => {
|
||||
console.log("Notification clicked:", event);
|
||||
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === "open") {
|
||||
// Handle open action
|
||||
const urlToOpen = event.notification.data.url || "/app/dashboard/";
|
||||
event.waitUntil(clients.openWindow(urlToOpen));
|
||||
}
|
||||
self.addEventListener('activate', function(event) {
|
||||
console.log('Service Worker activated');
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
// Handle service worker installation
|
||||
self.addEventListener("install", (event) => {
|
||||
console.log("Service Worker installed");
|
||||
self.skipWaiting();
|
||||
// Add special handling for push events
|
||||
self.addEventListener('push', function(event) {
|
||||
console.log('Push event received in service worker', event);
|
||||
|
||||
if (!event.data) {
|
||||
console.log('Push event has no data');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = event.data.json();
|
||||
console.log('Push data:', payload);
|
||||
|
||||
// Generate a unique ID for this notification
|
||||
const notificationId = payload.data?.notificationId || `push-${Date.now()}`;
|
||||
|
||||
event.waitUntil(
|
||||
// Check for existing notifications first
|
||||
self.registration.getNotifications({tag: notificationId})
|
||||
.then(function(notifications) {
|
||||
// Only show if no matching notification exists
|
||||
if (notifications.length === 0) {
|
||||
const title = payload.notification?.title || 'New Notification';
|
||||
const options = {
|
||||
body: payload.notification?.body || 'You have a new message',
|
||||
icon: '/static/img/logo.png',
|
||||
badge: '/static/img/logo.png',
|
||||
tag: notificationId, // Add tag to identify this notification
|
||||
data: payload.data || {}
|
||||
};
|
||||
|
||||
return self.registration.showNotification(title, options);
|
||||
} else {
|
||||
console.log('Prevented duplicate push notification', notificationId);
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error handling push event:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle service worker activation
|
||||
self.addEventListener("activate", (event) => {
|
||||
console.log("Service Worker activated");
|
||||
event.waitUntil(clients.claim());
|
||||
// Improve notification click handling for various environments
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
console.log('Notification clicked:', event.notification);
|
||||
event.notification.close();
|
||||
|
||||
const urlToOpen = event.notification.data?.url || '/app/dashboard/';
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({type: 'window', includeUncontrolled: true}).then(windowClients => {
|
||||
// Check if there is already a window open with the target URL
|
||||
for (let i = 0; i < windowClients.length; i++) {
|
||||
const client = windowClients[i];
|
||||
// If so, focus on that window
|
||||
if (client.url === urlToOpen && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
// If not, open a new window
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(urlToOpen);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,964 @@
|
||||
// Function to update leave requests
|
||||
function updateLeaveRequests() {
|
||||
// Fetch staff leave requests
|
||||
fetch('/app/get-staff-leaves/')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const staffLeavesContainer = document.querySelector('#staff-leaves-container');
|
||||
if (data.staff_leaves.length === 0) {
|
||||
staffLeavesContainer.innerHTML = `
|
||||
<div class="text-center">
|
||||
<small class="text-muted">No pending staff leave requests</small>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let staffLeavesHtml = '';
|
||||
data.staff_leaves.forEach(leave => {
|
||||
staffLeavesHtml += `
|
||||
<div class="d-flex align-items-center mb-3 pb-3 border-bottom">
|
||||
<div class="flex-shrink-0 me-3">
|
||||
<div class="rounded bg-light d-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
|
||||
<i class="fas fa-calendar-times text-info"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<small class="mb-0 d-block fw-medium">${leave.staff_name}</small>
|
||||
<small class="text-muted">From: ${leave.start_date}</small>
|
||||
<small class="text-muted d-block">To: ${leave.end_date}</small>
|
||||
<small class="text-muted d-block">${leave.message}</small>
|
||||
<div class="mt-2">
|
||||
<form method="post" action="/app/approve-staff-leave/${leave.id}/" class="d-inline">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${document.querySelector('[name=csrfmiddlewaretoken]').value}">
|
||||
<button type="submit" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-check me-1"></i>Approve
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/app/reject-staff-leave/${leave.id}/" class="d-inline ms-2">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${document.querySelector('[name=csrfmiddlewaretoken]').value}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-times me-1"></i>Reject
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
staffLeavesContainer.innerHTML = staffLeavesHtml;
|
||||
});
|
||||
|
||||
// Fetch student leave requests
|
||||
fetch('/app/get-student-leaves/')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const studentLeavesContainer = document.querySelector('#student-leaves-container');
|
||||
if (data.student_leaves.length === 0) {
|
||||
studentLeavesContainer.innerHTML = `
|
||||
<div class="text-center">
|
||||
<small class="text-muted">No pending student leave requests</small>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let studentLeavesHtml = '';
|
||||
data.student_leaves.forEach(leave => {
|
||||
studentLeavesHtml += `
|
||||
<div class="d-flex align-items-center mb-3 pb-3 border-bottom">
|
||||
<div class="flex-shrink-0 me-3">
|
||||
<div class="rounded bg-light d-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
|
||||
<i class="fas fa-calendar-times text-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<small class="mb-0 d-block fw-medium">${leave.student_name}</small>
|
||||
<small class="text-muted">From: ${leave.start_date}</small>
|
||||
<small class="text-muted d-block">To: ${leave.end_date}</small>
|
||||
<small class="text-muted d-block">${leave.message}</small>
|
||||
<div class="mt-2">
|
||||
<form method="post" action="/app/approve-student-leave/${leave.id}/" class="d-inline">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${document.querySelector('[name=csrfmiddlewaretoken]').value}">
|
||||
<button type="submit" class="btn btn-success btn-sm">
|
||||
<i class="fas fa-check me-1"></i>Approve
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/app/reject-student-leave/${leave.id}/" class="d-inline ms-2">
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value="${document.querySelector('[name=csrfmiddlewaretoken]').value}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-times me-1"></i>Reject
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
studentLeavesContainer.innerHTML = studentLeavesHtml;
|
||||
});
|
||||
}
|
||||
|
||||
// Meeting-related functions
|
||||
function viewMeetingNotes(meetingId) {
|
||||
const modal = document.getElementById('viewNotesModal');
|
||||
const errorDiv = document.getElementById('notesError');
|
||||
const loadingDiv = document.getElementById('notesLoading');
|
||||
const contentDiv = document.getElementById('meetingNotesContent');
|
||||
const notesText = document.getElementById('viewMeetingNotesText');
|
||||
|
||||
// Reset states
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'block';
|
||||
contentDiv.style.display = 'none';
|
||||
notesText.textContent = '';
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Add keyboard event listener for Escape key
|
||||
document.addEventListener('keydown', handleNotesEscapeKey);
|
||||
|
||||
// Fetch notes
|
||||
fetch(`/app/api/meetings/${meetingId}/notes/`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new TypeError("Response was not JSON");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
notesText.textContent = data.notes || 'No notes available';
|
||||
loadingDiv.style.display = 'none';
|
||||
contentDiv.style.display = 'block';
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to fetch notes');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
loadingDiv.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function closeNotesModal() {
|
||||
const modal = document.getElementById('viewNotesModal');
|
||||
const errorDiv = document.getElementById('notesError');
|
||||
const loadingDiv = document.getElementById('notesLoading');
|
||||
const contentDiv = document.getElementById('meetingNotesContent');
|
||||
|
||||
// Reset states
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
contentDiv.style.display = 'none';
|
||||
|
||||
// Hide modal
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Remove keyboard event listener
|
||||
document.removeEventListener('keydown', handleNotesEscapeKey);
|
||||
}
|
||||
|
||||
function handleNotesEscapeKey(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeNotesModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Make all meeting-related functions globally available
|
||||
window.viewMeetingAgenda = function(meetingId) {
|
||||
const modal = document.getElementById('viewAgendaModal');
|
||||
const errorDiv = document.getElementById('agendaError');
|
||||
const loadingDiv = document.getElementById('agendaLoading');
|
||||
const contentDiv = document.getElementById('meetingAgendaContent');
|
||||
const agendaText = document.getElementById('viewMeetingAgendaText');
|
||||
|
||||
// Reset states
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'block';
|
||||
contentDiv.style.display = 'none';
|
||||
agendaText.textContent = '';
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Add keyboard event listener for Escape key
|
||||
document.addEventListener('keydown', handleAgendaEscapeKey);
|
||||
|
||||
// Fetch agenda
|
||||
fetch(`/app/api/meetings/${meetingId}/agenda/`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Network response was not ok: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Received agenda data:', data);
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
if (data.success && data.agenda) {
|
||||
agendaText.textContent = data.agenda;
|
||||
contentDiv.style.display = 'block';
|
||||
} else {
|
||||
errorDiv.textContent = 'No agenda available for this meeting';
|
||||
errorDiv.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching agenda:', error);
|
||||
loadingDiv.style.display = 'none';
|
||||
errorDiv.textContent = error.message || 'Failed to fetch agenda';
|
||||
errorDiv.style.display = 'block';
|
||||
});
|
||||
};
|
||||
|
||||
window.closeAgendaModal = function() {
|
||||
const modal = document.getElementById('viewAgendaModal');
|
||||
const errorDiv = document.getElementById('agendaError');
|
||||
const loadingDiv = document.getElementById('agendaLoading');
|
||||
const contentDiv = document.getElementById('meetingAgendaContent');
|
||||
const agendaText = document.getElementById('viewMeetingAgendaText');
|
||||
|
||||
// Reset states
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
contentDiv.style.display = 'none';
|
||||
agendaText.textContent = '';
|
||||
|
||||
// Hide modal
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Remove keyboard event listener
|
||||
document.removeEventListener('keydown', handleAgendaEscapeKey);
|
||||
};
|
||||
|
||||
window.handleAgendaEscapeKey = function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeAgendaModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.viewMeetingNotes = function(meetingId) {
|
||||
const modal = document.getElementById('viewNotesModal');
|
||||
const errorDiv = document.getElementById('notesError');
|
||||
const loadingDiv = document.getElementById('notesLoading');
|
||||
const contentDiv = document.getElementById('meetingNotesContent');
|
||||
const notesText = document.getElementById('viewMeetingNotesText');
|
||||
|
||||
// Reset states
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'block';
|
||||
contentDiv.style.display = 'none';
|
||||
notesText.textContent = '';
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Add keyboard event listener for Escape key
|
||||
document.addEventListener('keydown', handleNotesEscapeKey);
|
||||
|
||||
// Fetch notes
|
||||
fetch(`/app/api/meetings/${meetingId}/notes/`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new TypeError("Response was not JSON");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
notesText.textContent = data.notes || 'No notes available';
|
||||
loadingDiv.style.display = 'none';
|
||||
contentDiv.style.display = 'block';
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to fetch notes');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
loadingDiv.style.display = 'none';
|
||||
});
|
||||
};
|
||||
|
||||
window.closeNotesModal = function() {
|
||||
const modal = document.getElementById('viewNotesModal');
|
||||
const errorDiv = document.getElementById('notesError');
|
||||
const loadingDiv = document.getElementById('notesLoading');
|
||||
const contentDiv = document.getElementById('meetingNotesContent');
|
||||
|
||||
// Reset states
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
contentDiv.style.display = 'none';
|
||||
|
||||
// Hide modal
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Remove keyboard event listener
|
||||
document.removeEventListener('keydown', handleNotesEscapeKey);
|
||||
};
|
||||
|
||||
window.handleNotesEscapeKey = function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeNotesModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.cancelMeeting = function(meetingId) {
|
||||
const modal = document.getElementById('cancelMeetingModal');
|
||||
const form = document.getElementById('cancelMeetingForm');
|
||||
const errorDiv = document.getElementById('cancelError');
|
||||
const loadingDiv = document.getElementById('cancelLoading');
|
||||
|
||||
// Reset form and error states
|
||||
form.reset();
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
// Set meeting ID
|
||||
document.getElementById('cancelMeetingId').value = meetingId;
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Add keyboard event listener for Escape key
|
||||
document.addEventListener('keydown', handleCancelEscapeKey);
|
||||
};
|
||||
|
||||
window.closeCancelModal = function() {
|
||||
const modal = document.getElementById('cancelMeetingModal');
|
||||
const form = document.getElementById('cancelMeetingForm');
|
||||
const errorDiv = document.getElementById('cancelError');
|
||||
const loadingDiv = document.getElementById('cancelLoading');
|
||||
|
||||
// Reset form and error states
|
||||
form.reset();
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
// Hide modal
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Remove keyboard event listener
|
||||
document.removeEventListener('keydown', handleCancelEscapeKey);
|
||||
};
|
||||
|
||||
window.handleCancelEscapeKey = function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeCancelModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.submitCancelForm = function() {
|
||||
const form = document.getElementById('cancelMeetingForm');
|
||||
const errorDiv = document.getElementById('cancelError');
|
||||
const loadingDiv = document.getElementById('cancelLoading');
|
||||
const submitBtn = form.querySelector('button[type="button"]');
|
||||
const reason = document.getElementById('cancellationReason');
|
||||
|
||||
// Validate form
|
||||
if (!reason.value.trim()) {
|
||||
reason.classList.add('is-invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
loadingDiv.style.display = 'block';
|
||||
errorDiv.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
const meetingId = document.getElementById('cancelMeetingId').value;
|
||||
|
||||
fetch(`/app/api/meetings/${meetingId}/cancel/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
},
|
||||
body: JSON.stringify({ reason: reason.value })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new TypeError("Response was not JSON");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closeCancelModal();
|
||||
window.location.reload();
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to cancel meeting');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
})
|
||||
.finally(() => {
|
||||
loadingDiv.style.display = 'none';
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
};
|
||||
|
||||
window.rescheduleMeeting = function(meetingId) {
|
||||
const modal = document.getElementById('rescheduleMeetingModal');
|
||||
const form = document.getElementById('rescheduleMeetingForm');
|
||||
const errorDiv = document.getElementById('rescheduleError');
|
||||
const loadingDiv = document.getElementById('rescheduleLoading');
|
||||
|
||||
// Reset form and error states
|
||||
form.reset();
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
// Set meeting ID and minimum date
|
||||
document.getElementById('rescheduleMeetingId').value = meetingId;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('rescheduleDate').min = today;
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Add keyboard event listener for Escape key
|
||||
document.addEventListener('keydown', handleRescheduleEscapeKey);
|
||||
};
|
||||
|
||||
window.closeRescheduleModal = function() {
|
||||
const modal = document.getElementById('rescheduleMeetingModal');
|
||||
const form = document.getElementById('rescheduleMeetingForm');
|
||||
const errorDiv = document.getElementById('rescheduleError');
|
||||
const loadingDiv = document.getElementById('rescheduleLoading');
|
||||
|
||||
// Reset form and error states
|
||||
form.reset();
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
// Hide modal
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Remove keyboard event listener
|
||||
document.removeEventListener('keydown', handleRescheduleEscapeKey);
|
||||
};
|
||||
|
||||
window.handleRescheduleEscapeKey = function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeRescheduleModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.submitRescheduleForm = function() {
|
||||
const form = document.getElementById('rescheduleMeetingForm');
|
||||
const errorDiv = document.getElementById('rescheduleError');
|
||||
const loadingDiv = document.getElementById('rescheduleLoading');
|
||||
const submitBtn = form.querySelector('button[type="button"]');
|
||||
const date = document.getElementById('rescheduleDate');
|
||||
const time = document.getElementById('rescheduleTime');
|
||||
|
||||
// Validate form
|
||||
let isValid = true;
|
||||
if (!date.value) {
|
||||
date.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
if (!time.value) {
|
||||
time.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
if (!isValid) return;
|
||||
|
||||
// Show loading state
|
||||
loadingDiv.style.display = 'block';
|
||||
errorDiv.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
const meetingId = document.getElementById('rescheduleMeetingId').value;
|
||||
|
||||
fetch(`/app/api/meetings/${meetingId}/reschedule/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
},
|
||||
body: JSON.stringify({
|
||||
meeting_date: date.value,
|
||||
meeting_time: time.value
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new TypeError("Response was not JSON");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closeRescheduleModal();
|
||||
window.location.reload();
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to reschedule meeting');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
})
|
||||
.finally(() => {
|
||||
loadingDiv.style.display = 'none';
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
};
|
||||
|
||||
// Schedule Meeting Modal Functions
|
||||
window.openScheduleMeetingModal = function() {
|
||||
const modal = document.getElementById('scheduleMeetingModal');
|
||||
const form = document.getElementById('scheduleMeetingForm');
|
||||
const errorDiv = document.getElementById('scheduleMeetingError');
|
||||
const loadingDiv = document.getElementById('scheduleMeetingLoading');
|
||||
|
||||
// Reset form and error states
|
||||
form.reset();
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Set minimum date to today
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('meetingDate').min = today;
|
||||
|
||||
// Add keyboard event listener for Escape key
|
||||
document.addEventListener('keydown', handleEscapeKey);
|
||||
};
|
||||
|
||||
window.closeScheduleMeetingModal = function() {
|
||||
const modal = document.getElementById('scheduleMeetingModal');
|
||||
const form = document.getElementById('scheduleMeetingForm');
|
||||
const errorDiv = document.getElementById('scheduleMeetingError');
|
||||
const loadingDiv = document.getElementById('scheduleMeetingLoading');
|
||||
|
||||
// Reset form and error states
|
||||
form.reset();
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
// Hide modal
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Remove keyboard event listener
|
||||
document.removeEventListener('keydown', handleEscapeKey);
|
||||
};
|
||||
|
||||
// Function to handle Escape key press
|
||||
window.handleEscapeKey = function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeScheduleMeetingModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle meeting link field based on online meeting checkbox
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const isOnlineCheckbox = document.getElementById('isOnline');
|
||||
if (isOnlineCheckbox) {
|
||||
isOnlineCheckbox.addEventListener('change', function() {
|
||||
const linkContainer = document.getElementById('meetingLinkContainer');
|
||||
const linkInput = document.getElementById('meetingLink');
|
||||
|
||||
if (this.checked) {
|
||||
linkContainer.style.display = 'block';
|
||||
linkInput.required = true;
|
||||
} else {
|
||||
linkContainer.style.display = 'none';
|
||||
linkInput.required = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize all tooltips
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Initialize all popovers
|
||||
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
|
||||
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||
return new bootstrap.Popover(popoverTriggerEl);
|
||||
});
|
||||
|
||||
// Update leave requests every 30 seconds
|
||||
setInterval(updateLeaveRequests, 30000);
|
||||
|
||||
// Initial update
|
||||
updateLeaveRequests();
|
||||
});
|
||||
|
||||
// Function to validate form
|
||||
window.validateMeetingForm = function() {
|
||||
const form = document.getElementById('scheduleMeetingForm');
|
||||
const date = document.getElementById('meetingDate');
|
||||
const time = document.getElementById('meetingTime');
|
||||
const duration = document.getElementById('meetingDuration');
|
||||
const isOnline = document.getElementById('isOnline');
|
||||
const link = document.getElementById('meetingLink');
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Reset validation states
|
||||
form.classList.remove('was-validated');
|
||||
|
||||
// Validate date
|
||||
if (!date.value) {
|
||||
date.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
date.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
// Validate time
|
||||
if (!time.value) {
|
||||
time.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
time.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
// Validate duration
|
||||
if (!duration.value || duration.value < 15 || duration.value > 120) {
|
||||
duration.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
duration.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
// Validate meeting link if online meeting
|
||||
if (isOnline.checked && !link.value) {
|
||||
link.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
link.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
// Function to submit the meeting form
|
||||
window.submitMeetingForm = function() {
|
||||
if (!validateMeetingForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const form = document.getElementById('scheduleMeetingForm');
|
||||
const errorDiv = document.getElementById('scheduleMeetingError');
|
||||
const loadingDiv = document.getElementById('scheduleMeetingLoading');
|
||||
const submitBtn = form.querySelector('button[type="button"]');
|
||||
|
||||
// Show loading state
|
||||
loadingDiv.style.display = 'block';
|
||||
errorDiv.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = {
|
||||
meeting_date: formData.get('meeting_date'),
|
||||
meeting_time: formData.get('meeting_time'),
|
||||
duration: parseInt(formData.get('duration')),
|
||||
is_online: document.getElementById('isOnline').checked,
|
||||
meeting_link: formData.get('meeting_link'),
|
||||
agenda: document.getElementById('scheduleMeetingAgenda').value
|
||||
};
|
||||
|
||||
fetch('/app/api/meetings/add/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.error || 'Failed to schedule meeting');
|
||||
});
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new TypeError("Response was not JSON");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closeScheduleMeetingModal();
|
||||
// Show success message
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.className = 'alert alert-success alert-dismissible fade show';
|
||||
successDiv.innerHTML = `
|
||||
<strong>Success!</strong> Meeting scheduled successfully.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
`;
|
||||
document.querySelector('.content-card').insertAdjacentElement('afterbegin', successDiv);
|
||||
// Reload after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to schedule meeting');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error scheduling meeting:', error);
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
})
|
||||
.finally(() => {
|
||||
loadingDiv.style.display = 'none';
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
};
|
||||
|
||||
window.completeMeeting = function(meetingId) {
|
||||
const modal = document.getElementById('completeMeetingModal');
|
||||
const form = document.getElementById('completeMeetingForm');
|
||||
const errorDiv = document.getElementById('completeError');
|
||||
const loadingDiv = document.getElementById('completeLoading');
|
||||
|
||||
// Reset form and error states
|
||||
form.reset();
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
// Set meeting ID
|
||||
document.getElementById('completeMeetingId').value = meetingId;
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Add keyboard event listener for Escape key
|
||||
document.addEventListener('keydown', handleCompleteEscapeKey);
|
||||
};
|
||||
|
||||
window.closeCompleteModal = function() {
|
||||
const modal = document.getElementById('completeMeetingModal');
|
||||
const form = document.getElementById('completeMeetingForm');
|
||||
const errorDiv = document.getElementById('completeError');
|
||||
const loadingDiv = document.getElementById('completeLoading');
|
||||
|
||||
// Reset form and error states
|
||||
form.reset();
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
|
||||
// Hide modal
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Remove keyboard event listener
|
||||
document.removeEventListener('keydown', handleCompleteEscapeKey);
|
||||
};
|
||||
|
||||
window.handleCompleteEscapeKey = function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeCompleteModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.viewCancellationReason = function(meetingId) {
|
||||
const modal = document.getElementById('viewReasonModal');
|
||||
const errorDiv = document.getElementById('reasonError');
|
||||
const loadingDiv = document.getElementById('reasonLoading');
|
||||
const contentDiv = document.getElementById('meetingReasonContent');
|
||||
const reasonText = document.getElementById('viewMeetingReasonText');
|
||||
|
||||
// Reset states
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'block';
|
||||
contentDiv.style.display = 'none';
|
||||
reasonText.textContent = '';
|
||||
|
||||
// Show modal
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// Add keyboard event listener for Escape key
|
||||
document.addEventListener('keydown', handleReasonEscapeKey);
|
||||
|
||||
// Fetch cancellation reason
|
||||
fetch(`/app/api/meetings/${meetingId}/cancellation-reason/`, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success && data.reason) {
|
||||
reasonText.textContent = data.reason;
|
||||
loadingDiv.style.display = 'none';
|
||||
contentDiv.style.display = 'block';
|
||||
} else {
|
||||
throw new Error(data.error || 'No cancellation reason available');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
errorDiv.textContent = error.message;
|
||||
errorDiv.style.display = 'block';
|
||||
loadingDiv.style.display = 'none';
|
||||
});
|
||||
};
|
||||
|
||||
window.closeReasonModal = function() {
|
||||
const modal = document.getElementById('viewReasonModal');
|
||||
const errorDiv = document.getElementById('reasonError');
|
||||
const loadingDiv = document.getElementById('reasonLoading');
|
||||
const contentDiv = document.getElementById('meetingReasonContent');
|
||||
const reasonText = document.getElementById('viewMeetingReasonText');
|
||||
|
||||
// Reset states
|
||||
errorDiv.style.display = 'none';
|
||||
loadingDiv.style.display = 'none';
|
||||
contentDiv.style.display = 'none';
|
||||
reasonText.textContent = '';
|
||||
|
||||
// Hide modal
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
|
||||
// Remove keyboard event listener
|
||||
document.removeEventListener('keydown', handleReasonEscapeKey);
|
||||
};
|
||||
|
||||
window.handleReasonEscapeKey = function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeReasonModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Notice Modal Functions
|
||||
window.openAddNoticeModal = function() {
|
||||
document.getElementById('addNoticeModal').style.display = 'block';
|
||||
document.addEventListener('keydown', handleNoticeEscapeKey);
|
||||
}
|
||||
|
||||
window.closeAddNoticeModal = function() {
|
||||
document.getElementById('addNoticeModal').style.display = 'none';
|
||||
document.removeEventListener('keydown', handleNoticeEscapeKey);
|
||||
// Reset form
|
||||
document.getElementById('addNoticeForm').reset();
|
||||
// Hide error and loading states
|
||||
document.getElementById('addNoticeError').style.display = 'none';
|
||||
document.getElementById('addNoticeLoading').style.display = 'none';
|
||||
}
|
||||
|
||||
window.handleNoticeEscapeKey = function(event) {
|
||||
if (event.key === 'Escape') {
|
||||
closeAddNoticeModal();
|
||||
}
|
||||
}
|
||||
|
||||
window.submitNoticeForm = function() {
|
||||
const form = document.getElementById('addNoticeForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Show loading state
|
||||
document.getElementById('addNoticeLoading').style.display = 'block';
|
||||
document.getElementById('addNoticeError').style.display = 'none';
|
||||
|
||||
fetch('/app/add-notice/', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Close modal and refresh page to show new notice
|
||||
closeAddNoticeModal();
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Show error message
|
||||
document.getElementById('addNoticeError').textContent = data.error || 'Failed to add notice';
|
||||
document.getElementById('addNoticeError').style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('addNoticeError').textContent = 'An error occurred while adding the notice';
|
||||
document.getElementById('addNoticeError').style.display = 'block';
|
||||
})
|
||||
.finally(() => {
|
||||
document.getElementById('addNoticeLoading').style.display = 'none';
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
// Initialize dashboard functionality when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Navigation handling is now in navigation.js
|
||||
|
||||
// Initialize progress bars
|
||||
function initializeProgressBars() {
|
||||
document.querySelectorAll('.progress-bar').forEach(bar => {
|
||||
const progress = parseFloat(bar.getAttribute('aria-valuenow')) || 0;
|
||||
bar.style.width = `${progress}%`;
|
||||
|
||||
// Set color based on progress
|
||||
if (progress < 25) {
|
||||
bar.classList.add('bg-danger');
|
||||
} else if (progress < 75) {
|
||||
bar.classList.add('bg-warning');
|
||||
} else {
|
||||
bar.classList.add('bg-success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Call initialization
|
||||
initializeProgressBars();
|
||||
|
||||
// Initialize action buttons
|
||||
document.querySelectorAll('[data-action]').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const action = this.dataset.action;
|
||||
const id = this.dataset.id;
|
||||
|
||||
switch(action) {
|
||||
case 'view':
|
||||
viewStudent(id);
|
||||
break;
|
||||
case 'edit':
|
||||
editStudent(id);
|
||||
break;
|
||||
case 'delete':
|
||||
deleteStudent(id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize Firebase and request notification permission
|
||||
console.log('Firebase available:', typeof firebase !== 'undefined');
|
||||
console.log('Firebase messaging available:', typeof firebase !== 'undefined' && typeof firebase.messaging !== 'undefined');
|
||||
console.log('Service Worker supported:', 'serviceWorker' in navigator);
|
||||
console.log('Notification permission:', Notification.permission);
|
||||
|
||||
if (typeof getDeviceToken === 'function') {
|
||||
getDeviceToken().catch(error => {
|
||||
console.error('Error initializing Firebase:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Student Management Functions
|
||||
function viewStudent(id) {
|
||||
fetch(`/app/student/${id}/`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const modal = document.getElementById('studentDetailsModal');
|
||||
if (modal) {
|
||||
document.getElementById('studentName').textContent = data.name;
|
||||
document.getElementById('studentPhone').textContent = data.phone;
|
||||
document.getElementById('studentEmail').textContent = data.email || 'Not provided';
|
||||
document.getElementById('studentAddress').textContent = data.permanent_address || 'Not provided';
|
||||
document.getElementById('studentCourse').textContent = data.course?.name || 'Not assigned';
|
||||
document.getElementById('studentStatus').textContent = data.status || 'Not set';
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching student details:', error);
|
||||
alert('Error loading student details');
|
||||
});
|
||||
}
|
||||
|
||||
function editStudent(id) {
|
||||
fetch(`/app/student/${id}/edit/`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const modal = document.getElementById('editStudentModal');
|
||||
if (modal) {
|
||||
document.getElementById('editStudentId').value = data.id;
|
||||
document.getElementById('editStudentName').value = data.name;
|
||||
document.getElementById('editStudentPhone').value = data.phone;
|
||||
document.getElementById('editStudentEmail').value = data.email || '';
|
||||
document.getElementById('editStudentAddress').value = data.permanent_address || '';
|
||||
document.getElementById('editStudentCourse').value = data.course?.id || '';
|
||||
document.getElementById('editStudentStatus').value = data.status || '';
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching student data:', error);
|
||||
alert('Error loading student data');
|
||||
});
|
||||
}
|
||||
|
||||
function deleteStudent(id) {
|
||||
if (confirm('Are you sure you want to delete this student?')) {
|
||||
fetch(`/app/student/${id}/delete/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const row = document.querySelector(`tr[data-student-id="${id}"]`);
|
||||
if (row) {
|
||||
row.remove();
|
||||
}
|
||||
alert('Student deleted successfully');
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error deleting student:', error);
|
||||
alert('Error deleting student');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Batch Management Functions
|
||||
function editBatch(id) {
|
||||
fetch(`/app/batch/${id}/edit/`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const modal = document.getElementById('editBatchModal');
|
||||
if (modal) {
|
||||
document.getElementById('editBatchId').value = data.id;
|
||||
document.getElementById('editBatchName').value = data.name;
|
||||
document.getElementById('editBatchYear').value = data.year;
|
||||
document.getElementById('editBatchStatus').checked = data.is_active;
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching batch data:', error);
|
||||
alert('Error loading batch data');
|
||||
});
|
||||
}
|
||||
|
||||
function deleteBatch(id) {
|
||||
if (confirm('Are you sure you want to delete this batch?')) {
|
||||
fetch(`/app/batch/${id}/delete/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const row = document.querySelector(`tr[data-batch-id="${id}"]`);
|
||||
if (row) {
|
||||
row.remove();
|
||||
}
|
||||
alert('Batch deleted successfully');
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error deleting batch:', error);
|
||||
alert('Error deleting batch');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Tracking Management Functions
|
||||
function editTracking(id) {
|
||||
fetch(`/app/tracking/${id}/edit/`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const modal = document.getElementById('editTrackingModal');
|
||||
if (modal) {
|
||||
document.getElementById('editTrackingId').value = data.id;
|
||||
document.getElementById('editTrackingStudent').value = data.student_id;
|
||||
document.getElementById('editTrackingCourse').value = data.course_id;
|
||||
document.getElementById('editTrackingProgress').value = data.progress_status;
|
||||
document.getElementById('editTrackingPercentage').value = data.completion_percentage;
|
||||
document.getElementById('editTrackingStartDate').value = data.start_date;
|
||||
document.getElementById('editTrackingCurrentPeriod').value = data.current_period;
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching tracking data:', error);
|
||||
alert('Error loading tracking data');
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTracking(id) {
|
||||
if (confirm('Are you sure you want to delete this tracking record?')) {
|
||||
fetch(`/app/tracking/${id}/delete/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const row = document.querySelector(`tr[data-tracking-id="${id}"]`);
|
||||
if (row) {
|
||||
row.remove();
|
||||
}
|
||||
alert('Tracking record deleted successfully');
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error deleting tracking record:', error);
|
||||
alert('Error deleting tracking record');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Filter Functions
|
||||
function filterStudents() {
|
||||
const searchText = document.getElementById('studentSearch').value.toLowerCase();
|
||||
const statusFilter = document.getElementById('statusFilter').value;
|
||||
const table = document.getElementById('studentTable');
|
||||
const rows = table.getElementsByTagName('tr');
|
||||
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const cells = row.getElementsByTagName('td');
|
||||
if (cells.length === 0) continue;
|
||||
|
||||
const studentDetails = cells[1].textContent.toLowerCase();
|
||||
const contactInfo = cells[2].textContent.toLowerCase();
|
||||
const parentInfo = cells[3].textContent.toLowerCase();
|
||||
const courseInfo = cells[4].textContent.toLowerCase();
|
||||
const status = cells[5].textContent.trim().toLowerCase();
|
||||
|
||||
const matchesSearch = studentDetails.includes(searchText) ||
|
||||
contactInfo.includes(searchText) ||
|
||||
parentInfo.includes(searchText) ||
|
||||
courseInfo.includes(searchText);
|
||||
const matchesStatus = !statusFilter || status.includes(statusFilter.toLowerCase());
|
||||
|
||||
row.style.display = (matchesSearch && matchesStatus) ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function filterTrackings() {
|
||||
const searchText = document.getElementById('trackingSearch').value.toLowerCase();
|
||||
const progressFilter = document.getElementById('progressFilter').value;
|
||||
const table = document.getElementById('trackingTable');
|
||||
const rows = table.getElementsByTagName('tr');
|
||||
|
||||
for (let i = 1; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const cells = row.getElementsByTagName('td');
|
||||
if (cells.length === 0) continue;
|
||||
|
||||
const student = cells[0].textContent.toLowerCase();
|
||||
const course = cells[1].textContent.toLowerCase();
|
||||
const progress = cells[2].textContent.trim().toLowerCase();
|
||||
|
||||
const matchesSearch = student.includes(searchText) || course.includes(searchText);
|
||||
const matchesProgress = !progressFilter || progress === progressFilter.toLowerCase();
|
||||
|
||||
row.style.display = (matchesSearch && matchesProgress) ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Close modals when clicking outside
|
||||
window.onclick = function(event) {
|
||||
const modals = document.querySelectorAll('.modal');
|
||||
modals.forEach(modal => {
|
||||
if (event.target === modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close modals with close button
|
||||
document.querySelectorAll('.close-modal').forEach(button => {
|
||||
button.onclick = function() {
|
||||
const modal = this.closest('.modal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Modal Functions
|
||||
function openAddCourseTrackingModal() {
|
||||
// Reset form before showing modal
|
||||
const form = document.getElementById('addCourseTrackingForm');
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
|
||||
// Load students for dropdown
|
||||
fetch('/app/get-students/', {
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new Error('Response was not JSON');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const studentSelect = document.querySelector('#student');
|
||||
if (!studentSelect) {
|
||||
console.error('Student select element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
studentSelect.innerHTML = '<option value="">Select Student</option>';
|
||||
|
||||
if (data.success && data.students && Array.isArray(data.students)) {
|
||||
data.students.forEach(student => {
|
||||
if (student && student.id && student.display_name) {
|
||||
studentSelect.innerHTML += `<option value="${student.id}">${student.display_name}</option>`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
studentSelect.innerHTML = '<option value="" selected>No students available</option>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading students:', error);
|
||||
const studentSelect = document.querySelector('#student');
|
||||
if (studentSelect) {
|
||||
studentSelect.innerHTML = '<option value="" selected>Failed to load students</option>';
|
||||
}
|
||||
});
|
||||
|
||||
// Load courses for dropdown
|
||||
fetch('/app/get-courses/', {
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new Error('Response was not JSON');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const courseSelect = document.querySelector('#track_course');
|
||||
if (!courseSelect) {
|
||||
console.error('Course select element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
courseSelect.innerHTML = '<option value="">Select Course</option>';
|
||||
|
||||
if (data.success && data.courses && Array.isArray(data.courses)) {
|
||||
data.courses.forEach(course => {
|
||||
if (course && course.id && course.display_name) {
|
||||
courseSelect.innerHTML += `<option value="${course.id}">${course.display_name}</option>`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
courseSelect.innerHTML = '<option value="" selected>No courses available</option>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading courses:', error);
|
||||
const courseSelect = document.querySelector('#track_course');
|
||||
if (courseSelect) {
|
||||
courseSelect.innerHTML = '<option value="" selected>Failed to load courses</option>';
|
||||
}
|
||||
});
|
||||
|
||||
// Show the modal
|
||||
const modal = document.getElementById('addCourseTrackingModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
function closeAddCourseTrackingModal() {
|
||||
document.getElementById('addCourseTrackingModal').style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
+138
-36
@@ -42,6 +42,28 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||
closeAllModals();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize responsive modals
|
||||
initResponsiveModals();
|
||||
|
||||
// Make all tables responsive
|
||||
makeAllTablesResponsive();
|
||||
|
||||
// Handle long content
|
||||
handleLongContent();
|
||||
|
||||
// Process any dynamically added tables
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList') {
|
||||
makeAllTablesResponsive();
|
||||
handleLongContent();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing the document for added nodes
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
|
||||
// Function to initialize all dashboard components
|
||||
@@ -485,11 +507,8 @@ function showWeakPasswordModal() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword === '123') {
|
||||
alert('Please choose a stronger password!');
|
||||
passwordArea.focus();
|
||||
return;
|
||||
}
|
||||
// Get CSRF token
|
||||
var csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]')?.value || '';
|
||||
|
||||
// Create form data
|
||||
var formData = new FormData();
|
||||
@@ -503,25 +522,27 @@ function showWeakPasswordModal() {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Cache-Control': 'no-cache'
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(function(response) {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
// Use notification system instead of alert
|
||||
// Show success message
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('Password Updated', 'Your password has been changed successfully!', 'success');
|
||||
window.showNotification('Password Updated', data.message || 'Your password has been changed successfully. Please login again.', 'success');
|
||||
} else {
|
||||
alert('Password changed successfully!');
|
||||
alert(data.message || 'Password changed successfully. Please login again.');
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
document.getElementById('weakPasswordModal')?.remove();
|
||||
// No redirect to login page
|
||||
|
||||
// Wait a moment for the notification to be visible
|
||||
setTimeout(function() {
|
||||
// Redirect to login page
|
||||
window.location.href = data.redirect || '/login/';
|
||||
}, 1500);
|
||||
} else if (data.error) {
|
||||
// Show error notification
|
||||
if (typeof window.showNotification === 'function') {
|
||||
@@ -585,18 +606,6 @@ function setupEventHandlers() {
|
||||
});
|
||||
}
|
||||
|
||||
// Function to get device token for notifications
|
||||
function getDeviceToken() {
|
||||
// Do nothing - removed to prevent recursion
|
||||
console.log("getDeviceToken called - disabled to prevent recursion");
|
||||
}
|
||||
|
||||
// Add global function to get device token - but avoid recursion
|
||||
window.getDeviceToken = function() {
|
||||
console.log("FCM token function called once");
|
||||
// Implementation depends on your Firebase setup - but don't call itself again
|
||||
};
|
||||
|
||||
// Function to close all custom modals
|
||||
function closeAllModals() {
|
||||
// Close standard modals with display:none style
|
||||
@@ -730,12 +739,6 @@ function setupDirectPasswordHandlers() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword === '123') {
|
||||
alert('Please choose a stronger password!');
|
||||
passwordArea.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get CSRF token
|
||||
var csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]')?.value || '';
|
||||
|
||||
@@ -757,16 +760,21 @@ function setupDirectPasswordHandlers() {
|
||||
.then(response => response.json())
|
||||
.then(function(data) {
|
||||
if (data.success) {
|
||||
// Use notification system instead of alert
|
||||
// Show success message
|
||||
if (typeof window.showNotification === 'function') {
|
||||
window.showNotification('Password Updated', 'Your password has been changed successfully!', 'success');
|
||||
window.showNotification('Password Updated', data.message || 'Your password has been changed successfully. Please login again.', 'success');
|
||||
} else {
|
||||
alert('Password changed successfully!');
|
||||
alert(data.message || 'Password changed successfully. Please login again.');
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
document.getElementById('weakPasswordModal')?.remove();
|
||||
// No redirect to login page
|
||||
|
||||
// Wait a moment for the notification to be visible
|
||||
setTimeout(function() {
|
||||
// Redirect to login page
|
||||
window.location.href = data.redirect || '/login/';
|
||||
}, 1500);
|
||||
} else if (data.error) {
|
||||
// Show error notification
|
||||
if (typeof window.showNotification === 'function') {
|
||||
@@ -782,4 +790,98 @@ function setupDirectPasswordHandlers() {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Function to initialize responsive modals
|
||||
function initResponsiveModals() {
|
||||
// Function to update modal styles based on screen size
|
||||
function updateModalStyles() {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const isSmallMobile = window.innerWidth < 576;
|
||||
|
||||
document.querySelectorAll('.modal-dialog').forEach(function(dialog) {
|
||||
if (isSmallMobile) {
|
||||
dialog.classList.add('modal-fullscreen-sm-down');
|
||||
dialog.classList.remove('modal-dialog-centered');
|
||||
} else if (isMobile) {
|
||||
dialog.classList.remove('modal-fullscreen-sm-down');
|
||||
dialog.classList.add('modal-dialog-centered');
|
||||
} else {
|
||||
dialog.classList.remove('modal-fullscreen-sm-down', 'modal-dialog-centered');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial update
|
||||
updateModalStyles();
|
||||
|
||||
// Update on resize
|
||||
window.addEventListener('resize', debounce(updateModalStyles, 250));
|
||||
|
||||
// Update when modal is shown
|
||||
document.querySelectorAll('.modal').forEach(function(modal) {
|
||||
modal.addEventListener('show.bs.modal', updateModalStyles);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to make all tables responsive
|
||||
function makeAllTablesResponsive() {
|
||||
document.querySelectorAll('table:not(.table-responsive)').forEach(function(table) {
|
||||
if (!table.parentElement.classList.contains('table-responsive')) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.classList.add('table-responsive');
|
||||
table.parentNode.insertBefore(wrapper, table);
|
||||
wrapper.appendChild(table);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to handle long content
|
||||
function handleLongContent() {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
|
||||
// Handle text truncation
|
||||
document.querySelectorAll('[data-truncate-mobile]').forEach(function(element) {
|
||||
if (isMobile) {
|
||||
const fullText = element.getAttribute('data-full-text') || element.textContent;
|
||||
const maxLength = parseInt(element.getAttribute('data-truncate-mobile'), 10) || 50;
|
||||
|
||||
if (!element.getAttribute('data-full-text')) {
|
||||
element.setAttribute('data-full-text', fullText);
|
||||
}
|
||||
|
||||
if (fullText.length > maxLength) {
|
||||
element.textContent = fullText.substring(0, maxLength) + '...';
|
||||
}
|
||||
} else {
|
||||
// Restore full text on desktop
|
||||
if (element.getAttribute('data-full-text')) {
|
||||
element.textContent = element.getAttribute('data-full-text');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle long tables
|
||||
document.querySelectorAll('.table-responsive').forEach(function(table) {
|
||||
if (isMobile) {
|
||||
table.style.maxHeight = '400px';
|
||||
table.style.overflowY = 'auto';
|
||||
} else {
|
||||
table.style.maxHeight = '';
|
||||
table.style.overflowY = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Debounce function to limit the rate at which a function can fire
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function() {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(function() {
|
||||
func.apply(context, args);
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
@@ -157,4 +157,4 @@ messaging.onMessage((payload) => {
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
// Global variables to track current filters
|
||||
let currentPeriodFilter = '';
|
||||
let currentSearchText = '';
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('HOD Dashboard initializing...');
|
||||
|
||||
// Initialize filters
|
||||
initializeFilters();
|
||||
|
||||
// Initialize Firebase if available
|
||||
if (typeof getDeviceToken === 'function') {
|
||||
getDeviceToken().catch(error => {
|
||||
console.error('Error initializing Firebase:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize all filters
|
||||
function initializeFilters() {
|
||||
// Get the filter elements
|
||||
const periodFilter = document.getElementById('periodFilter');
|
||||
const searchBox = document.getElementById('subjectSearch');
|
||||
const tableBody = document.getElementById('subjectsTableBody');
|
||||
|
||||
if (!periodFilter || !searchBox || !tableBody) {
|
||||
console.error('Required elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset filter values
|
||||
periodFilter.value = '';
|
||||
searchBox.value = '';
|
||||
currentPeriodFilter = '';
|
||||
currentSearchText = '';
|
||||
|
||||
// Add event listeners
|
||||
periodFilter.addEventListener('change', function() {
|
||||
currentPeriodFilter = this.value;
|
||||
console.log('Period filter changed to:', currentPeriodFilter);
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
searchBox.addEventListener('input', function() {
|
||||
currentSearchText = this.value.toLowerCase();
|
||||
console.log('Search text changed to:', currentSearchText);
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
searchBox.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
this.value = '';
|
||||
currentSearchText = '';
|
||||
applyFilters();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply both filters
|
||||
function applyFilters() {
|
||||
const tableBody = document.getElementById('subjectsTableBody');
|
||||
if (!tableBody) {
|
||||
console.error('Subject table body not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = tableBody.getElementsByTagName('tr');
|
||||
let visibleCount = 0;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
|
||||
// Skip the "no results" row
|
||||
if (row.cells.length === 1) continue;
|
||||
|
||||
// Get cells content
|
||||
const nameCell = row.cells[0].textContent.toLowerCase();
|
||||
const codeCell = row.cells[1].textContent.toLowerCase();
|
||||
const periodCell = row.cells[2];
|
||||
|
||||
// Get the period number from the cell
|
||||
let periodNumber = '';
|
||||
if (periodCell.hasAttribute('data-period')) {
|
||||
periodNumber = periodCell.getAttribute('data-period');
|
||||
} else {
|
||||
// Extract just the period number from text content
|
||||
const periodText = periodCell.textContent.trim();
|
||||
periodNumber = periodText.split(' ')[0];
|
||||
}
|
||||
|
||||
// Clean up the period number (remove any non-numeric characters)
|
||||
periodNumber = periodNumber.replace(/\D/g, '');
|
||||
|
||||
// Determine matches
|
||||
const matchesPeriod = !currentPeriodFilter || periodNumber === currentPeriodFilter;
|
||||
const matchesSearch = !currentSearchText ||
|
||||
nameCell.includes(currentSearchText) ||
|
||||
codeCell.includes(currentSearchText);
|
||||
|
||||
// Show or hide based on both conditions
|
||||
if (matchesPeriod && matchesSearch) {
|
||||
row.style.display = '';
|
||||
visibleCount++;
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide "no results" message
|
||||
const noResultsRow = tableBody.querySelector('tr td[colspan="5"]');
|
||||
if (noResultsRow) {
|
||||
const parentRow = noResultsRow.parentElement;
|
||||
if (visibleCount === 0) {
|
||||
parentRow.style.display = '';
|
||||
} else {
|
||||
parentRow.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Filter applied. Visible rows:', visibleCount);
|
||||
}
|
||||
|
||||
// Initialize charts using Chart.js
|
||||
function initializeCharts() {
|
||||
const chartCanvas = document.getElementById('subjectDistributionChart');
|
||||
if (!chartCanvas) return;
|
||||
|
||||
const ctx = chartCanvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Get data attributes
|
||||
const courseName = chartCanvas.getAttribute('data-course-name') || 'Course';
|
||||
const durationType = chartCanvas.getAttribute('data-duration-type') || 'Year';
|
||||
|
||||
// Parse chart data
|
||||
let chartData;
|
||||
try {
|
||||
chartData = JSON.parse(chartCanvas.getAttribute('data-chart-data') || '[]');
|
||||
} catch (e) {
|
||||
console.error('Error parsing chart data:', e);
|
||||
chartData = [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
// Create labels based on duration type
|
||||
const labels = [];
|
||||
chartData.forEach((value, index) => {
|
||||
const num = index + 1;
|
||||
const suffix = getSuffix(num);
|
||||
labels.push(`${num}${suffix} ${durationType}`);
|
||||
});
|
||||
|
||||
// Create chart
|
||||
new Chart(ctx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: chartData,
|
||||
backgroundColor: [
|
||||
'rgba(75, 192, 192, 0.7)',
|
||||
'rgba(54, 162, 235, 0.7)',
|
||||
'rgba(255, 206, 86, 0.7)',
|
||||
'rgba(255, 99, 132, 0.7)',
|
||||
'rgba(153, 102, 255, 0.7)',
|
||||
'rgba(255, 159, 64, 0.7)',
|
||||
'rgba(199, 199, 199, 0.7)',
|
||||
'rgba(83, 102, 255, 0.7)'
|
||||
]
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: `${courseName} Subjects by ${durationType}`
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get ordinal suffix (1st, 2nd, 3rd, etc.)
|
||||
function getSuffix(num) {
|
||||
if (num >= 11 && num <= 13) return 'th';
|
||||
|
||||
switch (num % 10) {
|
||||
case 1: return 'st';
|
||||
case 2: return 'nd';
|
||||
case 3: return 'rd';
|
||||
default: return 'th';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize section navigation
|
||||
function initializeSectionNavigation() {
|
||||
const navButtons = document.querySelectorAll('[data-target]');
|
||||
|
||||
navButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const targetId = this.getAttribute('data-target');
|
||||
showSection(targetId);
|
||||
});
|
||||
});
|
||||
|
||||
// Initial section display
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
if (hash && document.getElementById(hash)) {
|
||||
showSection(hash);
|
||||
}
|
||||
}
|
||||
|
||||
// Show a specific section and update navigation
|
||||
function showSection(sectionId) {
|
||||
// Hide all sections
|
||||
const sections = document.querySelectorAll('.content-section');
|
||||
sections.forEach(section => {
|
||||
section.style.display = 'none';
|
||||
});
|
||||
|
||||
// Show the selected section
|
||||
const targetSection = document.getElementById(sectionId);
|
||||
if (targetSection) {
|
||||
targetSection.style.display = 'block';
|
||||
|
||||
// Update navigation
|
||||
const navButtons = document.querySelectorAll('[data-target]');
|
||||
navButtons.forEach(button => {
|
||||
const isActive = button.getAttribute('data-target') === sectionId;
|
||||
if (isActive) {
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update URL hash without scrolling
|
||||
const scrollPos = window.scrollY;
|
||||
window.location.hash = sectionId;
|
||||
window.scrollTo(0, scrollPos);
|
||||
|
||||
// Apply filters if we're in the subjects section
|
||||
if (sectionId === 'subjectsSection') {
|
||||
setTimeout(applyFilters, 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subject action functions
|
||||
function viewSubjectDetails(subjectId) {
|
||||
alert(`View details for subject ${subjectId}`);
|
||||
}
|
||||
|
||||
function editSubject(subjectId) {
|
||||
alert(`Edit subject ${subjectId}`);
|
||||
}
|
||||
|
||||
function confirmDeleteSubject(subjectId) {
|
||||
if (confirm('Are you sure you want to delete this subject?')) {
|
||||
alert(`Delete subject ${subjectId}`);
|
||||
}
|
||||
}
|
||||
|
||||
function openAddSubjectModal() {
|
||||
alert('Add new subject');
|
||||
}
|
||||
@@ -85,4 +85,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
// Run initial load before rendering content
|
||||
handleInitialLoad();
|
||||
|
||||
// Expose the showSection function globally for backward compatibility with onclick handlers
|
||||
window.showSection = function(sectionId) {
|
||||
if (typeof sectionId === 'string') {
|
||||
// Remove 'Section' suffix if it exists
|
||||
const cleanSectionId = sectionId.replace('Section', '');
|
||||
switchToSection(cleanSectionId);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -146,8 +146,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (responseDiv) {
|
||||
responseDiv.style.display = 'block';
|
||||
|
||||
+20
-893
@@ -1,785 +1,38 @@
|
||||
// Subject Materials Section Functions
|
||||
// Subject Materials JavaScript
|
||||
|
||||
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>
|
||||
`;
|
||||
});
|
||||
}
|
||||
// Global variable for file deletion
|
||||
window.fileIdToDelete = null;
|
||||
|
||||
// Modal Functions
|
||||
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();
|
||||
}
|
||||
|
||||
// Hide response div
|
||||
if (responseDiv) {
|
||||
responseDiv.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// If a subject ID was provided, pre-select it
|
||||
// Pre-select subject if provided
|
||||
if (subjectId && subjectSelect) {
|
||||
subjectSelect.value = subjectId;
|
||||
}
|
||||
|
||||
// Show modal
|
||||
if (modal) {
|
||||
modal.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
// Close upload modal
|
||||
function closeUploadModal() {
|
||||
const modal = document.getElementById('uploadFileModal');
|
||||
if (modal) {
|
||||
@@ -788,8 +41,9 @@ function closeUploadModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete file
|
||||
function deleteFile(fileId) {
|
||||
fileIdToDelete = fileId;
|
||||
window.fileIdToDelete = fileId;
|
||||
const modal = document.getElementById('confirmationModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'block';
|
||||
@@ -797,145 +51,18 @@ function deleteFile(fileId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Close confirmation modal
|
||||
function closeConfirmationModal() {
|
||||
const modal = document.getElementById('confirmationModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
fileIdToDelete = null;
|
||||
window.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');
|
||||
}
|
||||
}
|
||||
// Make modal functions globally accessible
|
||||
window.openUploadModal = openUploadModal;
|
||||
window.closeUploadModal = closeUploadModal;
|
||||
window.deleteFile = deleteFile;
|
||||
window.closeConfirmationModal = closeConfirmationModal;
|
||||
Reference in new issue
Block a user