huge updates

This commit is contained in:
sankalp committed 2025-04-15 04:20:26 +05:45
1 parent 2912191091
commit 8507d06ccf
82 files changed
+14911 -4509

No files matched your search

+97
View File
@@ -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));
}
}
-1
View File
@@ -19,4 +19,3 @@ img[src*="/static/img/logo.png"] {
margin-right: .5rem;
margin-top: -3px;
}
+102
View File
@@ -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;
}
}
+130 -78
View File
@@ -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);
}
})
);
});
+964
View File
@@ -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';
});
}
+406
View File
@@ -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
View File
@@ -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);
};
}
+1 -1
View File
@@ -157,4 +157,4 @@ messaging.onMessage((payload) => {
});
});
}
});
});
+271
View File
@@ -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');
}
+9
View File
@@ -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);
}
};
});
+10 -1
View File
@@ -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
View File
@@ -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;