/", views.delete_notice, name="delete_notice"),
+
+ # Password Reset URLs
+ path("password-reset/", auth.reset_password_options, name="reset_password_options"),
+ path("password-reset/phone/", auth.password_reset_phone, name="phone_reset_password"),
+ path("password-reset/phone/verify/", auth.password_reset_phone_verify, name="verify_reset_otp"),
+ path("password-reset/phone/resend/", auth.resend_phone_otp, name="resend_phone_otp"),
+ path("password-reset/email/", auth.password_reset_email, name="email_reset_password"),
+ path("password-reset/email/verify/", auth.password_reset_email_verify, name="password_reset_done"),
+ path("password-reset/email/resend/", auth.resend_email_code, name="resend_reset_code"),
+ path("password-reset/set/", auth.set_new_password, name="set_new_password"),
+
+ # Password security check
+ path("check-weak-password/", views.check_weak_password, name="check_weak_password"),
+
+ # Staff Management URLs
+ path("get-students/", staffviews.get_students, name="get_students"),
]
+
diff --git a/app/utils.py b/app/utils.py
new file mode 100644
index 0000000..f8ab97a
--- /dev/null
+++ b/app/utils.py
@@ -0,0 +1,186 @@
+import random
+import string
+import requests
+from django.template.loader import render_to_string
+from django.core.mail import send_mail
+from django.conf import settings
+from django.contrib.auth.tokens import default_token_generator
+from django.utils.http import urlsafe_base64_encode
+from django.utils.encoding import force_bytes
+from django.utils import timezone
+import pyotp
+import base64
+from .models import TOTPSecret, ResetToken, Student, Staff
+
+# OTP expiration time in seconds (5 minutes)
+OTP_EXPIRY = 300
+
+
+def generate_secret_key():
+ """Generate a random secret key for TOTP"""
+ return base64.b32encode(random.getrandbits(160).to_bytes(20, 'big')).decode('utf-8')
+
+
+def generate_otp(secret_key):
+ """Generate a TOTP using the secret key"""
+ totp = pyotp.TOTP(secret_key, interval=OTP_EXPIRY)
+ return totp.now()
+
+
+def store_secret_key(identifier, secret_key):
+ """Store secret key in database"""
+ # Delete any existing secrets for this identifier
+ TOTPSecret.objects.filter(identifier=identifier).delete()
+
+ # Create new secret with expiration
+ expires_at = timezone.now() + timezone.timedelta(seconds=OTP_EXPIRY)
+ return TOTPSecret.objects.create(
+ identifier=identifier,
+ secret_key=secret_key,
+ expires_at=expires_at
+ )
+
+
+def verify_otp(identifier, otp):
+ """Verify OTP using TOTP"""
+ try:
+ # Get the most recent secret for this identifier
+ secret = TOTPSecret.objects.filter(identifier=identifier).latest('created_at')
+
+ # Check if secret is expired
+ if secret.is_expired():
+ return False
+
+ # Verify OTP
+ totp = pyotp.TOTP(secret.secret_key, interval=OTP_EXPIRY)
+ return totp.verify(otp)
+ except TOTPSecret.DoesNotExist:
+ return False
+
+
+def send_otp_sms(phone, otp):
+ """Send OTP via SMS using a service"""
+ try:
+ # Get user details from phone number
+ student = Student.objects.filter(phone=phone).first()
+ staff = Staff.objects.filter(phone=phone).first()
+ user = student or staff
+
+ # Get user's name
+ user_name = user.name if user else "User"
+
+ # Calculate expiry time in minutes
+ expiry_minutes = OTP_EXPIRY // 60
+
+ # Render SMS message using template
+ message = render_to_string("login/sms_templates/otp_message.txt", {
+ "user_name": user_name,
+ "otp": otp,
+ "expiry_time": expiry_minutes
+ })
+
+ # This is a placeholder. You need to replace with actual SMS API implementation
+ # Example using a generic SMS API
+ # api_url = "https://api.example.com/send"
+ # payload = {
+ # "apikey": settings.SMS_API_KEY,
+ # "to": phone,
+ # "message": message
+ # }
+ # response = requests.post(api_url, json=payload)
+ # return response.status_code == 200
+
+ print("\nSMS Message Preview:")
+ print("-" * 50)
+ print(message)
+ print("-" * 50)
+ return True
+ except Exception as e:
+ print(f"Error sending OTP: {str(e)}")
+ return False
+
+
+def send_password_reset_email(user, request, email):
+ """
+ Send password reset code to user's email using HTML template
+ """
+ try:
+ # Generate a secret key for TOTP
+ secret_key = generate_secret_key()
+
+ # Store the secret key in database
+ store_secret_key(email, secret_key)
+
+ # Generate TOTP
+ totp = pyotp.TOTP(secret_key, interval=OTP_EXPIRY)
+ reset_code = totp.now()
+
+ # Build email context
+ context = {
+ "user": user,
+ "domain": request.META.get("HTTP_HOST", "localhost:8000"),
+ "protocol": "https" if request.is_secure() else "http",
+ "reset_code": reset_code,
+ }
+
+ # Render email content using template
+ subject = render_to_string("login/email_templates/password_reset_subject.txt")
+ message = render_to_string(
+ "login/email_templates/password_reset_email.html", context
+ )
+
+ # Send email using console backend
+ send_mail(
+ subject=subject,
+ message=message,
+ from_email="noreply@studentmanagement.com",
+ recipient_list=[email],
+ html_message=message,
+ fail_silently=False,
+ )
+ print(f"\nPassword reset code sent to {email}")
+ print(f"Reset Code: {reset_code}\n")
+ return True
+ except Exception as e:
+ print(f"Failed to send email: {str(e)}")
+ return False
+
+
+def verify_reset_code(email, code):
+ """Verify password reset code using TOTP"""
+ return verify_otp(email, code)
+
+
+def store_reset_token(token, identifier):
+ """Store reset token in database"""
+ # Delete any existing tokens for this identifier
+ ResetToken.objects.filter(identifier=identifier).delete()
+
+ # Create new token with expiration (15 minutes)
+ expires_at = timezone.now() + timezone.timedelta(minutes=15)
+ return ResetToken.objects.create(
+ token=token,
+ identifier=identifier,
+ expires_at=expires_at
+ )
+
+
+def verify_reset_token(token, identifier):
+ """Verify reset token from database"""
+ try:
+ reset_token = ResetToken.objects.get(token=token)
+
+ # Check if token is expired
+ if reset_token.is_expired():
+ return False
+
+ return reset_token.identifier == identifier
+ except ResetToken.DoesNotExist:
+ return False
+
+
+def cleanup_expired_tokens():
+ """Clean up expired TOTP secrets and reset tokens"""
+ TOTPSecret.objects.filter(expires_at__lt=timezone.now()).delete()
+ ResetToken.objects.filter(expires_at__lt=timezone.now()).delete()
+
diff --git a/app/views.py b/app/views.py
index 9724de7..2df4b4a 100644
--- a/app/views.py
+++ b/app/views.py
@@ -1,6 +1,865 @@
-from django.contrib.auth import authenticate, login, logout
-from django.http import HttpResponseRedirect, HttpResponse
-from django.shortcuts import render, redirect
-from django.contrib import messages
+from django.shortcuts import render, redirect, get_object_or_404
+from django.http import JsonResponse, HttpResponse, FileResponse
from django.contrib.auth.decorators import login_required
+from django.contrib import messages
+from django.db.models import Count, Q, Avg
+from django.utils import timezone
+from datetime import timedelta
+from django.conf import settings
+from .models import (
+ Student,
+ Course,
+ CourseTracking,
+ Student_Leave,
+ Staff_leave,
+ Attendance,
+ AttendanceRecord,
+ StudentFeedback,
+ Routine,
+ Staff,
+ Notice,
+ Institute,
+ InstituteFeedback,
+ Subject,
+ Batch,
+)
+from app.admin import custom_admin_site
+from django.views.decorators.http import require_http_methods, require_GET, require_POST
+from django.core.files.storage import default_storage
+from django.core.files.base import ContentFile
+import json
+import os
+from django.db.models.base import transaction
+from .utils import generate_otp, generate_secret_key, store_secret_key, verify_otp, send_otp_sms
+from django.conf import settings
+from dotenv import load_dotenv
+from .firebase import save_fcm_token, get_firebase_js, FCMDevice
+from django.template.loader import render_to_string
+from django.views.static import serve
+# Import views from studentviews.py
+from .studentviews import (
+ studentDashboard,
+ request_leave,
+ submit_feedback,
+ submit_institute_feedback,
+ get_subject_files,
+)
+
+# Import views from staffviews.py
+from .staffviews import (
+ teacherDashboard,
+ request_staff_leave,
+ submit_staff_institute_feedback,
+ save_attendance,
+ get_subject_schedule,
+ teacher_update_profile_picture,
+ teacher_change_password,
+)
+
+from django.contrib.auth.hashers import check_password
+
+# Load environment variables
+load_dotenv()
+
# Create your views here.
+
+
+
+@login_required
+def dashboard(request):
+ """Main dashboard view that redirects to appropriate dashboard based on user role"""
+ user = request.user
+ user_groups = user.groups.values_list("name", flat=True)
+
+
+
+ # Redirect based on user role
+ if "Student" in user_groups:
+ print("Redirecting to student dashboard")
+ return redirect("studentDashboard")
+ elif "Teacher" in user_groups:
+ print("Redirecting to teacher dashboard")
+ return redirect("teacherDashboard")
+ elif user.is_superuser or user.is_staff:
+ # Admin users get the admin dashboard
+ print("Showing admin dashboard")
+
+ # Get current date and date ranges
+ today = timezone.now().date()
+ last_week = timezone.now() - timedelta(days=7)
+ last_month = timezone.now() - timedelta(days=30)
+
+ # Debug prints for user authentication
+ print(f"Is authenticated: {request.user.is_authenticated}")
+ print(f"Is staff: {request.user.is_staff}")
+ print(f"Is superuser: {request.user.is_superuser}")
+ print("-------------------------\n")
+
+ # Student Statistics - with debug prints
+ total_students = Student.objects.count()
+ print("\nDEBUG Student Statistics:")
+ print("-------------------------")
+ print(f"Total students (from count): {total_students}")
+
+ # Get all students and print their statuses
+ all_students = Student.objects.all()
+ print("\nAll students and their statuses:")
+ for student in all_students:
+ print(f"- {student.name}: {student.status}")
+
+ active_students = Student.objects.filter(status="Active").count()
+ print(f"\nActive students: {active_students}")
+
+ completed_students = Student.objects.filter(status="Completed").count()
+ print(f"Completed students: {completed_students}")
+
+ on_leave_students = Student.objects.filter(status="Leave").count()
+ print(f"Students on leave: {on_leave_students}")
+
+ print(f"\nSum of status counts: {active_students + completed_students + on_leave_students}")
+ print("-------------------------\n")
+
+ # Staff Statistics
+ total_staff = Staff.objects.count()
+ active_staff = Staff.objects.filter(is_active=True).count()
+ staff_on_leave = (
+ Staff_leave.objects.filter(status=1).values("staff").distinct().count()
+ )
+
+ # Course Statistics
+ total_courses = Course.objects.count()
+ active_courses = (
+ Course.objects.filter(student_trackings__progress_status="In Progress")
+ .distinct()
+ .count()
+ )
+ completed_courses = (
+ Course.objects.filter(student_trackings__progress_status="Completed")
+ .distinct()
+ .count()
+ )
+
+ # Course Tracking Statistics
+ course_trackings = CourseTracking.objects.all()
+ in_progress_trackings = course_trackings.filter(progress_status="In Progress")
+ completed_trackings = course_trackings.filter(progress_status="Completed")
+ dropped_trackings = course_trackings.filter(progress_status="Dropped")
+
+ # Recent Leave Requests
+ recent_student_leaves = Student_Leave.objects.filter(
+ created_at__gte=last_week
+ ).order_by("-created_at")[:5]
+
+ recent_staff_leaves = Staff_leave.objects.filter(
+ created_at__gte=last_week
+ ).order_by("-created_at")[:5]
+
+ # Pending Leave Requests
+ pending_student_leaves = Student_Leave.objects.filter(status=0)
+ pending_staff_leaves = Staff_leave.objects.filter(status=0).order_by("-created_at")
+
+ # Attendance Statistics
+ today_classes = Attendance.objects.filter(date=today)
+ total_classes = today_classes.count()
+ total_students_today = AttendanceRecord.objects.filter(attendance__in=today_classes).count()
+ students_present = AttendanceRecord.objects.filter(
+ attendance__in=today_classes, student_attend=True
+ ).count()
+
+ # Recent Feedback
+ recent_feedback = StudentFeedback.objects.order_by("-created_at")[:5]
+
+ # Recent Institute Feedback
+ recent_institute_feedback = InstituteFeedback.objects.filter(
+ is_public=True
+ ).order_by("-created_at")[:10]
+
+ # Get notices
+ notices = Notice.objects.all().order_by("-created_at")
+
+ # Get recent activities
+ recent_activities = []
+
+ # Add recent leaves
+ for leave in recent_student_leaves:
+ recent_activities.append(
+ {
+ "title": f"Student Leave Request",
+ "description": f"{leave.student.name} requested leave from {leave.start_date} to {leave.end_date}",
+ "timestamp": leave.created_at,
+ }
+ )
+
+ for leave in recent_staff_leaves:
+ recent_activities.append(
+ {
+ "title": f"Staff Leave Request",
+ "description": f"{leave.staff.name} requested leave from {leave.start_date} to {leave.end_date}",
+ "timestamp": leave.created_at,
+ }
+ )
+
+ # Add recent feedback
+ for feedback in recent_feedback:
+ recent_activities.append(
+ {
+ "title": f"New Feedback",
+ "description": f"Feedback received for {feedback.teacher.name if feedback.teacher else 'Institute'}",
+ "timestamp": feedback.created_at,
+ }
+ )
+
+ # Add recent institute feedback
+ for feedback in recent_institute_feedback:
+ recent_activities.append(
+ {
+ "title": f"Institute Feedback",
+ "description": f"New feedback from {feedback.display_name}",
+ "timestamp": feedback.created_at,
+ }
+ )
+
+ # Add recent notices
+ for notice in notices:
+ recent_activities.append(
+ {
+ "title": f"New Notice",
+ "description": notice.title,
+ "timestamp": notice.created_at,
+ }
+ )
+
+ # Sort all activities by timestamp
+ recent_activities.sort(key=lambda x: x["timestamp"], reverse=True)
+ # Take only the most recent 10 activities
+ recent_activities = recent_activities[:10]
+
+ # Teacher Ratings
+ teacher_ratings = []
+ teachers = Staff.objects.filter(
+ Q(designation__icontains="Teacher")
+ | Q(designation__icontains="Staff")
+ | Q(designation__icontains="Principal")
+ | Q(designation__isnull=True)
+ | Q(designation="")
+ )
+
+ for teacher in teachers:
+ feedback = StudentFeedback.objects.filter(teacher=teacher)
+ avg_rating = feedback.aggregate(Avg("rating"))["rating__avg"] or 0
+ total_reviews = feedback.count()
+
+ # Get total students taught by this teacher
+ total_students = (
+ AttendanceRecord.objects.filter(attendance__routine__teacher=teacher)
+ .values("student")
+ .distinct()
+ .count()
+ )
+
+ teacher_ratings.append(
+ {
+ "name": teacher.name,
+ "designation": teacher.designation or "Teacher",
+ "image": teacher.image,
+ "avg_rating": avg_rating,
+ "total_reviews": total_reviews,
+ "total_students": total_students,
+ }
+ )
+
+ # Sort teachers by average rating (highest first)
+ teacher_ratings.sort(key=lambda x: x["avg_rating"], reverse=True)
+
+ # Course Progress Overview
+ course_progress = []
+ for course in Course.objects.all():
+ trackings = course.student_trackings.all()
+ if trackings.exists():
+ avg_progress = trackings.aggregate(
+ avg_progress=Avg("completion_percentage")
+ )["avg_progress"]
+ course_progress.append(
+ {
+ "course": course,
+ "total_students": trackings.count(),
+ "in_progress": trackings.filter(
+ progress_status="In Progress"
+ ).count(),
+ "completed": trackings.filter(progress_status="Completed").count(),
+ "average_progress": round(avg_progress, 1) if avg_progress else 0,
+ }
+ )
+
+ # Monthly Statistics
+ monthly_stats = {
+ "new_students": Student.objects.filter(joining_date__gte=last_month).count(),
+ "new_staff": Staff.objects.filter(joining_date__gte=last_month).count(),
+ "attendance_rate": round(
+ (
+ AttendanceRecord.objects.filter(
+ attendance__date__gte=last_month.date(), student_attend=True
+ ).count()
+ / AttendanceRecord.objects.filter(
+ attendance__date__gte=last_month.date()
+ ).count()
+ * 100
+ )
+ if AttendanceRecord.objects.filter(
+ attendance__date__gte=last_month.date()
+ ).exists()
+ else 0
+ ),
+ "leave_requests": Student_Leave.objects.filter(
+ created_at__gte=last_month
+ ).count()
+ + Staff_leave.objects.filter(created_at__gte=last_month).count(),
+ }
+
+ context = {
+ **custom_admin_site.each_context(
+ request
+ ), # This adds the admin context, including the sidebar.
+ # Student Statistics
+ "total_students": total_students,
+ "active_students": active_students,
+ "completed_students": completed_students,
+ "on_leave_students": on_leave_students,
+ # Staff Statistics
+ "total_staff": total_staff,
+ "active_staff": active_staff,
+ "staff_on_leave": staff_on_leave,
+ # Course Statistics
+ "total_courses": total_courses,
+ "active_courses": active_courses,
+ "completed_courses": completed_courses,
+ # Course Tracking Statistics
+ "in_progress_trackings": in_progress_trackings.count(),
+ "completed_trackings": completed_trackings.count(),
+ "dropped_trackings": dropped_trackings.count(),
+ # Leave Requests
+ "recent_student_leaves": recent_student_leaves,
+ "recent_staff_leaves": recent_staff_leaves,
+ "pending_student_leaves": pending_student_leaves,
+ "pending_staff_leaves": pending_staff_leaves,
+ # Attendance Statistics
+ "total_classes_today": total_classes,
+ "total_students_today": total_students_today,
+ "students_present": students_present,
+ "attendance_rate": round(
+ (students_present / total_students_today * 100) if total_students_today > 0 else 0
+ ),
+ # Feedback
+ "recent_feedback": recent_feedback,
+ "recent_institute_feedback": recent_institute_feedback,
+ "teacher_ratings": teacher_ratings,
+ # Course Progress
+ "course_progress": course_progress,
+ # Monthly Statistics
+ "monthly_stats": monthly_stats,
+ "notices": notices, # Add notices to context
+ "recent_activities": recent_activities, # Add recent activities to context
+ }
+
+ return render(request, "dashboard.html", context)
+ else:
+ # If user doesn't belong to any specific group, try to determine user type
+ print("User doesn't belong to any specific group")
+ if hasattr(user, 'student'):
+ print("User has student attribute, redirecting to student dashboard")
+ return redirect("studentDashboard")
+ elif hasattr(user, 'staff'):
+ print("User has staff attribute, redirecting to teacher dashboard")
+ return redirect("teacherDashboard")
+ else:
+ print("Unknown user type, showing default dashboard")
+ return render(request, "dashboard.html", {"unknown_user_type": True})
+
+
+@login_required
+def update_profile(request):
+ if request.method == "POST":
+ user = request.user
+ user_groups = user.groups.values_list("name", flat=True)
+
+ # Handle profile image update
+ if "profile_image" in request.FILES:
+ try:
+ image = request.FILES["profile_image"]
+
+ if "Student" in user_groups:
+ student = Student.objects.get(id=user.id)
+ if student.image:
+ # Delete old image if updating
+ if os.path.isfile(student.image.path):
+ os.remove(student.image.path)
+ # Save new image
+ student.image = image
+ student.save()
+ messages.success(request, "Profile picture updated successfully.")
+
+ elif "Teacher" in user_groups:
+ staff = Staff.objects.get(id=user.id)
+ if staff.image:
+ # Delete old image if updating
+ if os.path.isfile(staff.image.path):
+ os.remove(staff.image.path)
+ # Save new image
+ staff.image = image
+ staff.save()
+ messages.success(request, "Profile picture updated successfully.")
+
+ else:
+ messages.error(request, "User profile not found. Please make sure you are logged in as a student or staff member.")
+
+ except (Student.DoesNotExist, Staff.DoesNotExist) as e:
+ messages.error(request, "User profile not found in the database.")
+ except Exception as e:
+ messages.error(request, f"Error updating profile picture: {str(e)}")
+ else:
+ messages.error(request, "No profile picture uploaded.")
+
+ # Redirect based on user type
+ if "Student" in user_groups:
+ return redirect("studentDashboard")
+ elif "Teacher" in user_groups:
+ return redirect("teacherDashboard")
+ else:
+ return redirect("dashboard")
+
+ return redirect("dashboard")
+
+
+@login_required
+def change_password(request):
+ if request.method == "POST":
+ user = request.user
+ current_password = request.POST.get("current_password")
+ new_password = request.POST.get("new_password")
+ confirm_password = request.POST.get("confirm_password")
+
+ # Check if this is an AJAX request
+ is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
+
+ # Validate input
+ if not all([current_password, new_password, confirm_password]):
+ if is_ajax:
+ return JsonResponse({"success": False, "error": "All password fields are required."})
+ messages.error(request, "All password fields are required.")
+ return redirect("studentDashboard")
+
+ if new_password != confirm_password:
+ if is_ajax:
+ return JsonResponse({"success": False, "error": "New passwords do not match."})
+ messages.error(request, "New passwords do not match.")
+ return redirect("studentDashboard")
+
+ # Check current password
+ if not user.check_password(current_password):
+ if is_ajax:
+ return JsonResponse({"success": False, "error": "Current password is incorrect."})
+ messages.error(request, "Current password is incorrect.")
+ return redirect("studentDashboard")
+
+ # Set new password
+ try:
+ user.set_password(new_password)
+ user.save()
+ if is_ajax:
+ return JsonResponse({"success": True})
+
+ messages.success(
+ request, "Password changed successfully. Please login again."
+ )
+ return redirect("login")
+ except Exception as e:
+ if is_ajax:
+ return JsonResponse({"success": False, "error": f"Error changing password: {str(e)}"})
+ messages.error(request, f"Error changing password: {str(e)}")
+
+ # Redirect based on user type
+ if hasattr(user, "student"):
+ return redirect("studentDashboard")
+ elif hasattr(user, "staff"):
+ return redirect("teacherDashboard")
+ else:
+ return redirect("dashboard")
+
+ return redirect("dashboard")
+
+
+@login_required
+@require_GET
+def get_subjects(request):
+ """Get subjects for a course and semester"""
+ course_id = request.GET.get("course_id")
+ semester = request.GET.get("semester", 1)
+
+ if not course_id:
+ return JsonResponse({"success": False, "message": "Course ID is required"})
+
+ try:
+ subjects = Subject.objects.filter(
+ course_id=course_id, semester_or_year=semester
+ ).values("id", "name")
+
+ return JsonResponse({"success": True, "subjects": list(subjects)})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)})
+
+
+@login_required
+@require_GET
+def get_teachers(request):
+ """Get teachers for the institute"""
+ try:
+ teachers = Staff.objects.filter(
+ Q(designation__icontains="Teacher")
+ | Q(designation__icontains="Staff")
+ | Q(designation__icontains="Principal")
+ | Q(designation__isnull=True)
+ | Q(designation="")
+ ).values("id", "name")
+
+ return JsonResponse({"success": True, "teachers": list(teachers)})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)})
+
+
+@login_required
+@require_GET
+def get_course_duration(request):
+ """Get course duration details"""
+ course_id = request.GET.get("course_id")
+
+ if not course_id:
+ return JsonResponse({"success": False, "message": "Course ID is required"})
+
+ try:
+ course = Course.objects.get(id=course_id)
+
+ # Calculate number of semesters or years
+ total_periods = course.duration
+ if course.duration_type == "Semester":
+ total_periods = course.duration * 2 # 2 semesters per year
+
+ # Create a list of periods
+ periods = []
+ for i in range(1, total_periods + 1):
+ if course.duration_type == "Semester":
+ periods.append({"value": i, "label": f"Semester {i}"})
+ else:
+ periods.append({"value": i, "label": f"Year {i}"})
+
+ return JsonResponse(
+ {
+ "success": True,
+ "course": {
+ "id": course.id,
+ "name": course.name,
+ "duration": course.duration,
+ "duration_type": course.duration_type,
+ "total_periods": total_periods,
+ "periods": periods,
+ },
+ }
+ )
+ except Course.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Course not found"})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)})
+
+
+@login_required
+def approve_student_leave(request, leave_id):
+ """View to approve a student leave request"""
+ if request.method == "POST":
+ leave = get_object_or_404(Student_Leave, id=leave_id)
+ leave.status = 1 # Approved
+ leave.save()
+ messages.success(
+ request, f"Leave request for {leave.student.name} has been approved."
+ )
+ return redirect("dashboard")
+
+
+@login_required
+def reject_student_leave(request, leave_id):
+ """View to reject a student leave request"""
+ if request.method == "POST":
+ leave = get_object_or_404(Student_Leave, id=leave_id)
+ leave.status = 2 # Rejected
+ leave.save()
+ messages.warning(
+ request, f"Leave request for {leave.student.name} has been rejected."
+ )
+ return redirect("dashboard")
+
+
+@login_required
+def approve_staff_leave(request, leave_id):
+ """View to approve a staff leave request"""
+ if request.method == "POST":
+ leave = get_object_or_404(Staff_leave, id=leave_id)
+ leave.status = 1 # Approved
+ leave.save()
+ messages.success(
+ request, f"Leave request for {leave.staff.name} has been approved."
+ )
+ return redirect("dashboard")
+
+
+@login_required
+def reject_staff_leave(request, leave_id):
+ """View to reject a staff leave request"""
+ if request.method == "POST":
+ leave = get_object_or_404(Staff_leave, id=leave_id)
+ leave.status = 2 # Rejected
+ leave.save()
+ messages.warning(
+ request, f"Leave request for {leave.staff.name} has been rejected."
+ )
+ return redirect("dashboard")
+
+
+@login_required
+def add_notice(request):
+ if request.method == "POST":
+ title = request.POST.get("title")
+ message = request.POST.get("message")
+ image = request.FILES.get("image")
+
+ if title and message:
+ notice = Notice.objects.create(title=title, message=message, image=image)
+ messages.success(request, "Notice added successfully.")
+ else:
+ messages.error(request, "Title and message are required.")
+
+ return redirect("dashboard")
+
+
+@login_required
+def delete_notice(request, notice_id):
+ if request.method == "POST":
+ try:
+ notice = Notice.objects.get(id=notice_id)
+ notice.delete()
+ return JsonResponse({"success": True})
+ except Notice.DoesNotExist:
+ return JsonResponse({"success": False, "error": "Notice not found"})
+
+ return JsonResponse({"success": False, "error": "Invalid request method"})
+
+
+@login_required
+def student_dashboard(request):
+ student = request.user.student
+ today = timezone.now().date()
+
+ # Get attendance summary
+ attendance_records = AttendanceRecord.objects.filter(student=student)
+ total_classes = attendance_records.count()
+ attended_classes = attendance_records.filter(student_attend=True).count()
+ attendance_percentage = (attended_classes / total_classes * 100) if total_classes > 0 else 0
+
+ # Calculate current streak
+ current_streak = 0
+ longest_streak = 0
+ temp_streak = 1
+
+ for record in attendance_records.order_by('-attendance__date'):
+ if record.student_attend:
+ current_streak += 1
+ temp_streak += 1
+ longest_streak = max(longest_streak, temp_streak)
+ else:
+ break
+
+ attendance_summary = {
+ 'total_classes': total_classes,
+ 'attendance_percentage': attendance_percentage,
+ 'current_streak': current_streak,
+ 'longest_streak': longest_streak
+ }
+
+ # Get course tracking summary
+ course_trackings = CourseTracking.objects.filter(student=student)
+ course_summary = {
+ 'total_courses': course_trackings.count(),
+ 'completed_courses': course_trackings.filter(progress_status='Completed').count(),
+ 'ongoing_courses': course_trackings.filter(progress_status='In Progress').count(),
+ 'completion_percentage': (
+ sum(tracking.completion_percentage for tracking in course_trackings) / course_trackings.count()
+ if course_trackings.exists() else 0
+ )
+ }
+
+ # Get recent leaves
+ recent_leaves = Student_Leave.objects.filter(student=student).order_by('-created_at')[:5]
+
+ # Get recent feedback
+ recent_feedback = StudentFeedback.objects.filter(student=student).order_by('-created_at')[:5]
+
+ # Get notices
+ notices = Notice.objects.all().order_by('-created_at')[:5]
+
+ # Get attendance records
+ attendance_records = AttendanceRecord.objects.filter(student=student).order_by('-attendance__date')[:5]
+
+ # Get leave counts
+ pending_leaves = Student_Leave.objects.filter(student=student, status=0).count()
+ approved_leaves = Student_Leave.objects.filter(student=student, status=1).count()
+ rejected_leaves = Student_Leave.objects.filter(student=student, status=2).count()
+
+ # Get course details from course tracking
+ course_details = []
+ for tracking in course_trackings:
+ course_info = {
+ 'name': tracking.course.name,
+ 'start_date': tracking.start_date,
+ 'duration': tracking.course.duration,
+ 'duration_type': tracking.course.duration_type,
+ 'current_semester': tracking.current_semester,
+ 'progress_status': tracking.progress_status,
+ 'completion_percentage': tracking.completion_percentage,
+ 'semester_start_date': tracking.semester_start_date,
+ 'semester_end_date': tracking.semester_end_date,
+ 'expected_end_date': tracking.expected_end_date,
+ 'actual_end_date': tracking.actual_end_date,
+ 'remaining_days': tracking.remaining_days,
+ 'total_remaining_days': tracking.total_remaining_days
+ }
+ course_details.append(course_info)
+
+ context = {
+ 'student': student,
+ 'attendance_summary': attendance_summary,
+ 'course_summary': course_summary,
+ 'recent_leaves': recent_leaves,
+ 'recent_feedback': recent_feedback,
+ 'notices': notices,
+ 'attendance_records': attendance_records,
+ 'pending_leaves': pending_leaves,
+ 'approved_leaves': approved_leaves,
+ 'rejected_leaves': rejected_leaves,
+ 'course_details': course_details,
+ }
+ return render(request, 'student_dashboard.html', context)
+
+@login_required
+def teacher_dashboard(request):
+ teacher = request.user.teacher
+ today = timezone.now().date()
+
+ # Get today's routines
+ today_routines = Routine.objects.filter(
+ teacher=teacher,
+ day_of_week=today.weekday()
+ ).order_by('start_time')
+
+ # Get attendance summary
+ attendance_records = Attendance.objects.filter(routine__teacher=teacher)
+ total_classes = attendance_records.count()
+ total_students = AttendanceRecord.objects.filter(attendance__routine__teacher=teacher).count()
+ present_students = AttendanceRecord.objects.filter(
+ attendance__routine__teacher=teacher,
+ student_attend=True
+ ).count()
+
+ attendance_summary = {
+ 'total_classes': total_classes,
+ 'total_students': total_students,
+ 'present_students': present_students,
+ 'attendance_percentage': (present_students / total_students * 100) if total_students > 0 else 0
+ }
+
+ # Get recent leaves
+ recent_leaves = Student_Leave.objects.filter(
+ Q(status=0) | Q(status=1)
+ ).order_by('-created_at')[:5]
+
+ # Get recent feedback
+ recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by('-created_at')[:5]
+
+ # Get notices
+ notices = Notice.objects.all().order_by('-created_at')[:5]
+
+ # Get attendance records
+ attendance_records = Attendance.objects.filter(routine__teacher=teacher).order_by('-date')[:5]
+
+ # Get leave counts
+ pending_leaves = Student_Leave.objects.filter(status=0).count()
+ approved_leaves = Student_Leave.objects.filter(status=1).count()
+ rejected_leaves = Student_Leave.objects.filter(status=2).count()
+
+ context = {
+ 'teacher': teacher,
+ 'today_routines': today_routines,
+ 'attendance_summary': attendance_summary,
+ 'recent_leaves': recent_leaves,
+ 'recent_feedback': recent_feedback,
+ 'notices': notices,
+ 'attendance_records': attendance_records,
+ 'pending_leaves': pending_leaves,
+ 'approved_leaves': approved_leaves,
+ 'rejected_leaves': rejected_leaves,
+ }
+ return render(request, 'teacher_dashboard.html', context)
+
+@login_required
+@require_POST
+def save_fcm_token(request):
+ try:
+ data = json.loads(request.body)
+ token = data.get('token')
+
+ if not token:
+ return JsonResponse({'error': 'Token is required'}, status=400)
+
+ user = request.user
+ user_groups = user.groups.values_list("name", flat=True)
+
+ # Save token to the appropriate user model
+ if "Student" in user_groups:
+ user.fcm_token = token
+ user.save()
+ elif "Teacher" in user_groups:
+ user.fcm_token = token
+ user.save()
+
+ # Update or create FCMDevice
+ device, created = FCMDevice.objects.update_or_create(
+ token=token,
+ defaults={'is_active': True}
+ )
+
+ return JsonResponse({
+ 'status': 'success',
+ 'device_id': device.id,
+ 'created': created
+ })
+ except json.JSONDecodeError:
+ return JsonResponse({'error': 'Invalid JSON'}, status=400)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
+
+def serve_firebase_sw(request):
+ """Serve Firebase service worker with correct MIME type"""
+
+ # Get the path to the service worker in the static directory
+ sw_path = os.path.join(settings.STATICFILES_DIRS[0], 'firebase-messaging-sw.js')
+
+ # If the file doesn't exist in static directory, try staticfiles
+ if not os.path.exists(sw_path):
+ sw_path = os.path.join(settings.STATIC_ROOT, 'firebase-messaging-sw.js')
+
+ response = serve(request, 'firebase-messaging-sw.js', os.path.dirname(sw_path))
+ response['Content-Type'] = 'application/javascript'
+ return response
+
+@login_required
+def check_weak_password(request):
+ """Check if the current user's password is '123'"""
+ user = request.user
+ is_weak = check_password('123', user.password)
+ return JsonResponse({'is_weak': is_weak})
diff --git a/assets/logo.png b/assets/logo.png
deleted file mode 100644
index 05e9f69..0000000
Binary files a/assets/logo.png and /dev/null differ
diff --git a/requirements.txt b/requirements.txt
index 4fa5351..4908d29 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,45 @@
asgiref==3.8.1
+CacheControl==0.14.2
+cachetools==5.5.2
+certifi==2025.1.31
+cffi==1.17.1
+charset-normalizer==3.4.1
+cryptography==44.0.2
Django==5.1.8
django-jazzmin==3.0.1
+django-mathfilters==1.0.0
+django-rest-framework==0.1.0
+djangorestframework==3.15.2
+firebase-admin==6.7.0
+google-api-core==2.24.2
+google-api-python-client==2.165.0
+google-auth==2.38.0
+google-auth-httplib2==0.2.0
+google-cloud-core==2.4.3
+google-cloud-firestore==2.20.1
+google-cloud-storage==3.1.0
+google-crc32c==1.7.1
+google-resumable-media==2.7.2
+googleapis-common-protos==1.69.2
+grpcio==1.71.0
+grpcio-status==1.71.0
+httplib2==0.22.0
+idna==3.10
+msgpack==1.1.0
+pillow==11.1.0
+proto-plus==1.26.1
+protobuf==5.29.4
+pyasn1==0.6.1
+pyasn1_modules==0.4.1
+pycparser==2.22
+PyJWT==2.10.1
+pyparsing==3.2.3
+python-dotenv==1.0.1
pytz==2025.2
-setuptools==78.1.0
-sqlparse==0.5.3
-typing_extensions==4.12.2
requests==2.32.3
+rsa==4.9
+sqlparse==0.5.3
+uritemplate==4.1.1
+urllib3==2.3.0
+whitenoise==6.9.0
+pyotp==2.9.0
diff --git a/static/css/base.css b/static/css/base.css
new file mode 100644
index 0000000..38715b4
--- /dev/null
+++ b/static/css/base.css
@@ -0,0 +1,577 @@
+/* Base styles */
+:root {
+ /* Modern Color Palette */
+ --primary: #4F46E5; /* Indigo 600 */
+ --primary-light: #818CF8;
+ --primary-dark: #4338CA;
+ --secondary: #64748B; /* Slate 500 */
+ --success: #10B981; /* Emerald 500 */
+ --warning: #F59E0B; /* Amber 500 */
+ --danger: #EF4444; /* Red 500 */
+ --info: #3B82F6; /* Blue 500 */
+
+ /* Neutral Colors */
+ --background: #F8FAFC; /* Slate 50 */
+ --card-bg: #FFFFFF;
+ --text-primary: #1E293B; /* Slate 800 */
+ --text-secondary: #64748B; /* Slate 500 */
+ --border-color: #E2E8F0; /* Slate 200 */
+ --hover-bg: #F1F5F9; /* Slate 100 */
+
+ /* Shadows */
+ --card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ --hover-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+
+ /* Transitions */
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+
+ /* Border Radius */
+ --radius-sm: 0.375rem;
+ --radius-md: 0.5rem;
+ --radius-lg: 0.75rem;
+ --radius-xl: 1rem;
+}
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ line-height: 1.6;
+ color: var(--text-primary);
+ background-color: var(--background);
+ margin: 0;
+ padding: 0;
+}
+
+.container {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+
+/* Common utility classes */
+.text-center {
+ text-align: center;
+}
+
+.mt-3 {
+ margin-top: 1rem;
+}
+
+.mb-3 {
+ margin-bottom: 1rem;
+}
+
+.py-3 {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+
+/* Animations */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.fade-in {
+ animation: fadeIn 0.3s ease-out;
+}
+
+/* Status Badges */
+.status-badge {
+ padding: 0.25rem 0.75rem;
+ border-radius: var(--radius-md);
+ font-size: 0.75rem;
+ font-weight: 500;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.bg-success, .completed, .leave-approved {
+ background-color: var(--success);
+ color: white;
+}
+
+.bg-warning, .ongoing, .leave-pending {
+ background-color: var(--warning);
+ color: white;
+}
+
+.bg-danger, .leave-rejected {
+ background-color: var(--danger);
+ color: white;
+}
+
+.bg-info, .upcoming {
+ background-color: var(--info);
+ color: white;
+}
+
+/* Form styles */
+.form-group {
+ margin-bottom: 1rem;
+}
+
+.form-control {
+ padding: 0.625rem 0.875rem;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-color);
+ transition: var(--transition);
+ font-size: 0.875rem;
+}
+
+.form-control:focus {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
+}
+
+/* Button styles */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 500;
+ padding: 0.5rem 1rem;
+ border-radius: var(--radius-md);
+ transition: var(--transition);
+ gap: 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.btn-primary {
+ background-color: var(--primary);
+ color: white;
+ border: none;
+}
+
+.btn-primary:hover {
+ background-color: var(--primary-dark);
+ transform: translateY(-1px);
+}
+
+/* Modern Card Styles */
+.content-card {
+ background-color: var(--card-bg);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--card-shadow);
+ padding: 1.5rem;
+ margin-bottom: 1.5rem;
+ transition: var(--transition);
+ border: 1px solid var(--border-color);
+}
+
+.content-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--hover-shadow);
+}
+
+/* Print Styles */
+@media print {
+ .btn-primary,
+ .modal,
+ .modal-backdrop {
+ display: none !important;
+ }
+
+ .content-card {
+ box-shadow: none;
+ border: 1px solid #ddd;
+ }
+}
+
+/* Tables */
+.table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+}
+
+.table th {
+ background-color: var(--hover-bg);
+ padding: 1rem;
+ font-weight: 600;
+ text-align: left;
+ color: var(--text-primary);
+ border-bottom: 2px solid var(--border-color);
+}
+
+.table td {
+ padding: 1rem;
+ border-bottom: 1px solid var(--border-color);
+ color: var(--text-secondary);
+}
+
+.table tr:hover td {
+ background-color: var(--hover-bg);
+}
+
+/* Responsive Design */
+@media (max-width: 1200px) {
+ .container {
+ padding-right: 12px;
+ padding-left: 12px;
+ }
+
+ .content-card {
+ padding: 1.25rem;
+ }
+}
+
+@media (max-width: 992px) {
+ .container {
+ padding-right: 10px;
+ padding-left: 10px;
+ }
+
+ .content-card {
+ padding: 1rem;
+ }
+
+ .btn {
+ padding: 0.4rem 0.8rem;
+ font-size: 0.8rem;
+ }
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding-right: 8px;
+ padding-left: 8px;
+ }
+
+ .content-card {
+ padding: 0.875rem;
+ margin-bottom: 1rem;
+ }
+
+ .form-control {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8rem;
+ }
+
+ .table th,
+ .table td {
+ padding: 0.75rem;
+ font-size: 0.875rem;
+ }
+
+ .status-badge {
+ padding: 0.2rem 0.5rem;
+ font-size: 0.7rem;
+ }
+}
+
+@media (max-width: 576px) {
+ .container {
+ padding-right: 6px;
+ padding-left: 6px;
+ }
+
+ .content-card {
+ padding: 0.75rem;
+ }
+
+ .btn {
+ padding: 0.35rem 0.7rem;
+ font-size: 0.75rem;
+ }
+
+ .form-control {
+ padding: 0.4rem 0.6rem;
+ font-size: 0.75rem;
+ }
+
+ .table {
+ display: block;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+
+ .table th,
+ .table td {
+ padding: 0.5rem;
+ font-size: 0.8rem;
+ }
+}
+
+@media (max-width: 375px) {
+ .container {
+ padding-right: 4px;
+ padding-left: 4px;
+ }
+
+ .content-card {
+ padding: 0.625rem;
+ }
+
+ .btn {
+ padding: 0.3rem 0.6rem;
+ font-size: 0.7rem;
+ }
+
+ .form-control {
+ padding: 0.35rem 0.5rem;
+ font-size: 0.7rem;
+ }
+
+ .table th,
+ .table td {
+ padding: 0.4rem;
+ font-size: 0.75rem;
+ }
+
+ .status-badge {
+ padding: 0.15rem 0.4rem;
+ font-size: 0.65rem;
+ }
+}
+
+@media (max-width: 320px) {
+ .container {
+ padding-right: 3px;
+ padding-left: 3px;
+ }
+
+ .content-card {
+ padding: 0.5rem;
+ }
+
+ .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.65rem;
+ }
+
+ .form-control {
+ padding: 0.3rem 0.4rem;
+ font-size: 0.65rem;
+ }
+
+ .table th,
+ .table td {
+ padding: 0.3rem;
+ font-size: 0.7rem;
+ }
+
+ .status-badge {
+ padding: 0.1rem 0.3rem;
+ font-size: 0.6rem;
+ }
+}
+
+/* Enhanced Responsive Styles */
+/* Mobile-first approach for better performance */
+
+/* Responsive layout containers */
+.container {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+
+/* Responsive tables */
+.table-responsive-sm {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+/* Table adjustments for small screens */
+@media (max-width: 576px) {
+ .table th,
+ .table td {
+ padding: 0.5rem 0.25rem;
+ font-size: 0.75rem;
+ }
+
+ .table-responsive-sm {
+ border: 0;
+ }
+}
+
+/* Responsive form controls */
+.form-control {
+ width: 100%;
+}
+
+/* Responsive card adjustments */
+@media (max-width: 576px) {
+ .card {
+ margin-bottom: 0.75rem;
+ }
+
+ .card-body {
+ padding: 0.75rem;
+ }
+
+ .card-title {
+ font-size: 1.1rem;
+ margin-bottom: 0.5rem;
+ }
+}
+
+/* Responsive spacing utilities */
+@media (max-width: 576px) {
+ .px-sm-1 { padding-left: 0.25rem !important; padding-right: 0.25rem !important; }
+ .py-sm-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; }
+ .p-sm-1 { padding: 0.25rem !important; }
+ .mx-sm-1 { margin-left: 0.25rem !important; margin-right: 0.25rem !important; }
+ .my-sm-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; }
+ .m-sm-1 { margin: 0.25rem !important; }
+}
+
+/* Adjustments for modals on small screens */
+@media (max-width: 576px) {
+ .modal-dialog {
+ margin: 0.5rem;
+ max-width: calc(100% - 1rem);
+ }
+
+ .modal-body {
+ padding: 0.75rem;
+ }
+
+ .modal-footer {
+ padding: 0.5rem;
+ }
+
+ .modal-header {
+ padding: 0.75rem;
+ }
+}
+
+/* Responsive image sizing */
+.img-fluid {
+ max-width: 100%;
+ height: auto;
+}
+
+/* Responsive flex utilities */
+.d-sm-flex {
+ display: flex !important;
+}
+
+@media (max-width: 576px) {
+ .flex-sm-column {
+ flex-direction: column !important;
+ }
+
+ .justify-content-sm-center {
+ justify-content: center !important;
+ }
+
+ .align-items-sm-center {
+ align-items: center !important;
+ }
+}
+
+/* Enhanced responsive grid system */
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: -15px;
+ margin-left: -15px;
+}
+
+@media (max-width: 576px) {
+ .row {
+ margin-right: -10px;
+ margin-left: -10px;
+ }
+
+ [class*="col-"] {
+ padding-right: 10px;
+ padding-left: 10px;
+ }
+}
+
+/* Responsive utilities for text */
+@media (max-width: 576px) {
+ .text-sm-center {
+ text-align: center !important;
+ }
+
+ .text-sm-left {
+ text-align: left !important;
+ }
+
+ .text-sm-right {
+ text-align: right !important;
+ }
+}
+
+/* Print styles */
+@media print {
+ .no-print {
+ display: none !important;
+ }
+
+ .container {
+ width: 100% !important;
+ max-width: none !important;
+ }
+
+ body {
+ font-size: 12pt;
+ }
+
+ a[href]:after {
+ content: " (" attr(href) ")";
+ }
+}
\ No newline at end of file
diff --git a/static/css/custom.css b/static/css/custom.css
new file mode 100644
index 0000000..5ee0c89
--- /dev/null
+++ b/static/css/custom.css
@@ -0,0 +1,22 @@
+/* Target the specific logo image */
+img[src*="/static/img/logo.png"] {
+ max-height: 100px !important;
+ width: auto !important;
+}
+
+/* Target login page logo */
+.jazzmin-login-page .login-logo img {
+ max-height: 100px !important;
+ width: auto !important;
+}
+
+/* Target admin interface logo */
+.brand-link img.img-circle {
+ max-height: 33px !important;
+ width: auto !important;
+ opacity: .8;
+ margin-left: .8rem;
+ margin-right: .5rem;
+ margin-top: -3px;
+}
+
diff --git a/static/css/dashboard.css b/static/css/dashboard.css
new file mode 100644
index 0000000..7990fc4
--- /dev/null
+++ b/static/css/dashboard.css
@@ -0,0 +1,1155 @@
+/* Modern Dashboard Styles */
+:root {
+ /* Color Palette */
+ --primary: #4F46E5;
+ --primary-dark: #4338CA;
+ --secondary: #6B7280;
+ --success: #10B981;
+ --warning: #F59E0B;
+ --danger: #EF4444;
+ --info: #3B82F6;
+
+ /* Neutral Colors */
+ --background: #F9FAFB;
+ --card-bg: #FFFFFF;
+ --text-primary: #111827;
+ --text-secondary: #6B7280;
+ --border-color: #E5E7EB;
+ --hover-bg: #F3F4F6;
+
+ /* Effects */
+ --card-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1);
+ --hover-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
+ --transition: all 0.2s ease;
+ --radius-sm: 0.375rem;
+ --radius-md: 0.5rem;
+ --radius-lg: 0.75rem;
+}
+
+/* Reset body padding since we removed the sidebar */
+body {
+ padding-left: 0 !important;
+ padding-top: 0 !important;
+}
+
+/* Base Layout Styles */
+body {
+ background-color: var(--background);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ min-height: 100vh;
+ padding-bottom: 80px;
+ position: relative;
+ color: var(--text-primary);
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+/* Bottom Navigation */
+.bottom-nav {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: var(--card-bg);
+ box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border-top: 1px solid var(--border-color);
+ padding: 0.5rem 0;
+}
+
+.nav-container {
+ display: flex;
+ overflow-x: auto;
+ padding: 0.5rem 1rem;
+ justify-content: space-around;
+ -webkit-overflow-scrolling: touch;
+ gap: 1rem;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.nav-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-secondary);
+ text-decoration: none;
+ padding: 0.75rem 1rem;
+ transition: var(--transition);
+ border-radius: var(--radius-md);
+ min-width: 100px;
+ position: relative;
+ text-align: center;
+ flex: 1;
+}
+
+.nav-item i {
+ font-size: 1.5rem;
+ margin-bottom: 0.5rem;
+ transition: var(--transition);
+}
+
+.nav-item span {
+ white-space: nowrap;
+ font-size: 0.85rem;
+ line-height: 1.2;
+ font-weight: 500;
+ display: block;
+ margin-top: 0.25rem;
+}
+
+.nav-item.active {
+ color: var(--primary);
+ background-color: var(--hover-bg);
+}
+
+.nav-item.active::after {
+ content: '';
+ position: absolute;
+ bottom: -0.25rem;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 4px;
+ height: 4px;
+ background-color: var(--primary);
+ border-radius: 50%;
+}
+
+.nav-item:hover:not(.active) {
+ color: var(--primary);
+ background-color: var(--hover-bg);
+}
+
+/* Large Desktops */
+@media (min-width: 1200px) {
+ .nav-item {
+ min-width: 120px;
+ padding: 0.85rem 1.25rem;
+ }
+
+ .nav-item i {
+ font-size: 1.75rem;
+ }
+
+ .nav-item span {
+ font-size: 0.95rem;
+ }
+}
+
+/* Desktop */
+@media (max-width: 1199px) {
+ .nav-item {
+ min-width: 110px;
+ padding: 0.75rem 1rem;
+ }
+
+ .nav-item i {
+ font-size: 1.5rem;
+ }
+
+ .nav-item span {
+ font-size: 0.85rem;
+ }
+}
+
+/* Tablets */
+@media (max-width: 768px) {
+ .nav-container {
+ padding: 0.5rem;
+ gap: 0.5rem;
+ }
+
+ .nav-item {
+ min-width: 90px;
+ padding: 0.6rem 0.75rem;
+ }
+
+ .nav-item i {
+ font-size: 1.25rem;
+ margin-bottom: 0.35rem;
+ }
+
+ .nav-item span {
+ font-size: 0.75rem;
+ }
+}
+
+/* Mobile Landscape */
+@media (max-width: 576px) {
+ .nav-container {
+ padding: 0.4rem;
+ gap: 0.35rem;
+ }
+
+ .nav-item {
+ min-width: 75px;
+ padding: 0.5rem 0.5rem;
+ }
+
+ .nav-item i {
+ font-size: 1.1rem;
+ margin-bottom: 0.25rem;
+ }
+
+ .nav-item span {
+ font-size: 0.7rem;
+ }
+}
+
+/* Mobile Portrait */
+@media (max-width: 375px) {
+ .nav-container {
+ padding: 0.3rem;
+ gap: 0.25rem;
+ }
+
+ .nav-item {
+ min-width: 65px;
+ padding: 0.4rem;
+ }
+
+ .nav-item i {
+ font-size: 1rem;
+ margin-bottom: 0.2rem;
+ }
+
+ .nav-item span {
+ font-size: 0.65rem;
+ font-weight: 400;
+ }
+}
+
+/* Summary Cards */
+.summary-card {
+ background: var(--card-bg);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--border-color);
+ transition: var(--transition);
+ overflow: hidden;
+}
+
+.summary-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--hover-shadow);
+}
+
+.summary-card .card-header {
+ background-color: var(--hover-bg);
+ padding: 1rem 1.25rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.summary-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;
+}
+
+.summary-card .card-body {
+ padding: 1.25rem;
+}
+
+/* Progress Bars */
+.progress {
+ height: 0.75rem;
+ background-color: var(--hover-bg);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+}
+
+.progress-bar {
+ border-radius: var(--radius-md);
+ transition: var(--transition);
+}
+
+/* Welcome Section */
+.welcome-section {
+ background: linear-gradient(135deg, #4F46E5, #3B82F6) !important;
+ color: white !important;
+ padding: 2rem;
+ border-radius: var(--radius-lg);
+ margin-bottom: 2rem;
+ position: relative;
+ overflow: hidden;
+ box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.1), 0 2px 4px -1px rgba(79, 70, 229, 0.06);
+}
+
+.welcome-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(45deg, rgba(255,255,255,0.1) 25%, transparent 25%),
+ linear-gradient(-45deg, rgba(255,255,255,0.1) 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.1) 75%),
+ linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.1) 75%);
+ background-size: 20px 20px;
+ opacity: 0.1;
+}
+
+.welcome-section .profile-picture-container.profile-sm {
+ width: 80px;
+ height: 80px;
+ margin-right: 1.5rem;
+ border-radius: 50%;
+ overflow: hidden;
+ border: 3px solid rgba(255, 255, 255, 0.2);
+}
+
+.welcome-section .profile-picture {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.welcome-section h2 {
+ font-size: 1.75rem;
+ margin-bottom: 0.5rem;
+ color: white;
+}
+
+.welcome-section .text-muted {
+ color: rgba(255, 255, 255, 0.9) !important;
+}
+
+.welcome-section .badge {
+ font-size: 0.8rem;
+ padding: 0.35rem 0.65rem;
+}
+
+.welcome-section .logout-btn {
+ background-color: rgba(255, 255, 255, 0.1);
+ color: white;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: var(--radius-md);
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ text-decoration: none;
+}
+
+.welcome-section .logout-btn:hover {
+ background-color: rgba(255, 255, 255, 0.2);
+ transform: translateY(-1px);
+}
+
+/* Activity Scroll */
+.activity-scroll {
+ max-height: 300px;
+ overflow-y: auto;
+ padding-right: 0.5rem;
+ scrollbar-width: thin;
+ scrollbar-color: var(--text-secondary) var(--hover-bg);
+}
+
+.activity-scroll::-webkit-scrollbar {
+ width: 4px;
+}
+
+.activity-scroll::-webkit-scrollbar-track {
+ background: var(--hover-bg);
+ border-radius: var(--radius-md);
+}
+
+.activity-scroll::-webkit-scrollbar-thumb {
+ background: var(--text-secondary);
+ border-radius: var(--radius-md);
+}
+
+.activity-scroll::-webkit-scrollbar-thumb:hover {
+ background: var(--text-primary);
+}
+
+/* Feedback Cards */
+.feedback-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.feedback-card {
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ padding: 1rem;
+ border: 1px solid #e0e0e0;
+ margin-bottom: 1rem;
+}
+
+.feedback-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+}
+
+.user-info {
+ display: flex;
+ gap: 1rem;
+ flex: 1;
+}
+
+.student-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--hover-bg);
+ flex-shrink: 0;
+}
+
+.student-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.student-avatar i {
+ font-size: 1.5rem;
+ color: var(--text-secondary);
+}
+
+.feedback-content {
+ flex: 1;
+ min-width: 0; /* Prevents text overflow */
+}
+
+.rating-stars {
+ color: var(--warning);
+ font-size: 1rem;
+ flex-shrink: 0;
+ padding-top: 0.25rem;
+}
+
+.rating-stars i {
+ margin-right: 2px;
+}
+
+.feedback-message {
+ color: #333;
+ font-size: 0.9rem;
+ line-height: 1.5;
+ margin-top: 0.5rem;
+ background: var(--hover-bg);
+ padding: 0.5rem;
+ border-radius: 6px;
+}
+
+.feedback-message p {
+ margin: 0;
+ word-wrap: break-word;
+}
+
+.empty-state {
+ text-align: center;
+ padding: 2rem;
+ color: var(--text-secondary);
+ background: var(--hover-bg);
+ border-radius: 8px;
+ margin: 1rem 0;
+}
+
+.empty-state i {
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ display: block;
+ color: var(--text-secondary);
+}
+
+.empty-state p {
+ margin: 0;
+ font-size: 0.9rem;
+}
+
+/* Rating Summary Styles */
+.rating-summary {
+ text-align: right;
+}
+
+.rating-stats {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.rating-number {
+ font-size: 2rem;
+ font-weight: bold;
+ color: var(--primary);
+}
+
+.rating-count {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+}
+
+/* Status Badge Styles */
+.status-badge {
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ font-weight: 500;
+}
+
+.status-badge.completed {
+ background-color: var(--success);
+ color: white;
+}
+
+.status-badge.ongoing {
+ background-color: var(--warning);
+ color: #000;
+}
+
+.status-badge.upcoming {
+ background-color: var(--info);
+ color: white;
+}
+
+/* Leave Badge Styles */
+.leave-pending {
+ background-color: var(--warning);
+ color: #000;
+}
+
+.leave-approved {
+ background-color: var(--success);
+ color: white;
+}
+
+.leave-rejected {
+ background-color: var(--danger);
+ color: white;
+}
+
+/* Content Card Styles */
+.content-card {
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem;
+ margin-bottom: 1.5rem;
+}
+
+/* Special style for notice card to use full height */
+.notice-section .content-card {
+ height: 100%; /* Take full height of parent */
+ display: flex;
+ flex-direction: column;
+}
+
+.notice-section .content-card h5 {
+ margin-bottom: 1rem;
+ flex-shrink: 0;
+}
+
+.notice-section .list-group {
+ flex-grow: 1; /* Take remaining space */
+ height: auto; /* Let flex handle the height */
+ min-height: 400px; /* Minimum height */
+ margin-bottom: 0;
+}
+
+/* Summary Card Styles */
+.summary-card {
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+}
+
+.summary-card .card-header {
+ background: var(--hover-bg);
+ border-bottom: 1px solid #e0e0e0;
+ padding: 1rem;
+}
+
+.summary-card .card-body {
+ padding: 1rem;
+}
+
+/* Progress Bars */
+.progress {
+ height: 12px;
+ background-color: #E8EAF6;
+ border-radius: 24px;
+ overflow: hidden;
+}
+
+.progress-bar {
+ border-radius: 24px;
+ transition: var(--transition);
+}
+
+/* Add this to replace inline style attributes */
+.progress-bar[style] {
+ width: var(--progress-width, 0%);
+}
+
+/* Tables */
+.table {
+ background: #FFFFFF;
+ border-radius: 16px;
+ overflow: hidden;
+ box-shadow: var(--card-shadow);
+ border-collapse: separate;
+ border-spacing: 0;
+}
+
+.table thead th {
+ background: var(--hover-bg);
+ border: none;
+ padding: 1rem 1.5rem;
+ font-weight: 600;
+ color: var(--primary);
+}
+
+.table tbody td {
+ padding: 1rem 1.5rem;
+ border-top: 1px solid var(--border-color);
+ transition: var(--transition);
+}
+
+.table tbody tr:hover td {
+ background-color: var(--hover-bg);
+}
+
+/* Custom tabs styling */
+.custom-tabs {
+ display: flex;
+ border-bottom: 1px solid #dee2e6;
+ margin-bottom: 1rem;
+}
+
+.custom-tab-btn {
+ padding: 0.5rem 1rem;
+ margin-bottom: -1px;
+ background: none;
+ border: 1px solid transparent;
+ border-top-left-radius: 0.25rem;
+ border-top-right-radius: 0.25rem;
+ cursor: pointer;
+ color: var(--secondary);
+}
+
+.custom-tab-btn.active {
+ color: var(--primary);
+ background-color: #fff;
+ border-color: #dee2e6 #dee2e6 #fff;
+}
+
+.custom-tab-content {
+ display: none;
+}
+
+.custom-tab-content.active {
+ display: block;
+}
+
+/* Fix for profile tab visibility */
+#profileSection.active .tab-pane.active {
+ display: block !important;
+}
+
+#profileSection.active .nav-tabs .nav-link.active {
+ color: var(--primary);
+ border-color: var(--primary);
+ background-color: #fff;
+}
+
+/* Action buttons enhanced */
+.action-button {
+ border-radius: 50px;
+ padding: 10px 20px;
+ font-weight: 500;
+ transition: var(--transition);
+ display: inline-flex;
+ align-items: center;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
+}
+
+.action-button:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+}
+
+.action-button i {
+ margin-right: 8px;
+}
+
+/* Feedback Section Styles */
+.feedback-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.student-avatar {
+ width: 40px;
+ height: 40px;
+ background: var(--primary-light);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-size: 1.5rem;
+}
+
+.rating {
+ display: flex;
+ gap: 0.25rem;
+}
+
+.rating i {
+ font-size: 1rem;
+ transition: transform 0.2s ease;
+}
+
+.rating i:hover {
+ transform: scale(1.2);
+}
+
+.reply-btn {
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ border-radius: 20px;
+ transition: all 0.2s ease;
+}
+
+.reply-btn:hover {
+ background-color: var(--primary);
+ color: white;
+ transform: translateY(-1px);
+}
+
+/* Rating Summary Styles */
+.rating-stats {
+ text-align: center;
+ padding: 1rem;
+ background: linear-gradient(145deg, #ffffff, #f5f5f5);
+ border-radius: 12px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+ min-width: 200px;
+}
+
+.rating-number {
+ line-height: 1;
+ margin-bottom: 0.5rem;
+}
+
+.rating-number .display-4 {
+ font-size: 2.5rem;
+ font-weight: 700;
+ color: var(--primary);
+ margin-bottom: 0;
+}
+
+.rating-stars {
+ display: flex;
+ justify-content: center;
+ gap: 0.25rem;
+ margin-bottom: 0.5rem;
+}
+
+.rating-stars i {
+ font-size: 1.1rem;
+ transition: transform 0.2s ease;
+}
+
+.rating-stars i:hover {
+ transform: scale(1.2);
+}
+
+.rating-count {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+}
+
+/* Responsive Welcome Section */
+@media (max-width: 992px) {
+ .welcome-section {
+ padding: 1.5rem;
+ }
+
+ .welcome-section .profile-picture-container.profile-sm {
+ width: 70px;
+ height: 70px;
+ margin-right: 1rem;
+ }
+
+ .welcome-section h2 {
+ font-size: 1.5rem;
+ }
+}
+
+@media (max-width: 768px) {
+ .welcome-section {
+ padding: 1.25rem;
+ }
+
+ .welcome-section .profile-picture-container.profile-sm {
+ width: 60px;
+ height: 60px;
+ }
+
+ .welcome-section h2 {
+ font-size: 1.25rem;
+ }
+
+ .welcome-section .text-muted {
+ font-size: 0.875rem;
+ }
+}
+
+@media (max-width: 576px) {
+ .welcome-section {
+ padding: 1rem;
+ }
+
+ .welcome-section .profile-picture-container.profile-sm {
+ width: 50px;
+ height: 50px;
+ margin-right: 0.75rem;
+ }
+
+ .welcome-section h2 {
+ font-size: 1.1rem;
+ }
+
+ .welcome-section .d-flex.justify-content-between {
+ gap: 1rem;
+ }
+
+ .welcome-section .badge {
+ font-size: 0.75rem;
+ padding: 0.25rem 0.5rem;
+ }
+
+ .welcome-section .logout-btn {
+ padding: 0.35rem 0.75rem;
+ font-size: 0.8rem;
+ }
+}
+
+@media (max-width: 375px) {
+ .welcome-section {
+ padding: 0.875rem;
+ }
+
+ .welcome-section .profile-picture-container.profile-sm {
+ width: 45px;
+ height: 45px;
+ }
+
+ .welcome-section h2 {
+ font-size: 1rem;
+ }
+
+ .welcome-section .logout-text {
+ display: none;
+ }
+
+ .welcome-section .logout-btn {
+ padding: 0.25rem 0.5rem;
+ }
+
+ .welcome-section .badge {
+ font-size: 0.7rem;
+ padding: 0.2rem 0.4rem;
+ }
+}
+
+/* Scrollable Containers */
+.feedback-list {
+ height: 300px;
+ overflow-y: auto;
+ padding-right: 10px;
+ scrollbar-width: thin;
+ scrollbar-color: var(--primary) var(--light);
+}
+
+.feedback-list::-webkit-scrollbar {
+ width: 6px;
+}
+
+.feedback-list::-webkit-scrollbar-track {
+ background: var(--light);
+ border-radius: 3px;
+}
+
+.feedback-list::-webkit-scrollbar-thumb {
+ background: var(--primary);
+ border-radius: 3px;
+}
+
+.feedback-list::-webkit-scrollbar-thumb:hover {
+ background: var(--primary-dark);
+}
+
+/* Make notice list scrollable */
+.list-group {
+ height: 400px;
+ overflow-y: auto;
+ padding-right: 10px;
+ scrollbar-width: thin;
+ scrollbar-color: var(--primary) var(--light);
+}
+
+.list-group::-webkit-scrollbar {
+ width: 6px;
+}
+
+.list-group::-webkit-scrollbar-track {
+ background: var(--light);
+ border-radius: 3px;
+}
+
+.list-group::-webkit-scrollbar-thumb {
+ background: var(--primary);
+ border-radius: 3px;
+}
+
+.list-group::-webkit-scrollbar-thumb:hover {
+ background: var(--primary-dark);
+}
+
+/* Responsive adjustments */
+@media (max-width: 576px) {
+ .list-group {
+ height: 300px;
+ }
+}
+
+/* Responsive Navigation */
+@media (max-width: 768px) {
+ .nav-container {
+ padding: 0.5rem;
+ gap: 0.25rem;
+ }
+
+ .nav-item {
+ min-width: 70px;
+ padding: 0.4rem;
+ }
+
+ .nav-item i {
+ font-size: 1.1rem;
+ margin-bottom: 0.2rem;
+ }
+
+ .nav-item span {
+ font-size: 0.65rem;
+ }
+}
+
+@media (max-width: 576px) {
+ .nav-container {
+ padding: 0.4rem;
+ gap: 0.2rem;
+ }
+
+ .nav-item {
+ min-width: 60px;
+ padding: 0.35rem;
+ }
+
+ .nav-item i {
+ font-size: 1rem;
+ margin-bottom: 0.15rem;
+ }
+
+ .nav-item span {
+ font-size: 0.6rem;
+ }
+}
+
+@media (max-width: 375px) {
+ .nav-container {
+ padding: 0.3rem;
+ gap: 0.15rem;
+ }
+
+ .nav-item {
+ min-width: 50px;
+ padding: 0.3rem;
+ }
+
+ .nav-item i {
+ font-size: 0.9rem;
+ margin-bottom: 0.1rem;
+ }
+
+ .nav-item span {
+ font-size: 0.55rem;
+ }
+}
+
+/* Enhanced Responsive Styles for Dashboard */
+
+/* Tablet and below adjustments for the welcome section */
+@media (max-width: 768px) {
+ /* Dashboard cards adjustments */
+ .content-card {
+ padding: 16px;
+ margin-bottom: 16px;
+ }
+
+ /* Better spacing for the summary cards */
+ .row > [class*="col-"] {
+ margin-bottom: 16px;
+ }
+
+ /* Tighter spacing for statistics cards */
+ .summary-card .stat-number {
+ font-size: 2rem;
+ }
+}
+
+/* Small devices (landscape phones) */
+@media (max-width: 576px) {
+ /* Dashboard cards adjustments */
+ .content-card {
+ padding: 12px;
+ margin-bottom: 12px;
+ border-radius: 8px;
+ }
+
+ /* Stacked layout for activity items */
+ .activity-item {
+ flex-direction: column;
+ }
+
+ .activity-item .activity-info {
+ margin-top: 8px;
+ width: 100%;
+ }
+
+ /* Summary cards for mobile */
+ .summary-card .stat-number {
+ font-size: 1.75rem;
+ }
+
+ .summary-card .card-body {
+ padding: 16px;
+ }
+
+ /* Simplified progress bars */
+ .progress {
+ height: 6px;
+ }
+
+ /* Mobile-optimized lists */
+ .list-group-item {
+ padding: 0.75rem;
+ }
+
+ /* Table adjustments */
+ .table th,
+ .table td {
+ padding: 0.5rem 0.25rem;
+ font-size: 0.75rem;
+ }
+
+ /* Modified tabs for mobile */
+ .custom-tab-btn {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.85rem;
+ }
+}
+
+/* Extra small devices (portrait phones) */
+@media (max-width: 375px) {
+ /* Tighter spacing */
+ .content-card {
+ padding: 10px;
+ margin-bottom: 10px;
+ }
+
+ /* Further reduced stat sizes */
+ .summary-card .stat-number {
+ font-size: 1.5rem;
+ }
+
+ /* Stacked buttons on very small screens */
+ .btn-sm-stack {
+ display: block;
+ width: 100%;
+ margin-bottom: 0.5rem;
+ }
+
+ /* Even smaller tabs */
+ .custom-tab-btn {
+ padding: 0.4rem 0.6rem;
+ font-size: 0.8rem;
+ }
+
+ /* Simplified profile sections */
+ .profile-info-row {
+ flex-direction: column;
+ }
+
+ .profile-info-label,
+ .profile-info-value {
+ width: 100%;
+ text-align: left;
+ }
+}
+
+/* Improvements for larger screens */
+@media (min-width: 992px) {
+ /* More breathing room */
+ .content-card {
+ padding: 24px;
+ margin-bottom: 24px;
+ }
+
+ /* Larger stats for desktop */
+ .summary-card .stat-number {
+ font-size: 2.75rem;
+ }
+}
+
+/* Extra styles for proper responsiveness of common elements */
+img.img-fluid {
+ max-width: 100%;
+ height: auto;
+}
+
+.table-responsive {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+/* Ensure buttons stay properly sized on mobile */
+@media (max-width: 576px) {
+ .btn {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+ }
+
+ .btn-lg {
+ padding: 0.5rem 1rem;
+ font-size: 1rem;
+ }
+
+ .btn-sm {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ }
+}
\ No newline at end of file
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..1f51c1a
Binary files /dev/null and b/static/favicon.ico differ
diff --git a/static/firebase-messaging-sw.js b/static/firebase-messaging-sw.js
new file mode 100644
index 0000000..7de7799
--- /dev/null
+++ b/static/firebase-messaging-sw.js
@@ -0,0 +1,87 @@
+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');
+
+firebase.initializeApp({
+ apiKey: "AIzaSyAYgYXsf4-iwSDEEBcNAJ3YJxz76C3STsU",
+ authDomain: "student-management-syste-2e040.firebaseapp.com",
+ projectId: "student-management-syste-2e040",
+ storageBucket: "student-management-syste-2e040.appspot.com",
+ messagingSenderId: "800148418884",
+ appId: "1:800148418884:web:144b9c85bfeeec12252711",
+ measurementId: "G-BNRM6QEJQV"
+});
+
+const messaging = firebase.messaging();
+
+// Handle background messages
+messaging.onBackgroundMessage((payload) => {
+ console.log('Received background message:', payload);
+
+ // 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'
+ }
+ ]
+ };
+
+ // 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);
+ });
+});
+
+// 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)
+ );
+ }
+});
+
+// Handle service worker installation
+self.addEventListener('install', (event) => {
+ console.log('Service Worker installed');
+ self.skipWaiting();
+});
+
+// Handle service worker activation
+self.addEventListener('activate', (event) => {
+ console.log('Service Worker activated');
+ event.waitUntil(clients.claim());
+});
diff --git a/static/img/favicon.ico b/static/img/favicon.ico
new file mode 100644
index 0000000..1f51c1a
Binary files /dev/null and b/static/img/favicon.ico differ
diff --git a/static/img/logo.png b/static/img/logo.png
new file mode 100644
index 0000000..1f51c1a
Binary files /dev/null and b/static/img/logo.png differ
diff --git a/static/img/user.png b/static/img/user.png
new file mode 100644
index 0000000..7127aa8
Binary files /dev/null and b/static/img/user.png differ
diff --git a/static/js/bootstrap-init.js b/static/js/bootstrap-init.js
new file mode 100644
index 0000000..7f40c70
--- /dev/null
+++ b/static/js/bootstrap-init.js
@@ -0,0 +1,25 @@
+// Initialize Bootstrap components
+function initializeBootstrapComponents() {
+ // Initialize tooltips if available
+ if (typeof bootstrap !== "undefined" && bootstrap.Tooltip) {
+ const tooltipTriggerList = [].slice.call(
+ document.querySelectorAll('[data-bs-toggle="tooltip"]'),
+ );
+ tooltipTriggerList.map(function(tooltipTriggerEl) {
+ return new bootstrap.Tooltip(tooltipTriggerEl);
+ });
+ }
+
+ // Initialize popovers if available
+ if (typeof bootstrap !== "undefined" && bootstrap.Popover) {
+ const popoverTriggerList = [].slice.call(
+ document.querySelectorAll('[data-bs-toggle="popover"]'),
+ );
+ popoverTriggerList.map(function(popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl);
+ });
+ }
+}
+
+// Make function globally accessible
+window.initializeBootstrapComponents = initializeBootstrapComponents;
\ No newline at end of file
diff --git a/static/js/dashboard.js b/static/js/dashboard.js
new file mode 100644
index 0000000..61c7ce1
--- /dev/null
+++ b/static/js/dashboard.js
@@ -0,0 +1,785 @@
+// Dashboard functionality
+
+// Wait for the DOM to be fully loaded before running any code
+document.addEventListener("DOMContentLoaded", function() {
+ // More reliable login detection - check for dashboard-specific elements
+ const isLoggedIn = (
+ // Check for common dashboard elements
+ !!document.querySelector('.dashboard-content') ||
+ !!document.querySelector('.side-navbar') ||
+ !!document.querySelector('#user-dropdown') ||
+ !!document.querySelector('.user-menu') ||
+ !!document.querySelector('.nav-link[href*="logout"]') ||
+ // Check URL patterns that would only be accessible when logged in
+ window.location.pathname.includes('/dashboard') ||
+ window.location.pathname.includes('/profile') ||
+ window.location.pathname.includes('/app/') ||
+ // Explicit authentication flag
+ window.IS_AUTHENTICATED === true
+ );
+
+ if (isLoggedIn) {
+ // Call with a slight delay to ensure page is fully loaded
+ setTimeout(function() {
+ checkWeakPassword();
+ }, 100);
+ }
+
+ // Initialize all components
+ initializeComponents();
+
+ // Set up event handlers
+ setupEventHandlers();
+
+ // Get device token for notifications
+ if (typeof window.getDeviceToken === 'function' && isLoggedIn) {
+ window.getDeviceToken();
+ }
+
+ // Add global keyboard event handler for Escape key
+ document.addEventListener('keydown', function(event) {
+ if (event.key === 'Escape' || event.keyCode === 27) {
+ closeAllModals();
+ }
+ });
+});
+
+// Function to initialize all dashboard components
+function initializeComponents() {
+ // Initialize any Bootstrap components
+ if (typeof bootstrap !== 'undefined') {
+ // Initialize tooltips
+ var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
+ tooltipTriggerList.map(function(tooltipTriggerEl) {
+ return new bootstrap.Tooltip(tooltipTriggerEl);
+ });
+
+ // Initialize popovers
+ var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
+ popoverTriggerList.map(function(popoverTriggerEl) {
+ return new bootstrap.Popover(popoverTriggerEl);
+ });
+ }
+
+ // Handle image loading errors
+ document.querySelectorAll('img').forEach(function(img) {
+ img.addEventListener("error", function() {
+ this.onerror = null;
+ this.src = "/static/img/user.png";
+ });
+ });
+
+ // Setup star rating if elements exist
+ if (typeof window.setupStarRating === 'function') {
+ window.setupStarRating();
+ }
+
+ // Setup tabs if elements exist
+ if (typeof window.setupCustomTabs === 'function') {
+ window.setupCustomTabs();
+ }
+
+ // Setup feedback tabs if elements exist
+ if (typeof window.setupFeedbackTabs === 'function') {
+ window.setupFeedbackTabs();
+ }
+
+ // Initialize Bootstrap components
+ if (typeof window.initializeBootstrapComponents === 'function') {
+ window.initializeBootstrapComponents();
+ }
+
+ // Set up form validation
+ if (typeof window.setupRatingFormValidation === 'function') {
+ window.setupRatingFormValidation();
+ }
+
+ // Ensure notifications.js is loaded
+ if (typeof window.showNotification !== 'function') {
+ // Dynamically load the notifications script if not already available
+ const script = document.createElement('script');
+ script.src = '/static/js/notifications.js';
+ script.async = true;
+ document.head.appendChild(script);
+ }
+
+ // Ensure notification toast container exists
+ if (!document.getElementById('notificationToast')) {
+ const toastContainer = document.createElement('div');
+ toastContainer.innerHTML = `
+
+
+
+
+ Message goes here
+
+
+
+ `;
+ document.body.appendChild(toastContainer.firstElementChild);
+ }
+}
+
+// Star rating functionality
+function setupStarRating() {
+ document.querySelectorAll('.rating').forEach(function(container) {
+ var stars = container.querySelectorAll('.star');
+ var ratingInput = container.querySelector('input[type="hidden"]');
+
+ stars.forEach(function(star, index) {
+ // Set initial state
+ if (ratingInput && parseInt(ratingInput.value) > index) {
+ star.classList.add('active');
+ }
+
+ // Add click handlers
+ star.addEventListener('click', function() {
+ // Update hidden input
+ if (ratingInput) {
+ ratingInput.value = index + 1;
+ }
+
+ // Update star display
+ stars.forEach(function(s, i) {
+ if (i <= index) {
+ s.classList.add('active');
+ } else {
+ s.classList.remove('active');
+ }
+ });
+ });
+
+ // Add hover effects
+ star.addEventListener('mouseenter', function() {
+ stars.forEach(function(s, i) {
+ if (i <= index) {
+ s.classList.add('hover');
+ } else {
+ s.classList.remove('hover');
+ }
+ });
+ });
+
+ container.addEventListener('mouseleave', function() {
+ stars.forEach(function(s) {
+ s.classList.remove('hover');
+ });
+ });
+ });
+ });
+}
+
+// Function to set up custom tabs
+function setupCustomTabs() {
+ var tabLinks = document.querySelectorAll('.custom-tabs .tab-link');
+
+ tabLinks.forEach(function(link) {
+ link.addEventListener('click', function(e) {
+ e.preventDefault();
+ var target = this.getAttribute('data-target');
+
+ // Hide all tab content
+ document.querySelectorAll('.custom-tabs .tab-content').forEach(function(tab) {
+ tab.classList.remove('active');
+ });
+
+ // Deactivate all tab links
+ tabLinks.forEach(function(tabLink) {
+ tabLink.classList.remove('active');
+ });
+
+ // Activate clicked tab and content
+ this.classList.add('active');
+ document.getElementById(target).classList.add('active');
+ });
+ });
+
+ // Activate first tab if none active
+ if (document.querySelector('.custom-tabs .tab-link.active') === null) {
+ var firstTab = document.querySelector('.custom-tabs .tab-link');
+ if (firstTab) {
+ firstTab.click();
+ }
+ }
+}
+
+// Function to set up feedback tabs
+function setupFeedbackTabs() {
+ var tabLinks = document.querySelectorAll('.feedback-tabs .tab-link');
+
+ tabLinks.forEach(function(link) {
+ link.addEventListener('click', function(e) {
+ e.preventDefault();
+ var target = this.getAttribute('data-target');
+
+ // Hide all tab content
+ document.querySelectorAll('.feedback-tabs .tab-content').forEach(function(tab) {
+ tab.classList.remove('active');
+ });
+
+ // Deactivate all tab links
+ tabLinks.forEach(function(tabLink) {
+ tabLink.classList.remove('active');
+ });
+
+ // Activate clicked tab and content
+ this.classList.add('active');
+ document.getElementById(target).classList.add('active');
+ });
+ });
+}
+
+// Function to check if the current user's password is weak and show a popup if needed
+function checkWeakPassword() {
+ // Only skip on login/register pages
+ if (window.location.pathname.indexOf('/login') !== -1 ||
+ window.location.pathname.indexOf('/register') !== -1 ||
+ window.location.pathname === '/') {
+ return;
+ }
+
+ // On dashboard page, we should definitely be logged in
+ const onDashboard = window.location.pathname.includes('/dashboard') ||
+ window.location.pathname.includes('/app/');
+
+ try {
+ // Basic fetch with no extra options to keep it simple
+ fetch('/app/check-weak-password/')
+ .then(function(response) {
+ // Handle unauthorized responses (not logged in)
+ if (response.status === 401 || response.status === 403) {
+ return null;
+ }
+
+ // Check if response is valid JSON
+ const contentType = response.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ return response.json().catch(error => {
+ return response.text().then(text => {
+ // Only show if there's an indication of a weak password
+ return text.includes('weak') ? { is_weak: true } : { is_weak: false };
+ });
+ });
+ } else {
+ // Not JSON, try to get text instead
+ return response.text().then(text => {
+ // Only show if there's an indication of a weak password
+ return text.includes('weak') ? { is_weak: true } : { is_weak: false };
+ });
+ }
+ })
+ .then(function(data) {
+ // Skip if null (user not authenticated)
+ if (data === null) return;
+
+ if (data && data.is_weak === true) {
+ // Show modal immediately
+ showWeakPasswordModal();
+ }
+ })
+ .catch(function(error) {
+ console.error('Error checking password strength:', error);
+ });
+ } catch (e) {
+ console.error('Error in checkWeakPassword:', e);
+ }
+}
+
+// Function to create and show the weak password modal
+function showWeakPasswordModal() {
+ // Check if modal already exists (prevent duplicates)
+ if (document.getElementById('weakPasswordModal')) {
+ document.getElementById('weakPasswordModal').style.display = 'block';
+ return;
+ }
+
+ // Get CSRF token
+ var csrfToken = '';
+ var csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]');
+
+ if (csrfInput) {
+ csrfToken = csrfInput.value;
+ } else {
+ // Try to get from cookie if not found in form
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = cookies[i].trim();
+ if (cookie.startsWith('csrftoken=')) {
+ csrfToken = cookie.substring('csrftoken='.length, cookie.length);
+ break;
+ }
+ }
+ }
+
+ // Create modal HTML with password inputs
+ var modalHtml = `
+
+
+
+
+
+
+ Your current password is weak and poses a security risk.
+
+
Please change your password to a more secure one to continue using the system.
+
+
+
+
+
`;
+
+ // Append modal to body
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
+
+ // Add media query styles for better responsiveness
+ const styleEl = document.createElement('style');
+ styleEl.textContent = `
+ @media (max-width: 576px) {
+ #weakPasswordModal .modal-content {
+ width: 95% !important;
+ padding: 15px !important;
+ }
+
+ #weakPasswordModal .modal-title {
+ font-size: 1.1rem !important;
+ }
+
+ #weakPasswordModal .form-label {
+ font-size: 0.9rem !important;
+ }
+ }
+
+ @media (max-height: 640px) {
+ #weakPasswordModal {
+ align-items: flex-start !important;
+ padding-top: 20px !important;
+ }
+ }
+ `;
+ document.head.appendChild(styleEl);
+
+ // Set up password handling directly
+ var passwordArea = document.getElementById('new_password_area');
+ var confirmArea = document.getElementById('confirm_password_area');
+ var lengthDisplay = document.getElementById('password-length-display');
+ var changeBtn = document.getElementById('password-change-btn');
+ var showPasswordCheckbox = document.getElementById('show_password');
+ var passwordForm = document.getElementById('password-form-div');
+
+ if (passwordArea && lengthDisplay) {
+ // Focus on password field
+ setTimeout(function() {
+ passwordArea.focus();
+ }, 100);
+
+ // Update function that directly reads the password field
+ function updateLengthDisplay() {
+ var password = passwordArea.value || '';
+ // Update display with current length
+ lengthDisplay.textContent = 'Current length: ' + password.length + ' characters';
+ }
+
+ // Add multiple event listeners to catch all input cases
+ ['input', 'keyup', 'keydown', 'change', 'paste'].forEach(function(eventType) {
+ passwordArea.addEventListener(eventType, updateLengthDisplay);
+ });
+
+ // Add keydown event for Enter key
+ passwordArea.addEventListener('keydown', function(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ // Focus on confirm field if Enter is pressed in password field
+ confirmArea.focus();
+ }
+ });
+
+ // Add keydown event for Enter key on confirm field
+ confirmArea.addEventListener('keydown', function(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ // Trigger submit button click if Enter is pressed in confirm field
+ if (changeBtn) {
+ changeBtn.click();
+ }
+ }
+ });
+
+ // Initial update
+ updateLengthDisplay();
+ }
+
+ // Toggle password visibility
+ if (showPasswordCheckbox) {
+ showPasswordCheckbox.addEventListener('change', function() {
+ var type = this.checked ? 'text' : 'password';
+ passwordArea.type = type;
+ confirmArea.type = type;
+ });
+ }
+
+ if (changeBtn) {
+ changeBtn.addEventListener('click', function() {
+ // Get the current password from the hidden field (more reliable)
+ var currentPassword = document.getElementById('current_password_hidden').value || '123';
+ var newPassword = passwordArea.value || '';
+ var confirmPassword = confirmArea.value || '';
+
+ // Check for empty fields first
+ if (!newPassword) {
+ alert('New password is required.');
+ passwordArea.focus();
+ return;
+ }
+
+ if (!confirmPassword) {
+ alert('Please confirm your password.');
+ confirmArea.focus();
+ return;
+ }
+
+ // Validate passwords
+ if (newPassword.length < 8) {
+ alert('Password must be at least 8 characters long! Your password is ' + newPassword.length + ' characters.');
+ passwordArea.focus();
+ return;
+ }
+
+ if (newPassword !== confirmPassword) {
+ alert('Passwords do not match!');
+ confirmArea.focus();
+ return;
+ }
+
+ if (newPassword === '123') {
+ alert('Please choose a stronger password!');
+ passwordArea.focus();
+ return;
+ }
+
+ // Create form data
+ var formData = new FormData();
+ formData.append('csrfmiddlewaretoken', csrfToken);
+ formData.append('current_password', currentPassword);
+ formData.append('new_password', newPassword);
+ formData.append('confirm_password', confirmPassword);
+
+ // Submit form
+ fetch('/app/change-password/', {
+ method: 'POST',
+ body: formData,
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Cache-Control': 'no-cache'
+ }
+ })
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(data) {
+ if (data.success) {
+ // Use notification system instead of alert
+ if (typeof window.showNotification === 'function') {
+ window.showNotification('Password Updated', 'Your password has been changed successfully!', 'success');
+ } else {
+ alert('Password changed successfully!');
+ }
+
+ // Close the modal
+ document.getElementById('weakPasswordModal')?.remove();
+ // No redirect to login page
+ } else if (data.error) {
+ // Show error notification
+ if (typeof window.showNotification === 'function') {
+ window.showNotification('Error', data.error, 'error');
+ } else {
+ alert(data.error);
+ }
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ window.location.reload();
+ });
+ });
+ }
+
+ // Handle form submission (including Enter key)
+ if (passwordForm) {
+ passwordForm.addEventListener('submit', function(e) {
+ e.preventDefault();
+ // Trigger the password change button click
+ if (changeBtn) {
+ changeBtn.click();
+ }
+ });
+ }
+}
+
+// Function to set up event handlers
+function setupEventHandlers() {
+ // Set up any tab switching behavior
+ var tabLinks = document.querySelectorAll('[data-bs-toggle="tab"]');
+ tabLinks.forEach(function(tabLink) {
+ tabLink.addEventListener('click', function(event) {
+ event.preventDefault();
+ var targetTab = document.querySelector(this.getAttribute('href'));
+ if (targetTab) {
+ // Hide all tabs
+ document.querySelectorAll('.tab-pane').forEach(function(tab) {
+ tab.classList.remove('active', 'show');
+ });
+
+ // Show target tab
+ targetTab.classList.add('active', 'show');
+
+ // Update active state on tab links
+ tabLinks.forEach(function(link) {
+ link.classList.remove('active');
+ });
+ this.classList.add('active');
+ }
+ });
+ });
+
+ // Close any alerts after a delay
+ var alerts = document.querySelectorAll('.alert:not(.alert-warning)');
+ alerts.forEach(function(alert) {
+ setTimeout(function() {
+ alert.style.display = 'none';
+ }, 5000);
+ });
+}
+
+// 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
+ document.querySelectorAll('[id$="Modal"]').forEach(modal => {
+ if (modal.style && modal.style.display === 'block') {
+ modal.style.display = 'none';
+
+ // If there's a specific close function, try to call it
+ const modalId = modal.id;
+ const closeFunction = window['close' + modalId.charAt(0).toUpperCase() + modalId.slice(1)];
+ if (typeof closeFunction === 'function') {
+ closeFunction();
+ } else {
+ // Reset body overflow as a fallback
+ document.body.style.overflow = '';
+ }
+ }
+ });
+
+ // Explicitly handle the weak password modal - but only allow force close
+ const weakPasswordModal = document.getElementById('weakPasswordModal');
+ if (weakPasswordModal && weakPasswordModal.style.display === 'block') {
+ // Do not auto-close the weak password modal
+ // This will be handled by the specific close function
+ }
+
+ // Close Bootstrap modals
+ const bootstrapModals = document.querySelectorAll('.modal.show');
+ bootstrapModals.forEach(modal => {
+ if (window.bootstrap && window.bootstrap.Modal) {
+ const bsModal = bootstrap.Modal.getInstance(modal);
+ if (bsModal) {
+ bsModal.hide();
+ }
+ }
+ });
+}
+
+// Set up direct event handlers without jQuery
+function setupDirectPasswordHandlers() {
+ // Get references to the new password elements
+ var passwordArea = document.getElementById('new_password_area');
+ var confirmArea = document.getElementById('confirm_password_area');
+ var lengthDisplay = document.getElementById('password-length-display');
+ var changeBtn = document.getElementById('password-change-btn');
+ var passwordForm = document.getElementById('password-form-div');
+
+ if (passwordArea && lengthDisplay) {
+ // Focus on password field
+ setTimeout(function() {
+ passwordArea.focus();
+ }, 100);
+
+ // Update function that directly reads the password field
+ function updateLengthDisplay() {
+ var password = passwordArea.value || '';
+ // Update display with current length
+ lengthDisplay.textContent = 'Current length: ' + password.length + ' characters';
+ }
+
+ // Add multiple event listeners to catch all input cases
+ ['input', 'keyup', 'keydown', 'change', 'paste'].forEach(function(eventType) {
+ passwordArea.addEventListener(eventType, updateLengthDisplay);
+ });
+
+ // Add keydown event for Enter key
+ passwordArea.addEventListener('keydown', function(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ // Focus on confirm field if Enter is pressed in password field
+ confirmArea.focus();
+ }
+ });
+
+ // Add keydown event for Enter key on confirm field
+ confirmArea.addEventListener('keydown', function(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ // Trigger submit button click if Enter is pressed in confirm field
+ if (changeBtn) {
+ changeBtn.click();
+ }
+ }
+ });
+
+ // Initial update
+ updateLengthDisplay();
+ }
+
+ // Handle form submission (including Enter key)
+ if (passwordForm) {
+ passwordForm.addEventListener('submit', function(e) {
+ e.preventDefault();
+ // Trigger the password change button click
+ if (changeBtn) {
+ changeBtn.click();
+ }
+ });
+ }
+
+ if (changeBtn) {
+ changeBtn.addEventListener('click', function() {
+ // Get the current password from the hidden field (more reliable)
+ var currentPassword = document.getElementById('current_password_hidden').value || '123';
+ var newPassword = passwordArea.value || '';
+ var confirmPassword = confirmArea.value || '';
+
+ // Check for empty fields first
+ if (!newPassword) {
+ alert('New password is required.');
+ passwordArea.focus();
+ return;
+ }
+
+ if (!confirmPassword) {
+ alert('Please confirm your password.');
+ confirmArea.focus();
+ return;
+ }
+
+ // Validate passwords
+ if (newPassword.length < 8) {
+ alert('Password must be at least 8 characters long! Your password is ' + newPassword.length + ' characters.');
+ passwordArea.focus();
+ return;
+ }
+
+ if (newPassword !== confirmPassword) {
+ alert('Passwords do not match!');
+ confirmArea.focus();
+ 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();
+ formData.append('csrfmiddlewaretoken', csrfToken);
+ formData.append('current_password', currentPassword);
+ formData.append('new_password', newPassword);
+ formData.append('confirm_password', confirmPassword);
+
+ // Submit form
+ fetch('/app/change-password/', {
+ method: 'POST',
+ body: formData,
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest'
+ }
+ })
+ .then(response => response.json())
+ .then(function(data) {
+ if (data.success) {
+ // Use notification system instead of alert
+ if (typeof window.showNotification === 'function') {
+ window.showNotification('Password Updated', 'Your password has been changed successfully!', 'success');
+ } else {
+ alert('Password changed successfully!');
+ }
+
+ // Close the modal
+ document.getElementById('weakPasswordModal')?.remove();
+ // No redirect to login page
+ } else if (data.error) {
+ // Show error notification
+ if (typeof window.showNotification === 'function') {
+ window.showNotification('Error', data.error, 'error');
+ } else {
+ alert(data.error);
+ }
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ window.location.reload();
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/static/js/firebase-config.example.js b/static/js/firebase-config.example.js
new file mode 100644
index 0000000..dbac75a
--- /dev/null
+++ b/static/js/firebase-config.example.js
@@ -0,0 +1,13 @@
+// Firebase configuration
+const firebaseConfig = {
+ apiKey: "YOUR_API_KEY",
+ authDomain: "YOUR_AUTH_DOMAIN",
+ projectId: "YOUR_PROJECT_ID",
+ storageBucket: "YOUR_STORAGE_BUCKET",
+ messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
+ appId: "YOUR_APP_ID",
+ measurementId: "YOUR_MEASUREMENT_ID"
+};
+
+// VAPID key for push notifications
+const vapidKey = "YOUR_VAPID_KEY";
\ No newline at end of file
diff --git a/static/js/firebase-init.js b/static/js/firebase-init.js
new file mode 100644
index 0000000..bafee3c
--- /dev/null
+++ b/static/js/firebase-init.js
@@ -0,0 +1,160 @@
+// Get CSRF token from cookie
+function getCookie(name) {
+ let cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ const cookies = document.cookie.split(';');
+ for (let i = 0; i < cookies.length; i++) {
+ const cookie = cookies[i].trim();
+ if (cookie.substring(0, name.length + 1) === (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+}
+
+// Initialize Firebase
+firebase.initializeApp(firebaseConfig);
+const messaging = firebase.messaging();
+
+async function registerServiceWorker() {
+ try {
+ if (!('serviceWorker' in navigator)) {
+ throw new Error('Service Worker is not supported in this browser');
+ }
+
+ // Unregister any existing service workers
+ const existingRegistrations = await navigator.serviceWorker.getRegistrations();
+ for (let registration of existingRegistrations) {
+ await registration.unregister();
+ }
+
+ // Register the new service worker
+ const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js', {
+ scope: '/',
+ updateViaCache: 'none'
+ });
+
+ console.log('Service Worker registered with scope:', registration.scope);
+ await navigator.serviceWorker.ready;
+ return registration;
+ } catch (error) {
+ console.error('Service Worker registration failed:', error);
+ throw error;
+ }
+}
+
+async function getDeviceToken() {
+ try {
+ const registration = await registerServiceWorker();
+
+ // Request notification permission if not already granted
+ let permission = Notification.permission;
+ if (permission === 'default') {
+ permission = await Notification.requestPermission();
+ }
+
+ if (permission !== 'granted') {
+ throw new Error('Notification permission not granted');
+ }
+
+ // Get FCM token
+ const token = await messaging.getToken({
+ vapidKey: vapidKey,
+ serviceWorkerRegistration: registration
+ });
+
+ if (!token) {
+ throw new Error('Failed to get FCM token');
+ }
+
+ console.log('Got FCM token:', token);
+
+ // Get CSRF token
+ const csrftoken = getCookie('csrftoken');
+ if (!csrftoken) {
+ throw new Error('CSRF token not found');
+ }
+
+ // Send the token to your backend for storage
+ const response = await fetch('/app/saveFCMToken/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': csrftoken
+ },
+ body: JSON.stringify({ token: token })
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to save token to server');
+ }
+
+ console.log('FCM Token saved successfully');
+ return token;
+ } catch (error) {
+ console.error('Error in getDeviceToken:', error);
+ try {
+ // Clean up and retry on failure
+ await messaging.deleteToken().catch(console.error);
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ for (let registration of registrations) {
+ await registration.unregister();
+ }
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ return getDeviceToken();
+ } catch (retryError) {
+ console.error('Error during cleanup and retry:', retryError);
+ throw error;
+ }
+ }
+}
+
+// Make getDeviceToken available globally
+window.getDeviceToken = getDeviceToken;
+
+// Handle token refresh
+messaging.onTokenRefresh = async () => {
+ try {
+ await getDeviceToken();
+ } catch (error) {
+ console.error('Error refreshing token:', error);
+ }
+};
+
+// Handle foreground messages
+messaging.onMessage((payload) => {
+ console.log('Foreground message received:', payload);
+
+ if (Notification.permission === 'granted') {
+ const notificationTitle = payload.notification.title;
+ const notificationOptions = {
+ body: payload.notification.body,
+ icon: payload.notification.icon || '/static/img/logo.png',
+ badge: '/static/img/logo.png',
+ timestamp: Date.now(),
+ requireInteraction: true,
+ vibrate: [200, 100, 200],
+ tag: 'notification-' + Date.now(),
+ data: payload.data || {},
+ actions: [
+ {
+ action: 'open',
+ title: 'Open'
+ },
+ {
+ action: 'close',
+ title: 'Close'
+ }
+ ]
+ };
+
+ navigator.serviceWorker.ready.then(registration => {
+ registration.showNotification(notificationTitle, notificationOptions)
+ .catch(error => {
+ console.error('Error showing notification:', error);
+ });
+ });
+ }
+});
\ No newline at end of file
diff --git a/static/js/form-validation.js b/static/js/form-validation.js
new file mode 100644
index 0000000..eb694bc
--- /dev/null
+++ b/static/js/form-validation.js
@@ -0,0 +1,25 @@
+// Setup feedback form validation
+function setupRatingFormValidation() {
+ const ratingForm = document.querySelector(".feedback-form");
+ const ratingInput = document.getElementById("selected_rating");
+ const ratingError = document.getElementById("rating-error");
+
+ if (ratingForm && ratingInput && ratingError) {
+ // Add form validation before submission
+ ratingForm.addEventListener("submit", function(e) {
+ // Check if rating is selected
+ if (!ratingInput.value) {
+ e.preventDefault(); // Stop form submission
+ if (ratingError) {
+ ratingError.style.display = "block";
+ ratingError.scrollIntoView({ behavior: "smooth" });
+ }
+ return false;
+ }
+ return true;
+ });
+ }
+}
+
+// Make function globally accessible
+window.setupRatingFormValidation = setupRatingFormValidation;
\ No newline at end of file
diff --git a/static/js/navigation.js b/static/js/navigation.js
new file mode 100644
index 0000000..5cf1620
--- /dev/null
+++ b/static/js/navigation.js
@@ -0,0 +1,88 @@
+document.addEventListener('DOMContentLoaded', function () {
+
+ // Get all navigation items
+ const navItems = document.querySelectorAll('.nav-item[data-section]');
+
+ // Clear active state from all navigation items immediately
+ navItems.forEach(nav => nav.classList.remove('active'));
+
+ // Hide all sections initially to prevent flickering
+ document.querySelectorAll('.content-section').forEach(section => {
+ section.classList.remove('active');
+ section.style.display = 'none';
+ section.style.visibility = 'hidden';
+ });
+
+ // Function to switch to a section
+ function switchToSection(targetSection, skipStorage = false) {
+ if (!targetSection) return;
+
+ if (!skipStorage) {
+ sessionStorage.setItem('activeSection', targetSection);
+ }
+
+ // Remove active class from all nav items
+ navItems.forEach(nav => nav.classList.remove('active'));
+
+ // Add active class to corresponding nav item
+ const activeNav = document.querySelector(`.nav-item[data-section="${targetSection}"]`);
+ if (activeNav) {
+ activeNav.classList.add('active');
+ }
+
+ // Hide all sections first
+ document.querySelectorAll('.content-section').forEach(section => {
+ section.classList.remove('active');
+ section.style.display = 'none';
+ section.style.visibility = 'hidden';
+ });
+
+ // Show the target section
+ const targetElement = document.getElementById(`${targetSection}Section`);
+
+ if (targetElement) {
+ targetElement.classList.add('active');
+ targetElement.style.display = 'block';
+ targetElement.style.visibility = 'visible';
+
+ // Update URL hash without scrolling
+ history.replaceState(null, null, `#${targetSection}`);
+ }
+
+ // Make the page visible now that everything is ready
+ document.body.style.opacity = "1";
+ }
+
+ // Handle initial page load before showing content
+ function handleInitialLoad() {
+ let targetSection = 'dashboard'; // Default section
+
+ if (window.location.hash) {
+ targetSection = window.location.hash.substring(1);
+ } else if (sessionStorage.getItem('activeSection')) {
+ targetSection = sessionStorage.getItem('activeSection');
+ }
+
+ // Switch to the target section without saving to sessionStorage again
+ switchToSection(targetSection, true);
+ }
+
+ // Add event listeners to navigation items
+ navItems.forEach(item => {
+ const section = item.getAttribute('data-section');
+
+ item.addEventListener('click', function (e) {
+ e.preventDefault();
+ switchToSection(section);
+ });
+ });
+
+ // Handle hash changes (browser back/forward)
+ window.addEventListener('hashchange', function () {
+ const targetSection = window.location.hash.substring(1) || 'dashboard';
+ switchToSection(targetSection);
+ });
+
+ // Run initial load before rendering content
+ handleInitialLoad();
+});
diff --git a/static/js/notifications.js b/static/js/notifications.js
new file mode 100644
index 0000000..c9250a0
--- /dev/null
+++ b/static/js/notifications.js
@@ -0,0 +1,29 @@
+// Function to show notification toast
+function showNotification(title, message, type = "success") {
+ const toast = document.getElementById("notificationToast");
+ if (!toast) return;
+
+ const toastTitle = document.getElementById("toastTitle");
+ const toastMessage = document.getElementById("toastMessage");
+
+ toastTitle.innerText = title;
+ toastMessage.innerText = message;
+
+ // Set toast color based on type
+ toast.className = "toast";
+ if (type === "success") {
+ toast.classList.add("bg-success", "text-white");
+ } else if (type === "error") {
+ toast.classList.add("bg-danger", "text-white");
+ } else if (type === "warning") {
+ toast.classList.add("bg-warning");
+ } else if (type === "info") {
+ toast.classList.add("bg-info", "text-white");
+ }
+
+ const bsToast = new bootstrap.Toast(toast);
+ bsToast.show();
+}
+
+// Make function globally accessible
+window.showNotification = showNotification;
\ No newline at end of file
diff --git a/static/js/rating.js b/static/js/rating.js
new file mode 100644
index 0000000..a7b6536
--- /dev/null
+++ b/static/js/rating.js
@@ -0,0 +1,163 @@
+// Function to set up star rating
+function setupStarRating() {
+ console.log("Setting up star rating...");
+
+ // Handle display of existing ratings (read-only stars)
+ setupExistingRatings();
+}
+
+// Function to set up existing ratings (read-only stars)
+function setupExistingRatings() {
+ // Find all rating divs (not in forms)
+ document.querySelectorAll('.rating').forEach(container => {
+ if (!container) return;
+
+ // Make sure the stars have the right color
+ const stars = container.querySelectorAll('i.fas, i.far');
+ stars.forEach(star => {
+ if (star.classList.contains('fas')) {
+ star.style.color = '#FFC107';
+ }
+ });
+ });
+}
+
+// Add CSS styles for rating stars
+(function() {
+ const style = document.createElement('style');
+ style.textContent = `
+ .rating-star {
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+ .rating-star.hover,
+ .rating-star:hover,
+ .rating-star.active,
+ .rating-star.selected {
+ color: #FFD700 !important;
+ transform: scale(1.1);
+ }
+ `;
+ document.head.appendChild(style);
+})();
+
+// Global function to select rating stars
+function selectRating(star, inputId) {
+ console.log('selectRating called with star value:', star.getAttribute('data-value'), 'for input:', inputId);
+
+ // Get the value
+ const value = parseInt(star.getAttribute('data-value'));
+
+ // Find and update the input
+ const input = document.getElementById(inputId);
+ if (input) {
+ input.value = value;
+ console.log('Updated input value to:', value);
+ } else {
+ console.error('Input element not found with ID:', inputId);
+ }
+
+ // Update the stars
+ const container = star.closest('.rating-stars');
+ if (container) {
+ const stars = container.querySelectorAll('.rating-star');
+ stars.forEach(s => {
+ const starValue = parseInt(s.getAttribute('data-value'));
+ if (starValue <= value) {
+ s.classList.remove('far');
+ s.classList.add('fas');
+ s.classList.add('selected');
+ } else {
+ s.classList.remove('fas');
+ s.classList.add('far');
+ s.classList.remove('selected');
+ }
+ });
+ } else {
+ console.error('Rating container not found');
+ }
+
+ // Hide error message if any
+ let errorId;
+ if (inputId === 'teacher_rating') {
+ errorId = 'teacher-rating-error';
+ } else if (inputId === 'institute_rating') {
+ errorId = 'institute-rating-error';
+ } else if (inputId === 'teacher_modal_rating') {
+ errorId = 'teacher-modal-rating-error';
+ }
+
+ if (errorId) {
+ const errorElement = document.getElementById(errorId);
+ if (errorElement) {
+ errorElement.style.display = 'none';
+ }
+ }
+
+ return false;
+}
+
+// Initialize rating functionality when DOM is loaded
+document.addEventListener('DOMContentLoaded', () => {
+ console.log("DOM fully loaded");
+ setupStarRating();
+
+ // Add an additional check after a short delay to catch dynamically loaded content
+ setTimeout(setupStarRating, 500);
+});
+
+// Initialize on document load
+document.addEventListener('DOMContentLoaded', function() {
+ console.log('Rating.js loaded');
+
+ // Apply rating stars to static ratings (read-only)
+ const ratingContainers = document.querySelectorAll('.rating');
+ console.log('Found', ratingContainers.length, 'static rating containers');
+
+ ratingContainers.forEach(container => {
+ const stars = container.querySelectorAll('i');
+ stars.forEach(star => {
+ if (star.classList.contains('fas')) {
+ star.style.color = '#FFC107';
+ }
+ });
+ });
+
+ // Apply hover effects to rating stars
+ const ratingStars = document.querySelectorAll('.rating-star');
+ console.log('Found', ratingStars.length, 'interactive rating stars');
+
+ ratingStars.forEach(star => {
+ star.addEventListener('mouseover', function() {
+ // Find all stars in the same container
+ const container = this.closest('.rating-stars');
+ if (container) {
+ const stars = container.querySelectorAll('.rating-star');
+ const value = parseInt(this.getAttribute('data-value'));
+
+ stars.forEach(s => {
+ const starValue = parseInt(s.getAttribute('data-value'));
+ if (starValue <= value) {
+ s.classList.add('hover');
+ } else {
+ s.classList.remove('hover');
+ }
+ });
+ }
+ });
+
+ star.addEventListener('mouseout', function() {
+ // Find all stars in the same container
+ const container = this.closest('.rating-stars');
+ if (container) {
+ const stars = container.querySelectorAll('.rating-star');
+ stars.forEach(s => {
+ s.classList.remove('hover');
+ });
+ }
+ });
+ });
+});
+
+// Make function globally available
+window.selectRating = selectRating;
\ No newline at end of file
diff --git a/static/js/student_modals.js b/static/js/student_modals.js
new file mode 100644
index 0000000..be58d35
--- /dev/null
+++ b/static/js/student_modals.js
@@ -0,0 +1,169 @@
+// Functions for handling student modals
+
+// Close PDF viewer modal
+function closePdfViewerModal() {
+ document.getElementById('pdfViewerModal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+// Close subject files modal
+function closeSubjectFilesModal() {
+ document.getElementById('subjectFilesModal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+// Open subject files modal and load content
+function openSubjectFilesModal(subjectId) {
+ const modal = document.getElementById('subjectFilesModal');
+ const filesList = document.getElementById('subjectFilesList');
+
+ modal.style.display = 'block';
+ document.body.style.overflow = 'hidden';
+
+ filesList.innerHTML = `
+
+
+ Loading...
+
+
Loading subject files...
+
+ `;
+
+ fetch(`/app/subject/${subjectId}/files/`)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response.json();
+ })
+ .then(data => {
+ if (!data.success) {
+ filesList.innerHTML = `
+
+ ${data.message || 'No files available for this subject'}
+
+ `;
+ return;
+ }
+
+ if (!data.files || data.files.length === 0) {
+ filesList.innerHTML = `
+
+ No study materials available for this subject.
+
+ `;
+ return;
+ }
+
+ filesList.innerHTML = data.files.map(file => {
+ const fileExtension = file.file_url ? file.file_url.split('.').pop().toLowerCase() : '';
+ const iconClass = getFileIcon(fileExtension);
+ const fileIcon = ` `;
+
+ return `
+
+
+
+
+ ${fileIcon}
+ ${file.title}
+
+
${file.description || 'No description'}
+
+ ${file.file_type === 'syllabus'
+ ? 'Syllabus '
+ : 'Study Material '}
+ ${file.uploaded_by ? `Uploaded by: ${file.uploaded_by} ` : ''}
+ ${file.uploaded_at ? `• ${formatDate(file.uploaded_at)} ` : ''}
+
+
+
+ ${file.file_url ? `
+
+ ` : `
+
+ File not available
+
+ `}
+
+
+
+ `;
+ }).join('');
+ })
+ .catch(error => {
+ console.error('Error fetching subject files:', error);
+ filesList.innerHTML = `
+
+ Failed to load files. Please try again.
+
+ `;
+ });
+}
+
+// Get appropriate icon based on file type
+function getFileIcon(fileType) {
+ const iconMap = {
+ 'pdf': 'fa-file-pdf',
+ 'doc': 'fa-file-word',
+ 'docx': 'fa-file-word',
+ 'xls': 'fa-file-excel',
+ 'xlsx': 'fa-file-excel',
+ 'ppt': 'fa-file-powerpoint',
+ 'pptx': 'fa-file-powerpoint',
+ 'txt': 'fa-file-alt',
+ 'zip': 'fa-file-archive',
+ 'rar': 'fa-file-archive',
+ 'image': 'fa-file-image',
+ 'video': 'fa-file-video',
+ 'audio': 'fa-file-audio'
+ };
+ return iconMap[fileType] || 'fa-file';
+}
+
+// Format file size for display
+function formatFileSize(bytes) {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+}
+
+// Format date for display
+function formatDate(dateString) {
+ if (!dateString) return '';
+ const date = new Date(dateString);
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ });
+}
+
+// Initialize event listeners when the DOM is loaded
+document.addEventListener('DOMContentLoaded', function() {
+ // Handle Escape key to close modals
+ document.addEventListener('keydown', function(event) {
+ if (event.key === 'Escape') {
+ // Close all open modals
+ const pdfModal = document.getElementById('pdfViewerModal');
+ const filesModal = document.getElementById('subjectFilesModal');
+
+ if (pdfModal && pdfModal.style.display === 'block') {
+ closePdfViewerModal();
+ }
+
+ if (filesModal && filesModal.style.display === 'block') {
+ closeSubjectFilesModal();
+ }
+ }
+ });
+});
\ No newline at end of file
diff --git a/static/js/tabs.js b/static/js/tabs.js
new file mode 100644
index 0000000..bc4e612
--- /dev/null
+++ b/static/js/tabs.js
@@ -0,0 +1,62 @@
+// Setup custom tabs
+function setupCustomTabs() {
+ const tabButtons = document.querySelectorAll(".custom-tab-btn");
+ if (tabButtons.length > 0) {
+ tabButtons.forEach((button) => {
+ button.addEventListener("click", function() {
+ // Remove active class from all buttons
+ tabButtons.forEach((btn) => btn.classList.remove("active"));
+
+ // Add active class to clicked button
+ this.classList.add("active");
+
+ // Hide all content
+ document.querySelectorAll(".custom-tab-content").forEach((content) => {
+ content.classList.remove("active");
+ });
+
+ // Show target content
+ const target = this.getAttribute("data-target");
+ document.getElementById(target).classList.add("active");
+ });
+ });
+ }
+}
+
+// Setup feedback tabs
+function setupFeedbackTabs() {
+ const teacherTabBtn = document.getElementById("teacher-tab-btn");
+ const instituteTabBtn = document.getElementById("institute-tab-btn");
+
+ if (teacherTabBtn && instituteTabBtn) {
+ teacherTabBtn.addEventListener("click", function() {
+ switchFeedbackTab("teacher");
+ });
+
+ instituteTabBtn.addEventListener("click", function() {
+ switchFeedbackTab("institute");
+ });
+ }
+}
+
+// Tab switching function
+function switchFeedbackTab(tabId) {
+ // Hide all tab contents
+ document.querySelectorAll(".tab-pane").forEach((tab) => {
+ tab.classList.remove("active");
+ });
+
+ // Deactivate all tab buttons
+ document.querySelectorAll(".feedback-tab-btn").forEach((btn) => {
+ btn.classList.remove("active");
+ });
+
+ // Show selected tab content and activate button
+ document.getElementById(`${tabId}-tab-content`).classList.add("active");
+ document.getElementById(`${tabId}-tab-btn`).classList.add("active");
+}
+
+// Make functions globally accessible
+window.setupCustomTabs = setupCustomTabs;
+window.setupFeedbackTabs = setupFeedbackTabs;
+window.switchFeedbackTab = switchFeedbackTab;
\ No newline at end of file
diff --git a/static/js/teacher-attendance.js b/static/js/teacher-attendance.js
new file mode 100644
index 0000000..669ba9f
--- /dev/null
+++ b/static/js/teacher-attendance.js
@@ -0,0 +1,379 @@
+// Attendance Functions for Teacher Dashboard
+
+// Constants
+const ATTENDANCE_MODAL_ID = 'attendanceModal';
+const QUICK_ATTENDANCE_FORM_ID = 'quickAttendanceForm';
+const ATTENDANCE_FORM_ID = 'attendanceForm';
+
+// Attendance Modal Functions
+async function openAttendanceModal(routineId, date) {
+ try {
+ console.log('Opening attendance modal for routine:', routineId, 'date:', date);
+
+ // Validate and clean inputs
+ routineId = routineId?.trim().replace(/['"]+/g, '');
+ date = date || getCurrentDate();
+
+ if (!routineId) {
+ throw new Error('Invalid routine ID');
+ }
+
+ // Get required elements
+ const modal = document.getElementById(ATTENDANCE_MODAL_ID);
+ const modalRoutineId = document.getElementById('modalRoutineId');
+ const modalDate = document.getElementById('modalDate');
+ const modalStudentList = document.getElementById('modalStudentList');
+
+ if (!modal || !modalRoutineId || !modalDate || !modalStudentList) {
+ throw new Error('Required modal elements not found');
+ }
+
+ // Set form values
+ modalRoutineId.value = routineId;
+ modalDate.value = date;
+
+ // Show loading state
+ showLoadingSpinner(modalStudentList, 'Loading students...');
+
+ // Show modal
+ showModal(ATTENDANCE_MODAL_ID);
+
+ // Fetch students
+ const data = await apiRequest(`${API_BASE_URL}/get-students/?routine_id=${routineId}`);
+
+ if (data.error) {
+ throw new Error(data.error);
+ }
+
+ // Render student list with enhanced UI
+ renderStudentList(modalStudentList, data.students);
+
+ } catch (error) {
+ console.error('Error in openAttendanceModal:', error);
+ showError(document.getElementById('modalStudentList'), error);
+ hideModal(ATTENDANCE_MODAL_ID);
+ }
+}
+
+function renderStudentList(container, students) {
+ if (!container || !Array.isArray(students)) return;
+
+ container.innerHTML = `
+
+
+ `;
+}
+
+function closeAttendanceModal() {
+ hideModal(ATTENDANCE_MODAL_ID);
+ resetForm(QUICK_ATTENDANCE_FORM_ID);
+}
+
+// Attendance View Functions
+function toggleAttendanceView(view) {
+ const headerSection = document.getElementById('attendanceHeader');
+ const manageView = document.getElementById('manageAttendanceView');
+ const routineSelectionForm = document.getElementById('routineSelectionForm');
+ const attendanceFormContainer = document.getElementById('attendanceFormContainer');
+
+ if (view === 'recent') {
+ // Show recent view
+ headerSection.style.display = 'block';
+ manageView.style.display = 'none';
+
+ // Reset form and container
+ if (attendanceFormContainer) {
+ attendanceFormContainer.style.display = 'none';
+ attendanceFormContainer.innerHTML = '';
+ }
+
+ // Safely reset routine selection form
+ if (routineSelectionForm) {
+ routineSelectionForm.style.display = 'block';
+ // Reset form elements individually to avoid potential issues
+ const routineSelect = routineSelectionForm.querySelector('#routineSelect');
+ const dateSelect = routineSelectionForm.querySelector('#dateSelect');
+
+ if (routineSelect) {
+ routineSelect.selectedIndex = 0;
+ }
+
+ if (dateSelect) {
+ dateSelect.value = '';
+ }
+
+ // Clear any error messages
+ routineSelectionForm.querySelectorAll('.alert').forEach(alert => alert.remove());
+ }
+ } else {
+ // Show manage view
+ headerSection.style.display = 'none';
+ manageView.style.display = 'block';
+
+ // Load data if routine and date are selected
+ const routineSelect = document.getElementById('routineSelect');
+ const dateSelect = document.getElementById('dateSelect');
+
+ if (routineSelect?.value && dateSelect?.value) {
+ loadAttendanceData();
+ } else if (attendanceFormContainer) {
+ attendanceFormContainer.style.display = 'none';
+ attendanceFormContainer.innerHTML = '';
+ }
+ }
+}
+
+// Attendance Data Functions
+async function editAttendance(routineId, date) {
+ try {
+ console.log('Editing attendance for routine:', routineId, 'date:', date);
+
+ // Switch to manage view
+ toggleAttendanceView('manage');
+
+ // Hide routine selection form
+ const routineSelectionForm = document.getElementById('routineSelectionForm');
+ if (routineSelectionForm) {
+ routineSelectionForm.style.display = 'none';
+ }
+
+ // Update form inputs
+ const attendanceForm = document.getElementById(ATTENDANCE_FORM_ID);
+ if (attendanceForm) {
+ const routineInput = attendanceForm.querySelector('input[name="routine_id"]');
+ const dateInput = attendanceForm.querySelector('input[name="date"]');
+ if (routineInput) routineInput.value = routineId;
+ if (dateInput) dateInput.value = date;
+ }
+
+ // Show loading state
+ const container = document.getElementById('attendanceFormContainer');
+ if (!container) return;
+
+ container.style.display = 'block';
+ showLoadingSpinner(container, 'Loading attendance data...');
+
+ // Fetch attendance data
+ const [attendanceData, studentsData] = await Promise.all([
+ apiRequest(`${API_BASE_URL}/get-attendance-form/?routine_id=${routineId}&date=${date}`),
+ apiRequest(`${API_BASE_URL}/get-students/?routine_id=${routineId}`)
+ ]);
+
+ if (attendanceData.error || studentsData.error) {
+ throw new Error(attendanceData.error || studentsData.error);
+ }
+
+ // Render attendance form
+ renderAttendanceForm(container, routineId, date, attendanceData, studentsData.students);
+
+ } catch (error) {
+ console.error('Error editing attendance:', error);
+ if (container) {
+ showError(container, error);
+ }
+ }
+}
+
+function renderAttendanceForm(container, routineId, date, attendanceData, students) {
+ if (!container || !Array.isArray(students)) return;
+
+ container.innerHTML = `
+
+ `;
+}
+
+// Attendance Marking Functions
+function markAllPresent() {
+ const checkboxes = document.querySelectorAll(`#${ATTENDANCE_FORM_ID} input[type="checkbox"], #${QUICK_ATTENDANCE_FORM_ID} input[type="checkbox"]`);
+ checkboxes.forEach(checkbox => {
+ if (checkbox.id !== 'teacherAttend' && checkbox.id !== 'modalTeacherAttend') {
+ checkbox.checked = true;
+ }
+ });
+}
+
+function markAllAbsent() {
+ const checkboxes = document.querySelectorAll(`#${ATTENDANCE_FORM_ID} input[type="checkbox"], #${QUICK_ATTENDANCE_FORM_ID} input[type="checkbox"]`);
+ checkboxes.forEach(checkbox => {
+ if (checkbox.id !== 'teacherAttend' && checkbox.id !== 'modalTeacherAttend') {
+ checkbox.checked = false;
+ }
+ });
+}
+
+async function loadAttendanceData() {
+ try {
+ console.log('Loading attendance data...');
+ const routineId = document.getElementById('routineSelect')?.value;
+ const date = document.getElementById('dateSelect')?.value;
+ const container = document.getElementById('attendanceFormContainer');
+
+ if (!container) return;
+
+ if (!routineId || !date) {
+ container.style.display = 'none';
+ container.innerHTML = '';
+ return;
+ }
+
+ container.style.display = 'block';
+ showLoadingSpinner(container, 'Loading attendance data...');
+
+ const [attendanceData, studentsData] = await Promise.all([
+ apiRequest(`${API_BASE_URL}/get-attendance-form/?routine_id=${routineId}&date=${date}`),
+ apiRequest(`${API_BASE_URL}/get-students/?routine_id=${routineId}`)
+ ]);
+
+ if (attendanceData.error || studentsData.error) {
+ throw new Error(attendanceData.error || studentsData.error);
+ }
+
+ renderAttendanceForm(container, routineId, date, attendanceData, studentsData.students);
+
+ } catch (error) {
+ console.error('Error loading attendance data:', error);
+ if (container) {
+ showError(container, error);
+ }
+ }
+}
+
+// Form Submission Handler
+async function saveQuickAttendance() {
+ try {
+ const form = document.getElementById(QUICK_ATTENDANCE_FORM_ID);
+ if (!form) return;
+
+ const formData = new FormData(form);
+ const data = Object.fromEntries(formData.entries());
+
+ // Validate required fields
+ if (!data.routine_id || !data.date) {
+ throw new Error('Missing required fields');
+ }
+
+ // Show loading state
+ const submitButton = form.querySelector('button[type="submit"]');
+ if (submitButton) {
+ submitButton.disabled = true;
+ submitButton.innerHTML = ' Saving...';
+ }
+
+ // Submit attendance
+ const response = await apiRequest(`${API_BASE_URL}/save-attendance/`, {
+ method: 'POST',
+ body: JSON.stringify(data)
+ });
+
+ if (response.error) {
+ throw new Error(response.error);
+ }
+
+ // Show success message
+ showSuccess(form, 'Attendance saved successfully');
+
+ // Close modal and reset form
+ setTimeout(() => {
+ closeAttendanceModal();
+ }, 1500);
+
+ } catch (error) {
+ console.error('Error saving attendance:', error);
+ showError(form, error);
+
+ // Reset submit button
+ const submitButton = form.querySelector('button[type="submit"]');
+ if (submitButton) {
+ submitButton.disabled = false;
+ submitButton.innerHTML = ' Save Attendance';
+ }
+ }
+}
\ No newline at end of file
diff --git a/static/js/teacher-dashboard.js b/static/js/teacher-dashboard.js
new file mode 100644
index 0000000..bcbfae0
--- /dev/null
+++ b/static/js/teacher-dashboard.js
@@ -0,0 +1,9 @@
+// Main teacher dashboard entry point - imports all modules
+
+// Load utility functions first
+document.write('');
+
+// Load feature modules
+document.write('');
+document.write('');
+document.write('');
\ No newline at end of file
diff --git a/static/js/teacher-init.js b/static/js/teacher-init.js
new file mode 100644
index 0000000..829c1ad
--- /dev/null
+++ b/static/js/teacher-init.js
@@ -0,0 +1,243 @@
+// Initialization code for the teacher dashboard
+
+// Initialize when document is ready
+document.addEventListener('DOMContentLoaded', function() {
+ try {
+ console.log('Initializing teacher dashboard and attendance functions...');
+
+ // Handle hash navigation with priority
+ const hash = window.location.hash;
+ if (hash) {
+ // Remove the # and get the section name
+ const sectionId = hash.substring(1);
+ console.log('Found hash navigation target:', sectionId);
+
+ // Make all sections inactive and hidden first
+ document.querySelectorAll('.content-section').forEach(section => {
+ section.classList.remove('active');
+ section.style.display = 'none';
+ });
+
+ // Then activate just the target section
+ const targetSection = document.getElementById(sectionId);
+ if (targetSection) {
+ console.log('Activating section from hash:', sectionId);
+ targetSection.classList.add('active');
+ targetSection.style.display = 'block';
+
+ // Also activate the correct nav item
+ document.querySelectorAll('.nav-item').forEach(item => {
+ item.classList.remove('active');
+ const navSection = item.getAttribute('data-section');
+ if (navSection === sectionId) {
+ item.classList.add('active');
+ }
+ });
+
+ // If it's subjects, make sure the subjects nav is active
+ if (sectionId === 'subjectsSection') {
+ document.querySelectorAll('.nav-item').forEach(item => {
+ const navSection = item.getAttribute('data-section');
+ if (navSection === 'subjects') {
+ item.classList.add('active');
+ }
+ });
+ }
+ }
+ }
+
+ // Hide attendance form container by default
+ const attendanceFormContainer = document.getElementById('attendanceFormContainer');
+ if (attendanceFormContainer) {
+ attendanceFormContainer.style.display = 'none';
+ }
+
+ // Set today's date as default for date inputs
+ const dateInputs = document.querySelectorAll('input[type="date"]');
+ const today = new Date().toISOString().split('T')[0];
+
+ dateInputs.forEach(input => {
+ // Don't set a default value for the attendance date input
+ if (input.id !== 'dateSelect' && !input.value) {
+ input.value = today;
+ console.log('Set date for input:', input.id);
+ // Trigger any change event listeners
+ const event = new Event('change');
+ input.dispatchEvent(event);
+ }
+ });
+
+ // Add event listeners for routine selection form
+ const routineSelect = document.getElementById('routineSelect');
+ const dateSelect = document.getElementById('dateSelect');
+
+ if (routineSelect) {
+ // Remove auto-selection of first routine option
+ // We want nothing selected by default
+
+ routineSelect.addEventListener('change', function() {
+ console.log('Routine changed:', this.value);
+ loadAttendanceData();
+ });
+ }
+
+ if (dateSelect) {
+ dateSelect.addEventListener('change', function() {
+ console.log('Date changed:', this.value);
+ loadAttendanceData();
+ });
+ }
+
+ // Add event listeners for edit buttons
+ document.querySelectorAll('.edit-attendance-btn').forEach(button => {
+ button.addEventListener('click', function() {
+ const routineId = this.getAttribute('data-routine-id');
+ const date = this.getAttribute('data-date');
+ console.log('Edit button clicked:', { routineId, date });
+ editAttendance(routineId, date);
+ });
+ });
+
+ // Add event listeners for today's classes attendance buttons
+ document.querySelectorAll('.take-attendance-btn, .attendance-btn, .today-attendance-btn').forEach(button => {
+ button.addEventListener('click', function() {
+ console.log('Today attendance button clicked');
+ const routineId = this.getAttribute('data-routine-id');
+ const date = this.getAttribute('data-date') || today;
+
+ if (routineId) {
+ console.log('Taking attendance for routine:', routineId, 'date:', date);
+
+ // If this is a quick attendance button, open the modal
+ if (this.classList.contains('quick-attendance')) {
+ openAttendanceModal(routineId, date);
+ } else {
+ // Otherwise go to manage attendance view
+ editAttendance(routineId, date);
+ }
+ } else {
+ console.error('Missing routine ID for attendance button');
+ }
+ });
+ });
+
+ // Load initial data if values are present
+ if (routineSelect && dateSelect && routineSelect.value && dateSelect.value) {
+ loadAttendanceData();
+ }
+
+ // Initialize file upload form submission
+ const uploadForm = document.getElementById('uploadFileForm');
+ if (uploadForm) {
+ uploadForm.addEventListener('submit', function(e) {
+ e.preventDefault();
+
+ const formData = new FormData(uploadForm);
+ const submitBtn = uploadForm.querySelector('button[type="submit"]');
+ const responseDiv = document.getElementById('uploadResponse');
+
+ // Disable the submit button and show loading state
+ if (submitBtn) {
+ submitBtn.disabled = true;
+ submitBtn.innerHTML = ' Uploading...';
+ }
+
+ fetch('/app/manage-subject-files/', {
+ method: 'POST',
+ body: formData,
+ credentials: 'same-origin',
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (responseDiv) {
+ responseDiv.style.display = 'block';
+ responseDiv.className = data.success ? 'alert alert-success' : 'alert alert-danger';
+ responseDiv.innerHTML = data.message;
+ }
+
+ if (data.success) {
+ // Reload materials after successful upload
+ setTimeout(() => {
+ // Get the subject ID that was selected
+ const subjectId = document.getElementById('subject_id').value;
+ loadSubjectMaterials(subjectId);
+ closeUploadModal();
+ }, 1500);
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ if (responseDiv) {
+ responseDiv.style.display = 'block';
+ responseDiv.className = 'alert alert-danger';
+ responseDiv.innerHTML = 'An error occurred while uploading the file. Please try again.';
+ }
+ })
+ .finally(() => {
+ if (submitBtn) {
+ submitBtn.disabled = false;
+ submitBtn.innerHTML = ' Upload';
+ }
+ });
+ });
+ }
+
+ // Initialize delete confirmation button
+ const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
+ if (confirmDeleteBtn) {
+ confirmDeleteBtn.addEventListener('click', function() {
+ if (!fileIdToDelete) {
+ closeConfirmationModal();
+ return;
+ }
+
+ // Disable the button and show loading state
+ confirmDeleteBtn.disabled = true;
+ confirmDeleteBtn.innerHTML = ' Deleting...';
+
+ fetch('/app/delete-subject-file/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
+ },
+ body: JSON.stringify({ file_id: fileIdToDelete })
+ })
+ .then(response => response.json())
+ .then(data => {
+ closeConfirmationModal();
+
+ if (data.success) {
+ // Store the current subject ID if displayed in the UI
+ const currentSubjectHeading = document.querySelector('#subjectMaterialsContent .card-header h5');
+ let currentSubjectId = null;
+
+ if (currentSubjectHeading) {
+ // Find the subject ID from the upload button
+ const uploadBtn = currentSubjectHeading.closest('.card-header').querySelector('[data-subject-id]');
+ if (uploadBtn) {
+ currentSubjectId = uploadBtn.getAttribute('data-subject-id');
+ }
+ }
+
+ // Reload the materials section with the same subject if we had one
+ loadSubjectMaterials(currentSubjectId);
+ } else {
+ alert(data.message || 'Error deleting file.');
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ closeConfirmationModal();
+ alert('An error occurred while deleting the file. Please try again.');
+ })
+ .finally(() => {
+ confirmDeleteBtn.disabled = false;
+ confirmDeleteBtn.innerHTML = ' Delete';
+ });
+ });
+ }
+ } catch (error) {
+ console.error('Error initializing teacher dashboard:', error);
+ }
+});
\ No newline at end of file
diff --git a/static/js/teacher-materials.js b/static/js/teacher-materials.js
new file mode 100644
index 0000000..304af48
--- /dev/null
+++ b/static/js/teacher-materials.js
@@ -0,0 +1,941 @@
+// Subject Materials Section Functions
+
+function showSection(sectionId, forceOrSubjectId = null) {
+ console.log('Showing section:', sectionId, 'force/subjectId:', forceOrSubjectId);
+
+ // Special handling for subject materials section using the toggle function
+ if (sectionId === 'subjectMaterialsSection') {
+ const subjectId = forceOrSubjectId;
+ toggleSubjectView('materials');
+
+ // Get the section
+ const section = document.getElementById(sectionId);
+ if (section) {
+ // Make sure the header div is visible and properly styled
+ const headerDiv = section.querySelector('.d-flex.justify-content-between.align-items-center');
+ if (headerDiv) {
+ // Ensure header is visible with !important
+ headerDiv.style.cssText = "display: flex !important; opacity: 1; visibility: visible;";
+
+ // Make the heading more prominent
+ const heading = headerDiv.querySelector('h3');
+ if (heading) {
+ heading.style.fontWeight = 'bold';
+ heading.style.color = '#3F51B5';
+ }
+ }
+
+ // Now load the content
+ loadSubjectMaterials(subjectId);
+ }
+
+ return;
+ }
+
+ // Special handling for subjects section using the toggle function
+ if (sectionId === 'subjectsSection') {
+ toggleSubjectView('subjects');
+ return;
+ }
+
+ // Handle the case where we're being called from the back button
+ const forceDisplay = forceOrSubjectId === true;
+ // If not a boolean, treat as subjectId
+ const subjectId = typeof forceOrSubjectId === 'boolean' ? null : forceOrSubjectId;
+
+ // Special handling for forced navigation to subjects
+ if (sectionId === 'subjectsSection' && forceDisplay) {
+ console.log('FORCE NAVIGATION to subjects section');
+
+ // Hide all other sections first
+ document.querySelectorAll('.content-section').forEach(sect => {
+ if (sect.id !== 'subjectsSection') {
+ sect.style.display = 'none';
+ sect.classList.remove('active');
+ }
+ });
+
+ // Activate subjects section with high specificity
+ const subjectsSection = document.getElementById('subjectsSection');
+ if (subjectsSection) {
+ // Force display using multiple methods
+ subjectsSection.style.cssText = 'display: block !important; opacity: 1; visibility: visible;';
+ subjectsSection.classList.add('active');
+
+ // Force a redraw
+ void subjectsSection.offsetHeight;
+
+ // Add active class to subjects nav
+ document.querySelectorAll('.nav-item').forEach(item => {
+ const section = item.getAttribute('data-section');
+ if (section === 'subjects') {
+ item.classList.add('active');
+ } else {
+ item.classList.remove('active');
+ }
+ });
+
+ // Update URL hash
+ window.location.hash = 'subjectsSection';
+
+ // Scroll to top
+ window.scrollTo(0, 0);
+
+ console.log('Force navigation complete.');
+ return;
+ }
+ }
+
+ // Special handling for subjects section to correctly set the active nav item
+ if (sectionId === 'subjectsSection') {
+ // Update URL hash to track the section
+ window.location.hash = 'subjectsSection';
+ console.log('Setting hash to subjectsSection');
+ }
+
+ // Debug output - list all sections in DOM
+ const allSections = document.querySelectorAll('.content-section');
+ console.log('Available sections:');
+ allSections.forEach(section => {
+ console.log('- Section ID:', section.id, 'Display:', section.style.display, 'Classes:', section.className);
+ });
+
+ // Hide all sections
+ document.querySelectorAll('.content-section').forEach(section => {
+ section.classList.remove('active');
+ section.style.display = 'none';
+ console.log('Hiding section:', section.id);
+ });
+
+ // First remove active class from all nav items
+ document.querySelectorAll('.nav-item').forEach(item => {
+ item.classList.remove('active');
+ });
+
+ // Then add active class to the correct nav item
+ document.querySelectorAll('.nav-item').forEach(item => {
+ const navSection = item.getAttribute('data-section');
+ console.log('Nav item section:', navSection, 'comparing with:', sectionId);
+
+ if (navSection === sectionId ||
+ (sectionId === 'subjectMaterialsSection' && navSection === 'subjects') ||
+ (sectionId === 'subjectsSection' && navSection === 'subjects')) {
+ item.classList.add('active');
+ console.log('Activated nav item:', navSection);
+ }
+ });
+
+ // Show the selected section
+ const selectedSection = document.getElementById(sectionId);
+ if (selectedSection) {
+ console.log('Found section to show:', sectionId);
+ selectedSection.classList.add('active');
+ selectedSection.style.display = 'block';
+
+ // Force browser to recognize the display change
+ setTimeout(() => {
+ selectedSection.style.opacity = '0.99';
+ setTimeout(() => {
+ selectedSection.style.opacity = '1';
+ }, 10);
+ }, 10);
+ } else {
+ console.error('Could not find section with ID:', sectionId);
+ }
+
+ // Special handling for subjects section to ensure it's visible
+ if (sectionId === 'subjectsSection') {
+ const subjectsSection = document.getElementById('subjectsSection');
+ if (subjectsSection) {
+ console.log('Forcing subjects section to display');
+ subjectsSection.style.display = 'block';
+ subjectsSection.classList.add('active');
+
+ // The following forces a reflow
+ void subjectsSection.offsetWidth;
+ } else {
+ console.error('Subjects section element not found in DOM');
+ }
+ }
+
+ // Scroll to top
+ window.scrollTo(0, 0);
+}
+
+// Custom API function to get subject materials directly
+function getSubjectMaterials(subjectId = null) {
+ return new Promise((resolve, reject) => {
+ // Get the CSRF token
+ const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
+
+ // Build the URL for the API endpoint
+ let url = '/app/get-teacher-subjects/';
+
+ console.log('Fetching teacher subjects from:', url);
+
+ // First fetch the subjects
+ fetch(url, {
+ headers: {
+ 'X-CSRFToken': csrftoken
+ }
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+ return response.json();
+ })
+ .then(subjectsData => {
+ console.log('Teacher subjects data:', subjectsData);
+
+ if (!subjectsData.success || !subjectsData.subjects || subjectsData.subjects.length === 0) {
+ console.log('No subjects found or empty response');
+ resolve({ subjects: [] });
+ return;
+ }
+
+ let subjects = subjectsData.subjects;
+
+ // If a specific subject is requested, filter the subjects
+ if (subjectId) {
+ console.log('Filtering subjects by ID:', subjectId);
+ subjects = subjects.filter(subject => subject.id.toString() === subjectId.toString());
+
+ if (subjects.length === 0) {
+ console.log('No matching subject found for ID:', subjectId);
+ resolve({ subjects: [] });
+ return;
+ }
+ }
+
+ // Now we need to get the files for each subject
+ // We'll use the manage_subject_files endpoint with a GET request
+ // and parse the HTML response to extract the files
+
+ // Create a result object to store the subjects with their files
+ const result = {
+ subjects: []
+ };
+
+ // Process each subject to get its files
+ const promises = subjects.map(subject => {
+ const filesUrl = `/app/manage-subject-files/?subject=${subject.id}`;
+ console.log(`Fetching files for subject ${subject.name} (ID: ${subject.id}) from: ${filesUrl}`);
+
+ return fetch(filesUrl, {
+ headers: {
+ 'X-CSRFToken': csrftoken
+ }
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+ return response.text();
+ })
+ .then(html => {
+ // Parse the HTML to extract the files
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+
+ console.log(`Parsing HTML response for subject ${subject.name}`);
+
+ // Try different ways to find the subject card
+ let subjectCard = null;
+ let cardBody = null;
+
+ // Method 1: Find by exact heading text
+ const headings = Array.from(doc.querySelectorAll('.card-header h5, .card-title'));
+ subjectCard = headings.find(h5 => h5.textContent.trim() === subject.name);
+
+ if (subjectCard) {
+ console.log(`Found subject card for ${subject.name} using heading match`);
+ cardBody = subjectCard.closest('.card')?.querySelector('.card-body');
+ } else {
+ // Method 2: Find by card with subject ID in attributes
+ const cards = doc.querySelectorAll('.card[data-subject-id="' + subject.id + '"], .card[id*="subject-' + subject.id + '"]');
+ if (cards.length > 0) {
+ console.log(`Found subject card for ${subject.name} using ID attribute`);
+ subjectCard = cards[0];
+ cardBody = subjectCard.querySelector('.card-body');
+ } else {
+ // Method 3: Just take the first card if there's only one subject requested
+ if (subjectId) {
+ const allCards = doc.querySelectorAll('.card');
+ if (allCards.length > 0) {
+ console.log(`Using first card for ${subject.name} as fallback`);
+ subjectCard = allCards[0];
+ cardBody = subjectCard.querySelector('.card-body');
+ }
+ }
+ }
+ }
+
+ const files = [];
+ let hasSyllabus = false;
+
+ // If we found a card body with a table, extract the files
+ if (cardBody) {
+ // Look for table rows
+ const fileRows = cardBody.querySelectorAll('table tbody tr');
+ console.log(`Found ${fileRows.length} file rows for subject ${subject.name}`);
+
+ fileRows.forEach(row => {
+ const columns = row.querySelectorAll('td');
+ if (columns.length >= 3) { // Need at least 3 columns
+ const title = columns[0].textContent.trim();
+ const description = columns.length > 1 ? columns[1].textContent.trim() : '';
+ const date = columns.length > 2 ? columns[2].textContent.trim() : '';
+
+ // Extract file ID from the delete button
+ const deleteBtn = row.querySelector('button[onclick*="deleteFile"], button[data-file-id]');
+ let fileId = null;
+
+ if (deleteBtn) {
+ // Try onclick attribute
+ const onClickAttr = deleteBtn.getAttribute('onclick');
+ if (onClickAttr) {
+ const match = onClickAttr.match(/deleteFile\(['"]([^'"]*)['"]\)/);
+ if (match && match[1]) {
+ fileId = match[1];
+ }
+ }
+
+ // Try data attribute if onclick failed
+ if (!fileId) {
+ fileId = deleteBtn.getAttribute('data-file-id');
+ }
+ }
+
+ // Extract file URL from the download link
+ const actionCell = columns[columns.length - 1]; // Last column usually has actions
+ const downloadLink = actionCell.querySelector('a[href]:not([href="#"])');
+ let fileUrl = null;
+
+ if (downloadLink) {
+ fileUrl = downloadLink.getAttribute('href');
+ }
+
+ // Check if this is a syllabus file based on title or other indicators
+ const isSyllabus = title.toLowerCase().includes('syllabus');
+
+ // Set the syllabus flag for the subject if this file is a syllabus
+ if (isSyllabus) {
+ hasSyllabus = true;
+ }
+
+ files.push({
+ id: fileId,
+ title: title,
+ description: description,
+ uploaded_on: date,
+ file_url: fileUrl,
+ file_type: isSyllabus ? 'syllabus' : 'material'
+ });
+
+ console.log(`Added file: ${title} for subject ${subject.name}`);
+ }
+ });
+ } else {
+ console.log(`Could not find card body for subject ${subject.name}`);
+ }
+
+ // Check if the subject has a syllabus PDF using the View Syllabus button existence
+ const syllabusBtn = doc.querySelector(`a[href*="/subject/${subject.id}/syllabus/"], button[onclick*="viewSyllabus('${subject.id}')"]`);
+ if (syllabusBtn) {
+ console.log(`Found syllabus button for subject ${subject.name}`);
+ hasSyllabus = true;
+ }
+
+ // Add this subject with its files to the result
+ result.subjects.push({
+ ...subject,
+ materials: files,
+ has_syllabus: hasSyllabus
+ });
+
+ console.log(`Finished processing subject ${subject.name}, found ${files.length} files`);
+ });
+ });
+
+ // Wait for all promises to resolve
+ Promise.all(promises)
+ .then(() => {
+ console.log('Final result:', result);
+ resolve(result);
+ })
+ .catch(error => {
+ console.error('Error processing subjects:', error);
+ reject(error);
+ });
+ })
+ .catch(error => {
+ console.error('Error fetching subjects:', error);
+ reject(error);
+ });
+ });
+}
+
+function loadSubjectMaterials(subjectId = null) {
+ const contentDiv = document.getElementById('subjectMaterialsContent');
+ if (!contentDiv) {
+ console.error('Subject materials content container not found');
+ return;
+ }
+
+ // Show loading state in content area only
+ contentDiv.innerHTML = `
+
+
+ Loading...
+
+
Loading subject materials...
+
+ `;
+
+ 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 = `
+ If materials don't load automatically, try:
+
+ Open Subject Materials in New Tab
+
+ `;
+
+ // 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 = `
+
+
+ Empty response received from server
+
+ `;
+ 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 = `
+
+ Back to Subjects
+
+ `;
+ 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 = `
+
+
+ No subjects found matching your criteria.
+
+ `;
+ 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 = ' 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 = ` 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 = `
+
+
+ No subjects assigned to you. Please contact administrator.
+
+ `;
+ contentDiv.appendChild(directLink);
+ }
+ })
+ .catch(error => {
+ console.error('Error fetching subjects:', error);
+ contentDiv.innerHTML = `
+
+
+ Error fetching subjects: ${error.message}
+
+ `;
+ contentDiv.appendChild(directLink);
+ });
+ }
+
+ // If all else fails, show a simple message
+ if (!contentFound) {
+ console.log('All content extraction methods failed');
+ contentDiv.innerHTML = `
+
+
+ Unable to display subject materials automatically.
+
+ `;
+ 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 = `
+
+ Upload New Material
+
+ `;
+ contentDiv.appendChild(uploadBtn);
+ }
+ }, 200);
+ })
+ .catch(error => {
+ console.error('Error loading subject materials:', error);
+ contentDiv.innerHTML = `
+
+
+ Error loading subject materials: ${error.message}
+
+
+
+ Upload New Material
+
+
+ Open in New Tab
+
+
+ `;
+ });
+}
+
+function openUploadModal(subjectId = '') {
+ console.log('Opening upload modal with subject ID:', subjectId);
+ const modal = document.getElementById('uploadFileModal');
+ const subjectSelect = document.getElementById('subject_id');
+ const responseDiv = document.getElementById('uploadResponse');
+
+ if (!modal || !subjectSelect) {
+ console.error('Modal or subject select not found');
+ return;
+ }
+
+ // Reset the form
+ const uploadForm = document.getElementById('uploadFileForm');
+ if (uploadForm) {
+ uploadForm.reset();
+ }
+
+ if (responseDiv) {
+ responseDiv.style.display = 'none';
+ }
+
+ // If a subject ID was provided, pre-select it
+ if (subjectId && subjectSelect) {
+ subjectSelect.value = subjectId;
+ }
+
+ modal.style.display = 'block';
+ document.body.style.overflow = 'hidden';
+}
+
+function closeUploadModal() {
+ const modal = document.getElementById('uploadFileModal');
+ if (modal) {
+ modal.style.display = 'none';
+ document.body.style.overflow = '';
+ }
+}
+
+function deleteFile(fileId) {
+ fileIdToDelete = fileId;
+ const modal = document.getElementById('confirmationModal');
+ if (modal) {
+ modal.style.display = 'block';
+ document.body.style.overflow = 'hidden';
+ }
+}
+
+function closeConfirmationModal() {
+ const modal = document.getElementById('confirmationModal');
+ if (modal) {
+ modal.style.display = 'none';
+ document.body.style.overflow = '';
+ }
+ fileIdToDelete = null;
+}
+
+// Function to view subject syllabus
+function viewSyllabus(subjectId) {
+ if (!subjectId) return;
+
+ // Get the button element that was clicked
+ const syllabusButton = document.querySelector(`.syllabus-button[data-subject-id="${subjectId}"]`);
+
+ // Check if button was hidden - if so, there's no syllabus available
+ if (syllabusButton && syllabusButton.style.display === 'none') {
+ alert('No syllabus available for this subject.');
+ return;
+ }
+
+ // Show loading indicator
+ const loadingModal = document.createElement('div');
+ loadingModal.style.position = 'fixed';
+ loadingModal.style.top = '0';
+ loadingModal.style.left = '0';
+ loadingModal.style.width = '100%';
+ loadingModal.style.height = '100%';
+ loadingModal.style.backgroundColor = 'rgba(0,0,0,0.5)';
+ loadingModal.style.display = 'flex';
+ loadingModal.style.justifyContent = 'center';
+ loadingModal.style.alignItems = 'center';
+ loadingModal.style.zIndex = '10000';
+
+ loadingModal.innerHTML = `
+
+
+ Loading...
+
+
Loading syllabus...
+
+ `;
+
+ 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');
+ }
+}
\ No newline at end of file
diff --git a/static/js/teacher-utils.js b/static/js/teacher-utils.js
new file mode 100644
index 0000000..c388481
--- /dev/null
+++ b/static/js/teacher-utils.js
@@ -0,0 +1,217 @@
+// Utility functions for the teacher dashboard
+
+// Global variables and constants
+let fileIdToDelete = null;
+const API_BASE_URL = '/app';
+const DEFAULT_ERROR_MESSAGE = 'An error occurred. Please try again.';
+
+// Type checking utilities
+const isString = (value) => typeof value === 'string';
+const isObject = (value) => value !== null && typeof value === 'object';
+const isEmpty = (value) => value === null || value === undefined || value === '';
+
+// CSRF token management
+function getCSRFToken() {
+ try {
+ const cookies = document.cookie.split(';');
+ const csrfCookie = cookies.find(cookie => cookie.trim().startsWith('csrftoken='));
+ return csrfCookie ? decodeURIComponent(csrfCookie.split('=')[1]) : null;
+ } catch (error) {
+ console.error('Error getting CSRF token:', error);
+ return null;
+ }
+}
+
+// API request helper with better error handling and retries
+async function apiRequest(url, options = {}, retries = 3) {
+ const defaultOptions = {
+ headers: {
+ 'X-CSRFToken': getCSRFToken(),
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ credentials: 'same-origin'
+ };
+
+ for (let attempt = 1; attempt <= retries; attempt++) {
+ try {
+ const response = await fetch(url, { ...defaultOptions, ...options });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ if (attempt === retries) {
+ console.error(`API Request Error (attempt ${attempt}/${retries}):`, error);
+ throw error;
+ }
+ await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff
+ }
+ }
+}
+
+// UI Helpers with enhanced styling and accessibility
+function showLoadingSpinner(container, message = 'Loading...') {
+ if (!container) return;
+
+ container.innerHTML = `
+
+
+ Loading...
+
+
${message}
+
+ `;
+}
+
+function showError(container, error, withRetryButton = false) {
+ if (!container) return;
+
+ const errorMessage = error?.message || DEFAULT_ERROR_MESSAGE;
+ container.innerHTML = `
+
+
+ ${errorMessage}
+
+
+ ${withRetryButton ? `
+
+
+ Retry
+
+
+ ` : ''}
+ `;
+}
+
+function showSuccess(container, message, autoHide = true) {
+ if (!container) return;
+
+ container.innerHTML = `
+
+
+ ${message}
+
+
+ `;
+
+ if (autoHide) {
+ setTimeout(() => {
+ const alert = container.querySelector('.alert');
+ if (alert) {
+ alert.classList.remove('show');
+ setTimeout(() => alert.remove(), 150);
+ }
+ }, 5000);
+ }
+}
+
+// Modal Helpers with enhanced functionality
+function showModal(modalId) {
+ const modal = document.getElementById(modalId);
+ if (!modal) return;
+
+ modal.style.display = 'block';
+ document.body.style.overflow = 'hidden';
+
+ // Focus trap for accessibility
+ const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
+ if (focusableElements.length > 0) {
+ focusableElements[0].focus();
+ }
+}
+
+function hideModal(modalId) {
+ const modal = document.getElementById(modalId);
+ if (!modal) return;
+
+ modal.style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+// Form Helpers with validation
+function resetForm(formOrId) {
+ let form;
+
+ // Handle both form element and form ID
+ if (typeof formOrId === 'string') {
+ form = document.getElementById(formOrId);
+ } else if (formOrId instanceof HTMLFormElement) {
+ form = formOrId;
+ } else {
+ console.warn('Invalid form reference:', formOrId);
+ return;
+ }
+
+ if (!form) {
+ console.warn('Form not found:', formOrId);
+ return;
+ }
+
+ // Reset form values
+ form.reset();
+
+ // Clear all alerts and validation states
+ form.querySelectorAll('.alert, .is-invalid, .is-valid').forEach(el => {
+ el.classList.remove('alert', 'is-invalid', 'is-valid');
+ });
+
+ // Reset all checkboxes to unchecked state
+ form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
+ checkbox.checked = false;
+ });
+
+ // Reset all select elements to first option
+ form.querySelectorAll('select').forEach(select => {
+ if (select.options.length > 0) {
+ select.selectedIndex = 0;
+ }
+ });
+}
+
+// Date Helpers with enhanced formatting options
+function formatDate(date, format = 'YYYY-MM-DD') {
+ const d = new Date(date);
+ if (isNaN(d.getTime())) return '';
+
+ const year = d.getFullYear();
+ const month = String(d.getMonth() + 1).padStart(2, '0');
+ const day = String(d.getDate()).padStart(2, '0');
+
+ return format
+ .replace('YYYY', year)
+ .replace('MM', month)
+ .replace('DD', day);
+}
+
+function getCurrentDate() {
+ return formatDate(new Date());
+}
+
+// Validation Helpers
+function validateEmail(email) {
+ const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return re.test(email);
+}
+
+function validatePhone(phone) {
+ const re = /^\+?[\d\s-]{10,}$/;
+ return re.test(phone);
+}
+
+// Export all functions
+window.getCSRFToken = getCSRFToken;
+window.apiRequest = apiRequest;
+window.showLoadingSpinner = showLoadingSpinner;
+window.showError = showError;
+window.showSuccess = showSuccess;
+window.showModal = showModal;
+window.hideModal = hideModal;
+window.resetForm = resetForm;
+window.formatDate = formatDate;
+window.getCurrentDate = getCurrentDate;
+window.validateEmail = validateEmail;
+window.validatePhone = validatePhone;
\ No newline at end of file
diff --git a/student_management_system/jazzmin.py b/student_management_system/jazzmin.py
index 4cff028..8a3e150 100644
--- a/student_management_system/jazzmin.py
+++ b/student_management_system/jazzmin.py
@@ -1,56 +1,98 @@
JAZZMIN_SETTINGS = {
- "site_title": "Student Management System Admin",
- "site_header": "Student Management System",
+ "site_title": "Student Management System",
+ "site_header": "Student Management",
"site_brand": "SMS",
- "site_logo": "/dist/img/logo.png",
+ "site_logo": "img/logo.png",
+ "login_logo": "img/logo.png",
+ "login_logo_dark": None,
"site_logo_classes": "img-circle",
- "site_icon": "/dist/img/logo.png",
- "welcome_sign": "Welcome to the Student Management System",
- "copyright": "ADDRSSS",
- ############
- # Top Menu #
- ############
+ "login_logo_classes": "",
+ "custom_css": "css/custom.css",
+ "welcome_sign": "Welcome to Student Management System",
+ "copyright": "Student Management System",
+ "search_model": ["app.Student", "app.Staff", "app.Course"],
"topmenu_links": [
- {"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]},
- {"name": "Dashboard", "url": "dashboard", "permissions": ["auth.view_user"]},
+ {"name": "Dashboard", "url": "admin:index", "permissions": ["auth.view_user"]},
+ {"model": "auth.User"},
],
- #############
- # Side Menu #
- #############
+ "usermenu_links": [{"model": "auth.user"}],
"show_sidebar": True,
"navigation_expanded": True,
- # List of apps (and/or models) to base side menu ordering off of (does not need to contain all apps/models)
- "order_with_respect_to": ["auth", "dashboard", "app"],
- "custom_links": {
- "app": [
- {
- "name": "Dashboard",
- "url": "dashboard",
- "icon": "fas fa-tachometer-alt",
- },
- ],
- },
-
-
+ "hide_apps": [],
+ "hide_models": [],
+ "order_with_respect_to": [
+ "dashboard",
+ "authentication",
+ "organization",
+ "human_managementorganization.institute",
+ "organization.batch",
+ "organization.course",
+ "human_management.student",
+ "human_management.staff",
+ "human_management.routine",
+ "human_management.staff_leave",
+ "human_management.student_leave",
+ "human_management.studentfeedback",
+ "human_management.coursetracking",
+ "human_management.attendance",
+ "human_management.institutefeedback",
+ ],
"icons": {
"auth": "fas fa-users-cog",
"auth.user": "fas fa-user",
- "auth.group": "fas fa-users",
+ "auth.Group": "fas fa-users",
"app.student": "fas fa-user-graduate",
- "app.teacher": "fas fa-chalkboard-teacher",
+ "app.staff": "fas fa-chalkboard-teacher",
"app.course": "fas fa-book",
"app.subject": "fas fa-book-open",
- "app.academicyear": "fas fa-calendar",
"app.attendance": "fas fa-calendar-check",
- "app.attendancereport": "fas fa-clipboard-list",
- "app.leavereportstudent": "fas fa-user-clock",
- "app.leavereportteacher": "fas fa-clock",
- "app.feedbackstudent": "fas fa-comment-alt",
- "app.feedbackteacher": "fas fa-comments",
+ "app.studentfeedback": "fas fa-comment",
+ "app.institutefeedback": "fas fa-comments",
+ "app.student_leave": "fas fa-calendar-times",
+ "app.staff_leave": "fas fa-calendar-minus",
+ "app.notice": "fas fa-bullhorn",
},
+ "default_icon_parents": "fas fa-chevron-circle-right",
+ "default_icon_children": "fas fa-circle",
+ "related_modal_active": False,
+ "custom_js": None,
+ "show_ui_builder": False,
+ "changeform_format": "horizontal_tabs",
+ "changeform_format_overrides": {
+ "auth.user": "collapsible",
+ "auth.group": "vertical_tabs",
+ },
+ "language_chooser": False,
}
JAZZMIN_UI_TWEAKS = {
- "theme": "lumen",
- # "dark_mode_theme": "cyborg",
+ "navbar_small_text": False,
+ "footer_small_text": False,
+ "body_small_text": False,
+ "brand_small_text": False,
+ "brand_colour": False,
+ "accent": "accent-primary",
+ "navbar": "navbar-dark",
+ "no_navbar_border": False,
+ "navbar_fixed": False,
+ "layout_boxed": False,
+ "footer_fixed": False,
+ "sidebar_fixed": False,
+ "sidebar": "sidebar-dark-primary",
+ "sidebar_nav_small_text": False,
+ "sidebar_disable_expand": False,
+ "sidebar_nav_child_indent": False,
+ "sidebar_nav_compact_style": False,
+ "sidebar_nav_legacy_style": False,
+ "sidebar_nav_flat_style": False,
+ "theme": "default",
+ "dark_mode_theme": None,
+ "button_classes": {
+ "primary": "btn-primary",
+ "secondary": "btn-secondary",
+ "info": "btn-info",
+ "warning": "btn-warning",
+ "danger": "btn-danger",
+ "success": "btn-success"
+ }
}
diff --git a/student_management_system/settings.py b/student_management_system/settings.py
index 88327e7..f121cad 100644
--- a/student_management_system/settings.py
+++ b/student_management_system/settings.py
@@ -1,7 +1,7 @@
"""
Django settings for student_management_system project.
-Generated by 'django-admin startproject' using Django 5.1.4.
+Generated by 'django-admin startproject' using Django 5.1.6.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
@@ -11,25 +11,26 @@ https://docs.djangoproject.com/en/5.1/ref/settings/
"""
import os
+from pathlib import Path
+from dotenv import load_dotenv
from student_management_system.jazzmin import JAZZMIN_SETTINGS, JAZZMIN_UI_TWEAKS
-# Build paths inside the project like this: BASE_DIR / 'subdir'.
-BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+# Load environment variables
+load_dotenv()
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = "django-insecure-)bp&!(x=q)##0hl&7r&h2x$u_3bvd7td)yq4y&_k)$)ce%pj&m"
+SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
+DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True'
-ALLOWED_HOSTS = ["*"]
-
-JAZZMIN_SETTINGS = JAZZMIN_SETTINGS
-JAZZMIN_UI_TWEAKS = JAZZMIN_UI_TWEAKS
+ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '*').split(',')
# Application definition
@@ -41,11 +42,14 @@ INSTALLED_APPS = [
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
+ "rest_framework",
+ "mathfilters",
"app",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
+ "whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@@ -54,6 +58,11 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
+AUTHENTICATION_BACKENDS = [
+ "app.backends.MultiModelBackend",
+ "django.contrib.auth.backends.ModelBackend",
+]
+REST_FRAMEWORK = {"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"]}
ROOT_URLCONF = "student_management_system.urls"
TEMPLATES = [
@@ -120,22 +129,54 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
-STATIC_URL = "static/"
+STATIC_URL = "/static/"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
-
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
-MEDIA_URL = "/media/"
-MEDIA_ROOT = os.path.join(BASE_DIR, "media")
-
+# Configure whitenoise
+STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+
+MEDIA_URL = "/media/"
+MEDIA_ROOT = os.path.join(BASE_DIR, "media")
+
+
+# Jazmin Settings
+JAZZMIN_SETTINGS = JAZZMIN_SETTINGS
+JAZZMIN_UI_TWEAKS = JAZZMIN_UI_TWEAKS
+
+# Login redirect URL
+LOGIN_REDIRECT_URL = '/app/dashboard/'
+LOGIN_URL = '/login/'
+
+# Email Configuration
+EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend')
+DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'noreply@studentmanagementsystem.com')
+
+# Cache configuration for OTP storage
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'LOCATION': os.getenv('CACHE_LOCATION', 'unique-snowflake'),
+ }
+}
+
+# SMS API configuration
+SMS_API_KEY = os.getenv('SMS_API_KEY', '')
+SMS_SENDER_ID = os.getenv('SMS_SENDER_ID', 'SMSSYS')
+
+
+CSRF_TRUSTED_ORIGINS = [
+ "https://*.ngrok-free.app",
+ "https://*.ngrok.io"
+]
diff --git a/student_management_system/urls.py b/student_management_system/urls.py
index 661260f..e40550d 100644
--- a/student_management_system/urls.py
+++ b/student_management_system/urls.py
@@ -19,12 +19,32 @@ from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
+from django.contrib.auth import views as auth_views
+from app.admin import custom_admin_site
+from django.views.generic import RedirectView, TemplateView
+from app import views
+from app import auth
-urlpatterns = (
- [
- path("admin/", admin.site.urls),
- path("", include("app.urls")),
- ]
- + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
- + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
-)
+# Main URL patterns for the project
+urlpatterns = [
+ path("app/", include("app.urls")),
+ path("admin/", custom_admin_site.urls),
+
+ # Root URL redirects to dashboard
+ path("", RedirectView.as_view(url="/app/dashboard/", permanent=False)),
+
+ # Authentication URLs
+ path('login/', auth_views.LoginView.as_view(template_name='login/login.html'), name='login'),
+ path('logout/', auth_views.LogoutView.as_view(next_page='/login/'), name='logout'),
+
+ # Password reset URLs
+ path('password-reset/', auth.reset_password_options, name='password_reset'),
+
+ # Firebase service worker URL
+ path('firebase-messaging-sw.js', views.serve_firebase_sw),
+]
+
+# Add static and media file serving in development
+if settings.DEBUG:
+ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
\ No newline at end of file
diff --git a/templates/admin/app/routine/change_form.html b/templates/admin/app/routine/change_form.html
new file mode 100644
index 0000000..dc240da
--- /dev/null
+++ b/templates/admin/app/routine/change_form.html
@@ -0,0 +1,555 @@
+{% extends "admin/change_form.html" %}
+{% load static %}
+{% load mathfilters %}
+
+{% block extrahead %}
+{{ block.super }}
+
+
+
+{% endblock %}
+
+{% block content %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..64e8508
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,373 @@
+{% load static %}
+
+
+
+
+
+
+
+
+
+ {% block title %}Student Management System{% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block extrahead %}{% endblock %}
+
+
+ {% if messages %}
+
+ {% for message in messages %}
+
+ {% endfor %}
+
+ {% endif %}
+
+ {% block content %}{% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block extrajs %}{% endblock %}
+
+
\ No newline at end of file
diff --git a/templates/components/bottom_nav.html b/templates/components/bottom_nav.html
new file mode 100644
index 0000000..54a7597
--- /dev/null
+++ b/templates/components/bottom_nav.html
@@ -0,0 +1,134 @@
+
+
+
+
+
diff --git a/templates/components/common_modals.html b/templates/components/common_modals.html
new file mode 100644
index 0000000..4bd8ed3
--- /dev/null
+++ b/templates/components/common_modals.html
@@ -0,0 +1,114 @@
+{% load static %}
+
+
+
+
+
+
Update Profile Picture
+ ×
+
+
+
+
+
+
+
+
+
+
Change Password
+ ×
+
+
+
+
+
+
+
+
+
+
Request Leave
+ ×
+
+
+
+
+
+
+
+
+
+
Confirm Deletion
+ ×
+
+
+
Are you sure you want to delete this file? This action cannot be undone.
+
+
+ Cancel
+
+ Delete
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/components/student_feedback_modal.html b/templates/components/student_feedback_modal.html
new file mode 100644
index 0000000..de39b50
--- /dev/null
+++ b/templates/components/student_feedback_modal.html
@@ -0,0 +1,230 @@
+
+
+
+
+
Give Feedback
+ ×
+
+
+
+
+
+ Teacher Feedback
+
+
+ Institute Feedback
+
+
+
+
+
+
+
+
+ {% csrf_token %}
+
+
+ Select Teacher
+
+ Select Teacher
+ {% for teacher in teachers %}
+ {{ teacher.name }} - {{ teacher.designation }}
+ {% endfor %}
+
+
+
+
Rating
+
+
+
+
+
+
+
+
Please select a rating
+
+
+
+ Your Feedback
+
+
+
+ Cancel
+ Submit Feedback
+
+
+
+
+
+
+
+ {% csrf_token %}
+
+
+ Feedback Category
+
+ Select Category
+ General
+ Facilities
+ Teaching Quality
+ Infrastructure
+ Administration
+
+
+
+
Rating
+
+
+
+
+
+
+
+
Please select a rating
+
+
+
+ Your Feedback
+
+
+
+
+
+
+ Submit anonymously
+
+
+
+
+ Cancel
+ Submit Feedback
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/components/student_modals.html b/templates/components/student_modals.html
new file mode 100644
index 0000000..2d9cafd
--- /dev/null
+++ b/templates/components/student_modals.html
@@ -0,0 +1,32 @@
+{% load static %}
+
+
+
+
+
+
Subject Syllabus
+ ×
+
+
+
+
+
+
+
+
+
+
+
+
Subject Materials
+ ×
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/components/teacher_feedback_modal.html b/templates/components/teacher_feedback_modal.html
new file mode 100644
index 0000000..f539e98
--- /dev/null
+++ b/templates/components/teacher_feedback_modal.html
@@ -0,0 +1,109 @@
+
+
+
+
+
Give Institute Feedback
+ ×
+
+
+
+ {% csrf_token %}
+
+ Feedback Type
+
+ Select Type
+ {% for value, name in feedback_types %}
+ {{ name }}
+ {% endfor %}
+
+
+
+
+
Rating
+
+
+
+
+
+
+
+
Please select a rating
+
+
+
+
+ Feedback
+
+
+
+
+
+ Submit anonymously
+
+
+
+ Cancel
+ Submit Feedback
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/components/teacher_modals.html b/templates/components/teacher_modals.html
new file mode 100644
index 0000000..30e9ff7
--- /dev/null
+++ b/templates/components/teacher_modals.html
@@ -0,0 +1,63 @@
+
+
+
+
+
Take Attendance
+ ×
+
+
+
+ {% csrf_token %}
+
+
+
+
+
+
+
+ Teacher Present
+
+
+
+
+ Conducted
+ Not Conducted
+
+
+
+
+
+
+
+
+ #
+ Name
+ Student ID
+ Present
+
+
+
+
+
+
+
+
+
+
+
+ Mark All Present
+
+
+ Mark All Absent
+
+
+
+ Save Attendance
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/components/welcome_section.html b/templates/components/welcome_section.html
new file mode 100644
index 0000000..04c9957
--- /dev/null
+++ b/templates/components/welcome_section.html
@@ -0,0 +1,32 @@
+{% load static %}
+
+
+
+
+
+
+
+ {% if user.image %}
+
+
+
+ {% else %}
+
+
+
+ {% endif %}
+
+
Welcome, {{ user.name }}!
+
+ {% if user.designation %}
+ {{ user.designation }} ID: {{ user.id }}
+ {% else %}
+ Student ID: {{ user.id }}
+ {% endif %}
+
+
+
+
+
+
+
diff --git a/templates/dashboard.html b/templates/dashboard.html
new file mode 100644
index 0000000..8400e96
--- /dev/null
+++ b/templates/dashboard.html
@@ -0,0 +1,1048 @@
+{% extends "admin/base_site.html" %}
+{% load static %}
+{% load app_filters %}
+{% block extrahead %}
+{{ block.super }}
+
+{% endblock %}
+{% block extrastyle %}
+{{ block.super }}
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+
Welcome, Admin!
+
Student Management Dashboard
+
+
+
+
+ Logout
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Students
+
{{ total_students }}
+
+
+
+
+ Active
+ {{ active_students }}
+
+
+ Completed
+ {{ completed_students }}
+
+
+ On Leave
+ {{ on_leave_students }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Courses
+
{{ total_courses }}
+
+
+
+
+ Active
+ {{ active_courses }}
+
+
+ Completed
+ {{ completed_courses }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Staff
+
{{ total_staff }}
+
+
+
+
+ Active
+ {{ active_staff }}
+
+
+ On Leave
+ {{ staff_on_leave }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Classes Today
+
{{ total_classes_today }}
+
+
+ Attendance Rate
+
{{ attendance_rate }}%
+
+
+
+
+
+ Students Present
+ {{ students_present }}/{{ total_students_today }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Course Progress
+
+
+
+
+
+ Course
+ Students
+ Progress
+ Status
+
+
+
+ {% for progress in course_progress %}
+
+ {{ progress.course.name }}
+ {{ progress.total_students }}
+
+
+ {{ progress.average_progress }}%
+
+
+
+ {% if progress.average_progress == 100 %}
+ Completed
+ {% elif progress.average_progress > 0 %}
+ In Progress
+ {% else %}
+ Not Started
+ {% endif %}
+
+
+
+ {% empty %}
+
+ No course progress data available
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ Recent Activity
+
+
+
+
+
+
+
+
+
+
+
+ Pending Leave Requests
+
+
+
+
+
+ Name
+ Type
+ Start Date
+ End Date
+ Reason
+ Actions
+
+
+
+ {% for leave in pending_student_leaves %}
+
+ {{ leave.student.name }}
+
+ Student
+
+ {{ leave.start_date|date:"M d, Y" }}
+ {{ leave.end_date|date:"M d, Y" }}
+ {{ leave.message }}
+
+
+ {% csrf_token %}
+
+
+
+
+
+ {% csrf_token %}
+
+
+
+
+
+
+ {% endfor %}
+ {% for leave in pending_staff_leaves %}
+
+ {{ leave.staff.name }}
+
+ Staff
+
+ {{ leave.start_date|date:"M d, Y" }}
+ {{ leave.end_date|date:"M d, Y" }}
+ {{ leave.message }}
+
+
+ {% csrf_token %}
+
+
+
+
+
+ {% csrf_token %}
+
+
+
+
+
+
+ {% endfor %}
+ {% if not pending_student_leaves and not pending_staff_leaves %}
+
+ No pending leave requests
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Notice Board
+
+
+ {% csrf_token %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Add Notice
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Teacher Ratings
+
+
+ {% for teacher in teacher_ratings %}
+
+
+
+ {% if teacher.image %}
+
+ {% else %}
+
+
+
+ {% endif %}
+
+
{{ teacher.name }}
+ {{ teacher.designation }}
+
+
+
+
+ Average Rating
+ {{ teacher.avg_rating|floatformat:1 }}/5.0
+
+
+ {% for i in "12345"|make_list %}
+
+ {% endfor %}
+
+
+
+
+
+ Total Reviews
+ {{ teacher.total_reviews }}
+
+
+ Students Taught
+ {{ teacher.total_students }}
+
+
+
+
+ {% empty %}
+
+
No teacher ratings available
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+ Monthly Statistics
+
+
+
+
+
+
{{ monthly_stats.new_students }}
+
New Students
+
+
+
+
+
+
{{ monthly_stats.new_staff }}
+
New Staff
+
+
+
+
+
+
{{ monthly_stats.attendance_rate }}%
+
Attendance Rate
+
+
+
+
+
+
{{ monthly_stats.leave_requests }}
+
Leave Requests
+
+
+
+
+
+
+
+
+
+
+
+
+ Institute Feedback
+
+
+
+
+
+ S.N
+ User
+ Category
+ Rating
+ Feedback
+ Date
+
+
+
+ {% for feedback in recent_institute_feedback %}
+
+ {{ forloop.counter }}
+
+
+ {% if feedback.is_anonymous %}
+
+
+
+
+ Anonymous User
+
+ {% else %}
+ {% if feedback.user %}
+
+ {% if feedback.user.image %}
+
+ {% else %}
+
+
+
+ {% endif %}
+
+
{{ feedback.display_name }}
+
+ Student
+ {% if feedback.user.course %}
+
+ {{ feedback.user.course.name }}
+
+ {% endif %}
+ {% if feedback.user.current_semester %}
+
+ Semester {{ feedback.user.current_semester }}
+
+ {% endif %}
+
+
+ {% elif feedback.staff %}
+
+ {% if feedback.staff.image %}
+
+ {% else %}
+
+
+
+ {% endif %}
+
+
{{ feedback.display_name }}
+
+ Staff
+ {% if feedback.staff.designation %}
+
+ {{ feedback.staff.designation }}
+
+ {% endif %}
+
+
+ {% else %}
+
+
+
+
+
+ {{ feedback.display_name }}
+
+ {% endif %}
+ {% endif %}
+
+
+ {{ feedback.feedback_type }}
+
+
+ {% for i in "12345"|make_list %}
+
+ {% endfor %}
+
+
+
+
+ {{ feedback.feedback_text|truncatechars:100 }}
+
+
+ {{ feedback.created_at|date:"M d, Y" }}
+
+ {% empty %}
+
+
+
+
+
No institute feedback available
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block extrajs %}
+{{ block.super }}
+
+
+{% endblock %}
diff --git a/templates/hod/add_academicyear.html b/templates/hod/add_academicyear.html
deleted file mode 100644
index fc017b3..0000000
--- a/templates/hod/add_academicyear.html
+++ /dev/null
@@ -1,135 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}{{ title }}{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
- {% csrf_token %}
-
-
- {% if messages %}
-
- {% for message in messages %}
-
- {{ message }}
-
-
- {% endfor %}
-
- {% endif %}
-
-
- Cancel
-
-
- Save Academic Year
-
-
-
-
-
-
- {% block extrajs %}
-
-
- {% endblock %}
-{% endblock %}
diff --git a/templates/hod/add_course.html b/templates/hod/add_course.html
deleted file mode 100644
index 86b9ae1..0000000
--- a/templates/hod/add_course.html
+++ /dev/null
@@ -1,138 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}Add Course{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
-
- {% csrf_token %}
-
-
-
-
-
-
- {% if messages %}
-
- {% for message in messages %}
-
- {{ message }}
-
-
- {% endfor %}
-
- {% endif %}
-
-
- Cancel
-
-
- Save Course
-
-
-
-
-
-
- {% block extrajs %}
-
-
- {% endblock %}
-{% endblock %}
diff --git a/templates/hod/add_student.html b/templates/hod/add_student.html
deleted file mode 100644
index 11567eb..0000000
--- a/templates/hod/add_student.html
+++ /dev/null
@@ -1,149 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}{{ title }}{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-{% endblock %}
-{% block content %}
-
-
Add Student
-
- {% csrf_token %}
-
-
-
- First Name
-
-
-
- Middle Name
-
-
-
- Last Name
-
-
-
-
- Course
-
- Select Course
- {% for course in courses %}{{ course.name }} {% endfor %}
-
-
-
- Academic Year
-
- Select Academic Year
- {% for year in academic_years %}{{ year.start_date }} {% endfor %}
-
-
-
-
- Email
-
-
-
- Phone
-
-
-
- Date of Birth
-
-
-
- Gender
-
- Select Gender
- Male
- Female
-
-
-
-
- Address
-
-
-
-
- Father's Name
-
-
-
- Mother's Name
-
-
-
- Parents' Contact Number
-
-
-
- Father's Occupation
-
- Select Occupation
- {% for key, value in occupation_choices %}{{ value }} {% endfor %}
-
-
-
- Mother's Occupation
-
- Select Occupation
- {% for key, value in occupation_choices %}{{ value }} {% endfor %}
-
-
-
-
- Guardian's Name
-
-
-
- Guardian's Contact
-
-
-
- Relationship with Student
-
- Select Relationship
- {% for key, value in relationship_choices %}{{ value }} {% endfor %}
-
-
-
-
-
-
-{% endblock %}
diff --git a/templates/hod/add_subject.html b/templates/hod/add_subject.html
deleted file mode 100644
index dfb7e2c..0000000
--- a/templates/hod/add_subject.html
+++ /dev/null
@@ -1,54 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}Add Subject{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
- {% csrf_token %}
- {{ form.as_p }}
- Save
-
-
-
-
-{% endblock %}
diff --git a/templates/hod/add_teacher.html b/templates/hod/add_teacher.html
deleted file mode 100644
index 06dc64e..0000000
--- a/templates/hod/add_teacher.html
+++ /dev/null
@@ -1,100 +0,0 @@
-{% extends 'admin/base_site.html' %}
-{% block content %}
-
-
{{ title }}
-
- {% csrf_token %}
-
-
-
-
- Date of Birth
-
-
-
- Gender
-
- Select Gender
- Male
- Female
-
-
-
- Qualification
-
- Select Qualification
- {% for key, value in qualification_choices %}{{ value }} {% endfor %}
-
-
-
-
-
- Specialization
-
-
-
- Marital Status
-
- Select Status
- {% for key, value in marital_status_choices %}{{ value }} {% endfor %}
-
-
-
-
-
- Date of Joining
-
-
-
- Address
-
-
-
-
-
-
-{% endblock %}
diff --git a/templates/hod/dashboard.html b/templates/hod/dashboard.html
deleted file mode 100644
index dc081d5..0000000
--- a/templates/hod/dashboard.html
+++ /dev/null
@@ -1,55 +0,0 @@
-{% extends 'admin/base_site.html' %}
-{% block content %}
-
-
-
-
-
-
-
Total Students
-
{{ student_count }}
-
-
-
-
-
-
-
-
-
Total Teachers
-
{{ teacher_count }}
-
-
-
-
-
-
-
-
-
Total Courses
-
{{ course_count }}
-
-
-
-
-
-
-
-
-
Total Subjects
-
{{ subject_count }}
-
-
-
-
-
-
-{% endblock %}
diff --git a/templates/hod/edit_academicyear.html b/templates/hod/edit_academicyear.html
deleted file mode 100644
index f65c802..0000000
--- a/templates/hod/edit_academicyear.html
+++ /dev/null
@@ -1,94 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}{{ title }}{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
- {% csrf_token %}
-
- Start Date
-
-
-
-
-
- Save Changes
-
-
-
-
-
-
-{% endblock %}
diff --git a/templates/hod/edit_course.html b/templates/hod/edit_course.html
deleted file mode 100644
index 55b5502..0000000
--- a/templates/hod/edit_course.html
+++ /dev/null
@@ -1,152 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}{{ title }}{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
-
- {% csrf_token %}
-
-
-
-
-
-
- {% if messages %}
-
- {% for message in messages %}
-
- {{ message }}
-
-
- {% endfor %}
-
- {% endif %}
-
-
- Cancel
-
-
- Update Course
-
-
-
-
-
-
- {% block extrajs %}
-
-
- {% endblock %}
-{% endblock %}
diff --git a/templates/hod/edit_student.html b/templates/hod/edit_student.html
deleted file mode 100644
index 00abaef..0000000
--- a/templates/hod/edit_student.html
+++ /dev/null
@@ -1,212 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
- {% csrf_token %}
-
-
-
-
- Gender
-
- Male
- Female
-
-
-
- Course
-
- Select Course
- {% for course in courses %}
-
- {{ course.name }}
-
- {% endfor %}
-
-
-
- Academic Year
-
- Select Academic Year
- {% for year in academic_years %}
-
- {{ year.start_date }}
-
- {% endfor %}
-
-
-
-
-
- Father's Name
-
-
-
- Father's Occupation
-
- {% for key, value in occupation_choices %}
- {{ value }}
- {% endfor %}
-
-
-
- Mother's Name
-
-
-
-
-
- Mother's Occupation
-
- {% for key, value in occupation_choices %}
- {{ value }}
- {% endfor %}
-
-
-
- Guardian's Name
-
-
-
- Guardian's Number
-
-
-
- Relationship with Student
-
- {% for key, value in relationship_choices %}
-
- {{ value }}
-
- {% endfor %}
-
-
-
-
- Address
- {{ student.address }}
-
-
-
-
-{% endblock %}
diff --git a/templates/hod/edit_subject.html b/templates/hod/edit_subject.html
deleted file mode 100644
index c750ca8..0000000
--- a/templates/hod/edit_subject.html
+++ /dev/null
@@ -1,54 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}Edit Subject{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
- {% csrf_token %}
- {{ form.as_p }}
- Save
-
-
-
-
-{% endblock %}
diff --git a/templates/hod/edit_teacher.html b/templates/hod/edit_teacher.html
deleted file mode 100644
index 771fc5c..0000000
--- a/templates/hod/edit_teacher.html
+++ /dev/null
@@ -1,126 +0,0 @@
-{% extends 'admin/base_site.html' %}
-{% block content %}
-
-
Edit Teacher
-
- {% csrf_token %}
-
-
-
-
- Date of Birth
-
-
-
- Gender
-
- Male
- Female
-
-
-
- Qualification
-
- {% for key, value in qualification_choices %}
- {{ value }}
- {% endfor %}
-
-
-
-
-
- Specialization
-
-
-
- Marital Status
-
- {% for key, value in marital_status_choices %}
- {{ value }}
- {% endfor %}
-
-
-
-
-
- Date of Joining
-
-
-
- Address
- {{ teacher.address }}
-
-
-
-
Update Teacher
-
Cancel
-
-
-
-{% endblock %}
diff --git a/templates/hod/manage_academicyear.html b/templates/hod/manage_academicyear.html
deleted file mode 100644
index 520c9ba..0000000
--- a/templates/hod/manage_academicyear.html
+++ /dev/null
@@ -1,181 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}{{ title }}{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
-
- {% if academic_years %}
-
-
-
-
- S.N
- Start Date
- Created At
- Actions
-
-
-
- {% for academic_year in academic_years %}
-
- {{ forloop.counter }}
- {{ academic_year.start_date }}
- {{ academic_year.created_at }}
-
-
- Edit
-
-
- Delete
-
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
-
No Academic Years Found
-
Start by adding your first academic year using the button above.
-
- {% endif %}
-
-
-
- {% block extrajs %}
-
-
- {% endblock %}
-{% endblock %}
diff --git a/templates/hod/manage_course.html b/templates/hod/manage_course.html
deleted file mode 100644
index 7352948..0000000
--- a/templates/hod/manage_course.html
+++ /dev/null
@@ -1,194 +0,0 @@
-{% extends "admin/base_site.html" %} {% block page_title %}{{ title }}
-{%endblock %} {% load static %} {% block extrastyle %}
-
-
-
-{% endblock %} {% block content %}
-
-
-
-
-
-
-
-
- {% if courses %}
-
-
-
-
- S.N
- Course Name
- Course Type
- Duration
- Actions
-
-
-
- {% for course in courses %}
-
- {{ forloop.counter }}
- {{ course.name }}
- {{ course.get_course_type_display }}
-
- {{ course.duration }} {% if course.course_type == 'semester' %}
- Semesters {% else %} Years {% endif %}
-
-
-
- Edit
-
-
- Delete
-
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
-
No Courses Found
-
- Start by adding your first course using the button above.
-
-
- {% endif %}
-
-
-
-
-{% block extrajs %}
-
-
-{% endblock %} {% endblock %}
diff --git a/templates/hod/manage_student.html b/templates/hod/manage_student.html
deleted file mode 100644
index 7dd536e..0000000
--- a/templates/hod/manage_student.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}{{ title }}{% endblock %}
-{% block content %}
-
-
-
-
Manage Students
-
-
-
-
-
-
-
-
-
-
- S.N
- Full Name
- Course
- Academic Year
- Phone
- Email
- Actions
-
-
-
- {% for student in students %}
-
- {{ forloop.counter }}
- {{ student.first_name }}
- {{ student.course.name }}
- {{ student.academic_year.start_date|date:"Y" }}
- {{ student.phone }}
- {{ student.email }}
-
- Edit
- Delete
-
-
- {% empty %}
-
- No students found
-
- {% endfor %}
-
-
-
-
-{% endblock %}
diff --git a/templates/hod/manage_subject.html b/templates/hod/manage_subject.html
deleted file mode 100644
index 78ba22a..0000000
--- a/templates/hod/manage_subject.html
+++ /dev/null
@@ -1,190 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}{{ title }}{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
-
- {% if subjects %}
-
-
-
-
- S.N
- Subject Name
- Course
- Duration
- Actions
-
-
-
- {% for subject in subjects %}
-
- {{ forloop.counter }}
- {{ subject.name }}
- {{ subject.course.name }}
-
- {{ subject.duration }}
- {% if subject.course.course_type == 'semester' %}
- Semesters
- {% else %}
- Years
- {% endif %}
-
-
-
- Edit
-
-
- Delete
-
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
-
No Subjects Found
-
Start by adding your first subject using the button above.
-
- {% endif %}
-
-
-
- {% block extrajs %}
-
-
- {% endblock %}
-{% endblock %}
diff --git a/templates/hod/manage_teacher.html b/templates/hod/manage_teacher.html
deleted file mode 100644
index cbd8cd2..0000000
--- a/templates/hod/manage_teacher.html
+++ /dev/null
@@ -1,43 +0,0 @@
-{% extends 'admin/base_site.html' %}
-{% block content %}
-
-
Manage Teachers
-
Add Teacher
-
-
-
- S.N
- Name
- Email
- Phone
- Gender
- Qualification
- Marital Status
- Date of Joining
- Actions
-
-
-
- {% for teacher in teachers %}
-
- {{ forloop.counter }}
- {{ teacher.first_name }} {{ teacher.middle_name }} {{ teacher.last_name }}
- {{ teacher.email }}
- {{ teacher.phone }}
- {{ teacher.get_gender_display }}
- {{ teacher.get_qualification_display }}
- {{ teacher.get_marital_status_display }}
- {{ teacher.date_of_joining }}
-
- Edit
- Delete
-
-
- {% endfor %}
-
-
-
-{% endblock %}
diff --git a/templates/hod/student_feedback.html b/templates/hod/student_feedback.html
deleted file mode 100644
index d495376..0000000
--- a/templates/hod/student_feedback.html
+++ /dev/null
@@ -1,144 +0,0 @@
-{% extends 'admin/base_site.html' %}
-{% block content %}
- {% load static %}
-
-
-
-
-
-
-
-
-
-
-
-
- S.N
- Student Name
- Student Session
- Message
- Sended On
- Reply
-
-
- {% for feedback in feedbacks %}
-
-
- {{ forloop.counter }}
- {{ feedback.student_id.first_name }}
- {{ feedback.student_id.academic_year.start_date }}
- {{ feedback.feedback }}
- {{ feedback.created_at }}
-
- {% if feedback.feedback_reply == "" %}
- Reply
- {% else %}
- {{ feedback.feedback_reply }}
- {% endif %}
-
-
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
- {% comment %} Modal to Reply Feedback {% endcomment %}
-
-
-{% endblock %}
-{% comment %} Custom JS to Identify the Reply and Post Reply {% endcomment %}
-{% block custom_js %}
-
-{% endblock custom_js %}
diff --git a/templates/hod/student_leave_view.html b/templates/hod/student_leave_view.html
deleted file mode 100644
index 79db973..0000000
--- a/templates/hod/student_leave_view.html
+++ /dev/null
@@ -1,188 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}{{ title }}{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
-
- {% if leaves %}
-
-
-
-
- S.N
- Student Name
- Leave Date
- Leave Message
- Applied On
- Actions
-
-
-
- {% for leave in leaves %}
-
- {{ forloop.counter }}
- {{ leave.student_id.first_name }}
- {{ leave.leave_date }}
- {{ leave.leave_message }}
- {{ leave.created_at }}
-
- {% if leave.leave_status == 0 %}
-
- Approve
-
-
- Reject
-
- {% elif leave.leave_status == 1 %}
- Approved
- {% else %}
- Rejected
- {% endif %}
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
-
No Leave Applications Found
-
There are no leave applications to display at the moment.
-
- {% endif %}
-
-
-
- {% block extrajs %}
-
-
- {% endblock %}
-{% endblock %}
diff --git a/templates/hod/teacher_feedback.html b/templates/hod/teacher_feedback.html
deleted file mode 100644
index dac239c..0000000
--- a/templates/hod/teacher_feedback.html
+++ /dev/null
@@ -1,115 +0,0 @@
-{% extends 'admin/base_site.html' %}
-{% block content %}
-
-
-
-
-
-
-
-
-
-
-
-
-
- S.N
- Staff Name
- Message
- Sended On
- Reply
-
- {% for feedback in feedbacks %}
-
- {{ forloop.counter }}
- {{ feedback.teacher_id.first_name }} {{ feedback.teacher_id.last_name }}
- {{ feedback.feedback }}
- {{ feedback.created_at }}
-
- {% if feedback.feedback_reply == "" %}
- Reply
- {% else %}
- {{ feedback.feedback_reply }}
- {% endif %}
-
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Reply To :
-
-
-
-
Reply
-
-
-
-
-
-
-{% endblock %}
-{% block custom_js %}
-
-{% endblock custom_js %}
diff --git a/templates/hod/teacher_leave_view.html b/templates/hod/teacher_leave_view.html
deleted file mode 100644
index 6a4a946..0000000
--- a/templates/hod/teacher_leave_view.html
+++ /dev/null
@@ -1,186 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block page_title %}{{ title }}{% endblock %}
-{% load static %}
-{% block extrastyle %}
-
-
-
-{% endblock %}
-{% block content %}
-
-
-
-
-
-
- {% if leaves %}
-
-
-
-
- S.N
- Teacher Name
- Leave Date
- Leave Message
- Applied On
- Actions
-
-
-
- {% for leave in leaves %}
-
- {{ forloop.counter }}
- {{ leave.teacher_id.first_name }}{{ leave.teacher_id.middle_name }} {{ leave.teacher_id.last_name }}
- {{ leave.leave_date }}
- {{ leave.leave_message }}
- {{ leave.created_at }}
-
- {% if leave.leave_status == 0 %}
-
- Approve
-
-
- Reject
-
- {% elif leave.leave_status == 1 %}
- Approved
- {% else %}
- Rejected
- {% endif %}
-
-
- {% endfor %}
-
-
-
- {% else %}
-
-
-
No Leave Applications Found
-
There are no leave applications to display at the moment.
-
- {% endif %}
-
-
-
- {% block extrajs %}
-
-
- {% endblock %}
-{% endblock %}
diff --git a/templates/hod/view_attendance.html b/templates/hod/view_attendance.html
deleted file mode 100644
index 0184633..0000000
--- a/templates/hod/view_attendance.html
+++ /dev/null
@@ -1,208 +0,0 @@
-{% extends 'admin/base_site.html' %}
-{% block content %}
- {% load static %}
-
-
-
-
-
-
-
-
- {% comment %} Display Messages {% endcomment %}
- {% if messages %}
-
- {% endif %}
-
-
- Subject
-
- {% for subject in subjects %}{{ subject.name }} {% endfor %}
-
-
-
- Academic Year
-
- {% for academic_year in academic_years %}
- {{ academic_year.start_date }}
- {% endfor %}
-
-
-
-
-
-
-
-
-
- {% comment %} {% endcomment %}
-
- {% comment %} Displaying Students Here {% endcomment %}
-
-
-
-
-
-
-
-
-{% endblock %}
-{% block custom_js %}
-
-{% endblock custom_js %}
diff --git a/templates/login/email_reset.html b/templates/login/email_reset.html
new file mode 100644
index 0000000..71a32ac
--- /dev/null
+++ b/templates/login/email_reset.html
@@ -0,0 +1,170 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
Reset Password
+
+
Enter your email address and we'll send you a verification code to reset your password.
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+ {% if success %}
+
+ {{ success }}
+
+ {% endif %}
+
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% csrf_token %}
+
+ Email Address
+
+
+
+ Send Reset Code
+
+
+
← Back to Reset Options
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/email_templates/password_reset_email.html b/templates/login/email_templates/password_reset_email.html
new file mode 100644
index 0000000..63db663
--- /dev/null
+++ b/templates/login/email_templates/password_reset_email.html
@@ -0,0 +1,83 @@
+{% autoescape off %}
+
+
+
+
+
+ Password Reset
+
+
+
+
+
+
+
+
Hello,
+
We received a request to reset your password for your Student Management System account. If you did not make this request, please ignore this email.
+
To reset your password, use the following code:
+
{{ reset_code }}
+
This code will expire in 5 minutes.
+
If you have any questions, please contact the system administrator.
+
Thank you, Student Management System Team
+
+
+
+
+
+
+{% endautoescape %}
\ No newline at end of file
diff --git a/templates/login/email_templates/password_reset_subject.txt b/templates/login/email_templates/password_reset_subject.txt
new file mode 100644
index 0000000..db28747
--- /dev/null
+++ b/templates/login/email_templates/password_reset_subject.txt
@@ -0,0 +1 @@
+Password Reset for Student Management System
\ No newline at end of file
diff --git a/templates/login/login.html b/templates/login/login.html
new file mode 100644
index 0000000..830d2b6
--- /dev/null
+++ b/templates/login/login.html
@@ -0,0 +1,168 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ Login - Student Management System
+
+
+
+
+
+
Login
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% csrf_token %}
+
+ Email or Phone Number
+
+
+
+
+ Password
+
+
+
+ Login
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/password_reset_complete.html b/templates/login/password_reset_complete.html
new file mode 100644
index 0000000..9fea7ce
--- /dev/null
+++ b/templates/login/password_reset_complete.html
@@ -0,0 +1,113 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ Password Reset Complete - Student Management System
+
+
+
+
+
+
Password Reset Complete
+
+
✓
+
+
+
Your password has been successfully reset.
+
+
+
You can now use your new password to log in to your account.
+
+
Log In
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/password_reset_confirm.html b/templates/login/password_reset_confirm.html
new file mode 100644
index 0000000..788ba0f
--- /dev/null
+++ b/templates/login/password_reset_confirm.html
@@ -0,0 +1,178 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ Set New Password - Student Management System
+
+
+
+
+
+
Set New Password
+
+ {% if validlink %}
+
Please enter your new password below.
+
+ {% if form.errors %}
+
+ {% for field in form %}
+ {% for error in field.errors %}
+ {{ error }}
+ {% endfor %}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% csrf_token %}
+
+ New Password
+ {{ form.new_password1 }}
+
+
+
+ Confirm New Password
+ {{ form.new_password2 }}
+
+
+ Change Password
+
+ {% else %}
+
+
The password reset link was invalid, possibly because it has already been used or has expired.
+
+
Please request a new password reset link.
+
Request New Reset Link
+ {% endif %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/password_reset_done.html b/templates/login/password_reset_done.html
new file mode 100644
index 0000000..4546ca0
--- /dev/null
+++ b/templates/login/password_reset_done.html
@@ -0,0 +1,107 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ Password Reset Email Sent - Student Management System
+
+
+
+
+
+
Email Sent
+
+
✓
+
+
+
We've emailed you instructions for setting your password. You should receive them shortly.
+
+
+
If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder.
+
+
← Return to Login
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/password_reset_otp.html b/templates/login/password_reset_otp.html
new file mode 100644
index 0000000..53e1117
--- /dev/null
+++ b/templates/login/password_reset_otp.html
@@ -0,0 +1,270 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ Verify OTP - Student Management System
+
+
+
+
+
+
Verify OTP
+
+ {% if is_email %}
+
+ Code sent to: {{ email|stringformat:"s"|slice:":3" }}***@{{ email|stringformat:"s"|slice:"-10:" }}
+ Edit Email
+
+ {% else %}
+
+ Code sent to: +91 ******{{ phone|stringformat:"s"|slice:"-4:" }}
+ Edit Number
+
+ {% endif %}
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+
+ {% if is_email %}
+
+ {% else %}
+
+ {% endif %}
+ {% csrf_token %}
+
+ Enter Verification Code
+
+ {% if is_email %}
+
+ {% else %}
+
+ {% endif %}
+
+
+ Verify Code
+
+
+
+ Resend code in 60 s
+ {% if is_email %}
+
+ {% else %}
+
+ {% endif %}
+ {% csrf_token %}
+ {% if is_email %}
+
+ {% else %}
+
+ {% endif %}
+ Resend Code
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/password_reset_phone.html b/templates/login/password_reset_phone.html
new file mode 100644
index 0000000..4b02282
--- /dev/null
+++ b/templates/login/password_reset_phone.html
@@ -0,0 +1,197 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ Reset Password - Student Management System
+
+
+
+
+
+
Reset Password
+
+ {% if is_email %}
+
Enter your email address and we'll send you a verification code to reset your password.
+ {% else %}
+
Enter your phone number and we'll send you a verification code to reset your password.
+ {% endif %}
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+ {% if success %}
+
+ {{ success }}
+
+ {% endif %}
+
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% csrf_token %}
+
+ {% if is_email %}
+ Email Address
+
+ {% else %}
+ Phone Number
+
+ {% endif %}
+
+
+ Send Verification Code
+
+
+
← Back to Reset Options
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/password_reset_set.html b/templates/login/password_reset_set.html
new file mode 100644
index 0000000..6fd33e3
--- /dev/null
+++ b/templates/login/password_reset_set.html
@@ -0,0 +1,160 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ Set New Password - Student Management System
+
+
+
+
+
+
Set New Password
+
+
Please enter your new password below.
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% csrf_token %}
+
+ New Password
+
+
+
+
+ Confirm New Password
+
+
+
+ {% if is_email %}
+
+ {% else %}
+
+ {% endif %}
+
+
+ Set New Password
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/password_reset_verify.html b/templates/login/password_reset_verify.html
new file mode 100644
index 0000000..a5b9c1f
--- /dev/null
+++ b/templates/login/password_reset_verify.html
@@ -0,0 +1,219 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ Verify OTP - Student Management System
+
+
+
+
+
+
Verify OTP
+
OTP sent to: {{ phone }}
+
+ {% if error %}
+
+ {{ error }}
+
+ {% endif %}
+
+ {% if messages %}
+
+ {% for message in messages %}
+ {{ message }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% csrf_token %}
+
+ Enter OTP
+
+
+
+
+ Verify OTP
+
+
+ Resend OTP in 60 s
+
+ {% csrf_token %}
+
+ Resend OTP
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/reset_options.html b/templates/login/reset_options.html
new file mode 100644
index 0000000..f365771
--- /dev/null
+++ b/templates/login/reset_options.html
@@ -0,0 +1,156 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ Reset Password - Student Management System
+
+
+
+
+
+
Reset Password
+
+
Select how you would like to reset your password:
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/login/sms_templates/otp_message.txt b/templates/login/sms_templates/otp_message.txt
new file mode 100644
index 0000000..8dca73a
--- /dev/null
+++ b/templates/login/sms_templates/otp_message.txt
@@ -0,0 +1,9 @@
+Dear {{ user_name }},
+
+Your OTP for secure access is: {{ otp }}. Valid for {{ expiry_time }} minutes.
+
+For security, do not share this OTP. If you didn't request this, please ignore.
+
+Best regards,
+Student Management System
+support@sms.com
\ No newline at end of file
diff --git a/templates/login/verify_reset_code.html b/templates/login/verify_reset_code.html
new file mode 100644
index 0000000..a2fa42c
--- /dev/null
+++ b/templates/login/verify_reset_code.html
@@ -0,0 +1,61 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
+
+
+
+
+ {% if message %}
+
{{ message }}
+ {% endif %}
+
+ {% if error %}
+
{{ error }}
+ {% endif %}
+
+
+ {% csrf_token %}
+
+ Email address
+
+
+
+
+ Reset Code
+
+
+ Enter the 6-digit code sent to your email.
+
+
+
+
+ New Password
+
+
+ Your password must contain at least 8 characters.
+
+
+
+
+ Confirm New Password
+
+
+
+
+ Reset Password
+
+
+
+
+
Didn't receive the code? Request a new one
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/student/dashboard.html b/templates/student/dashboard.html
deleted file mode 100644
index 8252370..0000000
--- a/templates/student/dashboard.html
+++ /dev/null
@@ -1,83 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block content %}
- Dashboard
- Welcome
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
diff --git a/templates/student_dashboard.html b/templates/student_dashboard.html
new file mode 100644
index 0000000..1b916a5
--- /dev/null
+++ b/templates/student_dashboard.html
@@ -0,0 +1,843 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load tz %}
+{% load app_filters %}
+
+
+{% block content %}
+
+{% csrf_token %}
+
+
+
+ {% include 'components/welcome_section.html' with user=student %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Days Attended
+
{{ attendance_summary.classes_attended }}
+ {% if attendance_summary.actual_classes %}
+ Classes Recorded
+ {{ attendance_summary.actual_classes }}
+ {% endif %}
+
+
+ Attendance Rate
+
{{ attendance_summary.attendance_percentage|floatformat:1 }}%
+ {% if attendance_summary.actual_percentage %}
+ Rate (Recorded Only)
+ {{ attendance_summary.actual_percentage|floatformat:1 }}%
+ {% endif %}
+
+
+
+
+
+
+ Minimum required: 75%
+ {% if attendance_summary.attendance_percentage < 75 %}
+ Below minimum requirement
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total Courses
+
{{ course_summary.total_courses }}
+
+
+ Completion Rate
+
{{ course_summary.completion_percentage|floatformat:1 }}%
+
+
+
+
+ Completed
+ {{ course_summary.completed_courses }}
+
+
+ Ongoing
+ {{ course_summary.ongoing_courses }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pending
+ {{ pending_leaves }}
+
+
+ Approved
+ {{ approved_leaves }}
+
+
+ Rejected
+ {{ rejected_leaves }}
+
+
+ Request
+
+
+
+
+
+
+
+
+
+
+
+ Course Details
+
+
+ {% if course_details %}
+ {% for course in course_details %}
+
+
+
{{ course.name }}
+
+ {{ course.progress_status }}
+
+
+
+
+
+
+ {{ course.completion_percentage }}%
+
+
+
+
+
+
+
+
+
+ Start Date
+ {{ course.start_date|date:"M d, Y" }}
+
+
+ Duration Type
+ {{ course.duration_type }}
+
+
+ Total Duration
+
+ {% if course.duration_type == "Semester" %}
+ {{ course.duration }} years ({{ course.duration|multiply:2 }} semesters)
+ {% else %}
+ {{ course.duration }} years
+ {% endif %}
+
+
+
+ Expected End Date
+ {{ course.expected_end_date|date:"M d, Y" }}
+
+ {% if course.actual_end_date %}
+
+ Actual End Date
+ {{ course.actual_end_date|date:"M d, Y" }}
+
+ {% endif %}
+
+
+
+
+
+
+
+ Current Semester
+ {{ course.current_semester }}
+
+
+ Semester Start
+ {{ course.semester_start_date|date:"M d, Y" }}
+
+
+ Semester End
+ {{ course.semester_end_date|date:"M d, Y" }}
+
+ {% if course.remaining_days is not None %}
+
+ Semester Days Remaining
+ {{ course.remaining_days }} days
+
+ {% endif %}
+ {% if course.total_remaining_days is not None %}
+
+ Total Course Days Remaining
+ {{ course.total_remaining_days }} days
+
+ {% endif %}
+
+
+
+
+
+ {% endfor %}
+ {% else %}
+
+ No courses assigned yet.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+ Current Semester Subjects
+
+ {% if current_subjects %}
+
+ {% for subject in current_subjects %}
+
+
+
+
{{ subject.name }}
+
+ Semester: {{ subject.semester_or_year }}
+
+
+
+ View Files
+
+
+
+
+
+ {% endfor %}
+
+ {% else %}
+
+ No subjects assigned for the current semester.
+
+ {% endif %}
+
+
+
+
+
+
+ Class Routine
+
+
+
+
+
+ Time
+ Subject
+ Teacher
+
+
+
+ {% if current_semester_routines %}
+ {% for routine in current_semester_routines %}
+
+ {{ routine.start_time|time:"H:i" }} - {{ routine.end_time|time:"H:i" }}
+ {{ routine.subject.name }}
+ {{ routine.teacher.name }}
+
+ {% endfor %}
+ {% elif has_routines %}
+
+ No routines for current semester (Semester {{ student.current_semester }}).
+
+ {% else %}
+
+ No routines assigned yet.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ Leave Management
+
+
+
+
+ Request Leave
+
+
+
+
+
+
+
+ Start Date
+ End Date
+ Message
+ Status
+ Created At
+
+
+
+ {% for leave in student_leaves %}
+
+ {{ leave.start_date|date:"M d, Y" }}
+ {{ leave.end_date|date:"M d, Y" }}
+ {{ leave.message }}
+
+
+ {{ leave.get_status_display }}
+
+
+ {{ leave.created_at|date:"M d, Y H:i" }}
+
+ {% empty %}
+
+ No leave requests found.
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ Feedback
+
+
+
+ Give Feedback
+
+
+
+
+
+
Teacher Feedback
+ {% for feedback in student.feedbacks.all %}
+
+
+
+
{{ feedback.teacher.name }}
+
+ {% for i in "12345"|make_list %}
+ {% with i_float=forloop.counter|floatformat:0|add:"0" %}
+ {% if feedback.rating >= i_float %}
+
+ {% elif feedback.rating >= i_float|add:"-0.5" %}
+
+ {% else %}
+
+ {% endif %}
+ {% endwith %}
+ {% endfor %}
+
+
+
+
{{ feedback.feedback_text }}
+
+
+ {{ feedback.created_at|timesince }} ago
+ {{ feedback.rating }}/5
+
+
+
+ {% empty %}
+
+ No feedback given to teachers yet.
+
+ {% endfor %}
+
+
+
Institute Feedback
+ {% for feedback in student.institute_feedbacks.all %}
+
+
+
+
{{ feedback.feedback_type }}
+
+ {% for i in "12345"|make_list %}
+ {% with i_float=forloop.counter|floatformat:0|add:"0" %}
+ {% if feedback.rating >= i_float %}
+
+ {% elif feedback.rating >= i_float|add:"-0.5" %}
+
+ {% else %}
+
+ {% endif %}
+ {% endwith %}
+ {% endfor %}
+
+
+
+
{{ feedback.feedback_text }}
+
+
+ {{ feedback.created_at|timesince }} ago
+ {{ feedback.rating }}/5
+
+
+
+ {% empty %}
+
+ No institute feedback given yet.
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+ Notices
+
+
+ {% for notice in notices %}
+
+
+ {% if notice.image %}
+
+ {% endif %}
+
+
{{ notice.title }}
+
{{ notice.message }}
+
+
{{ notice.created_at|date:"M d, Y" }}
+ {% if notice.file %}
+
+ Download
+
+ {% endif %}
+
+
+
+
+ {% empty %}
+
+
No notices available.
+
+ {% endfor %}
+
+
+
+
+
+
+
+ Profile Information
+
+
+
+
+
+
+ {% if student.image %}
+
+
+
+ {% else %}
+
+
+
+ {% endif %}
+
+
{{ student.name }}
+
+ {{ student.id }}
+ {{ student.email }}
+ {{ student.phone }}
+
+
+
+ {{ student.status }}
+
+ {% if student.course %}
+ {{ student.course.name }}
+ {% endif %}
+ Semester {{ student.current_semester }}
+
+
+
+
+
+ Update Photo
+
+
+ Change Password
+
+
+
+
+
+
+
+
+
+
+
+
+
Full Name
+
{{ student.name }}
+
+
+
Gender
+
{{ student.gender }}
+
+
+
Birth Date
+
{{ student.birth_date|date:"M d, Y" }}
+
+
+
Marital Status
+
{{ student.marital_status }}
+
+
+
Citizenship No.
+
{{ student.citizenship_no|default:"Not provided" }}
+
+
+
Joining Date
+
{{ student.joining_date|date:"M d, Y" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Email Address
+
{{ student.email }}
+
+
+
Phone Number
+
{{ student.phone }}
+
+
+
Parent Name
+
{{ student.parent_name|default:"Not provided" }}
+
+
+
Parent Phone
+
{{ student.parent_phone|default:"Not provided" }}
+
+
+
Temporary Address
+
{{ student.temporary_address|default:"Not provided" }}
+
+
+
Permanent Address
+
{{ student.permanent_address|default:"Not provided" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Course
+
{{ student.course.name|default:"Not assigned" }}
+
+
+
Current Semester
+
{{ student.current_semester }}
+
+
+
Joining Date
+
{{ student.joining_date|date:"M d, Y" }}
+
+
+
Batches
+
+ {% for batch in student.batches.all %}
+ {{ batch.name }}
+ {% empty %}
+ No batches assigned
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+{% include 'components/common_modals.html' with user=student %}
+
+
+{% include 'components/student_modals.html' %}
+{% include 'components/student_feedback_modal.html' %}
+
+
+
+
+
+
+{% include 'components/bottom_nav.html' with user=student %}
+
+
+
+
+
+ Attendance Record
+
+
+
+
+
+
+
+
Attendance Summary
+
+
+ Days Attended
+
{{ attendance_summary.classes_attended }}
+ {% if attendance_summary.actual_classes %}
+ Classes Recorded
+ {{ attendance_summary.actual_classes }}
+ {% endif %}
+
+
+ Attendance Rate
+
{{ attendance_summary.attendance_percentage|floatformat:1 }}%
+ {% if attendance_summary.actual_percentage %}
+ Rate (Recorded Only)
+ {{ attendance_summary.actual_percentage|floatformat:1 }}%
+ {% endif %}
+
+
+
+
+ Minimum required: 75%
+ {% if attendance_summary.attendance_percentage < 75 %}
+ Below minimum requirement
+ {% endif %}
+
+
+
+
+
+
+
+
+
Attendance Details
+
+
+
+
{{ attendance_summary.present_count }}
+
Present
+
+
+
+
+
{{ attendance_summary.absent_count }}
+
Absent
+
+
+
+
+
{{ attendance_summary.current_streak }}
+
Current Streak
+
+
+
+
+
{{ attendance_summary.longest_streak }}
+
Best Streak
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Date
+ Subject
+ Teacher
+ Status
+
+
+
+ {% for record in attendance_records %}
+
+ {{ record.attendance.date|date:"M d, Y" }}
+ {{ record.attendance.routine.subject.name }}
+ {{ record.attendance.teacher.name }}
+
+
+ {% if record.student_attend %}Present{% else %}Absent{% endif %}
+
+
+
+ {% empty %}
+
+ No attendance records found.
+
+ {% endfor %}
+
+
+
+
+
+
+{% endblock %}
+
diff --git a/templates/subject_files.html b/templates/subject_files.html
new file mode 100644
index 0000000..a3b89b1
--- /dev/null
+++ b/templates/subject_files.html
@@ -0,0 +1,327 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block content %}
+
+{% csrf_token %}
+
+
+
+ {% include 'components/welcome_section.html' with user=teacher %}
+
+
+
+
+
+
+
+ Upload New Material
+
+
+
+ {% if subjects %}
+
+ {% for subject in subjects %}
+
+
+
+
+
+ {{ subject.files_count }} file(s) available
+
+
+ {% if subject.files %}
+
+ {% for file in subject.files %}
+
+
+
+
+ {% if file.file_type == 'syllabus' %}
+
+ {% else %}
+
+ {% endif %}
+ {{ file.title }}
+
+
+ {% if file.description %}{{ file.description }}{% endif %}
+
+
+ {% if file.file_type == 'syllabus' %}
+ Syllabus
+ {% else %}
+ Study Material
+ {% endif %}
+ {% if file.uploaded_at %}
+ Uploaded {{ file.uploaded_at|timesince }} ago
+ {% endif %}
+ {% if file.uploaded_by %}
+ by {{ file.uploaded_by }}
+ {% endif %}
+
+
+
+
+
+ {% endfor %}
+
+ {% else %}
+
+ No files available for this subject.
+
+ {% endif %}
+
+
+
+ Upload to {{ subject.name }}
+
+
+
+
+
+ {% endfor %}
+
+ {% else %}
+
+ You don't have any subjects assigned.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
Upload Study Material
+ ×
+
+
+
+ {% csrf_token %}
+
+
+ Subject
+
+ Select a subject
+ {% for subject in subjects %}
+ {{ subject.name }} ({{ subject.course }})
+ {% endfor %}
+
+
+
+
+ Title
+
+
+
+
+ Description (Optional)
+
+
+
+
+
File
+
+
Upload PDF, DOCX, PPT or other study materials. Max file size: 10MB
+
+
+
+
+
+ Cancel
+
+ Upload
+
+
+
+
+
+
+
+
+
+
+
+
Confirm Deletion
+ ×
+
+
+
Are you sure you want to delete this file? This action cannot be undone.
+
+
+ Cancel
+
+ Delete
+
+
+
+
+
+
+
+{% include 'components/bottom_nav.html' with user=teacher %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/teacher/dashboard.html b/templates/teacher/dashboard.html
deleted file mode 100644
index 8252370..0000000
--- a/templates/teacher/dashboard.html
+++ /dev/null
@@ -1,83 +0,0 @@
-{% extends "admin/base_site.html" %}
-{% block content %}
- Dashboard
- Welcome
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
diff --git a/templates/teacher_attendance.html b/templates/teacher_attendance.html
new file mode 100644
index 0000000..848b204
--- /dev/null
+++ b/templates/teacher_attendance.html
@@ -0,0 +1,254 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load app_filters %}
+{% block extrastyle %}
+
+
+
+
+
+{% endblock %}
+{% block content %}
+
+
+
+
+
+
+
+
Your Routines
+ {% if routines %}
+
+ {% else %}
+
+ You don't have any active routines.
+
+ {% endif %}
+
+
+
+
+
+ {% if selected_routine %}
+
+
+
+
+ {% csrf_token %}
+
+
+
+
+
+ Teacher Present
+
+
+
+ Class Status
+
+
+ Conducted
+
+
+ Not Conducted
+
+
+
+
+ Student Attendance
+ {% if students %}
+
+
+
+
+ Mark All Present
+
+
+ Mark All Absent
+
+
+
+ Save Attendance
+
+
+ {% else %}
+
+ No students found for this course and semester.
+
+ {% endif %}
+
+ {% else %}
+
+
+
Select a routine to manage attendance
+
Choose a class from the list on the left to take attendance.
+
+ {% endif %}
+
+
+
+
+{% endblock %}
+{% block extrahead %}
+
+
+{% endblock %}
diff --git a/templates/teacher_dashboard.html b/templates/teacher_dashboard.html
new file mode 100644
index 0000000..a89b028
--- /dev/null
+++ b/templates/teacher_dashboard.html
@@ -0,0 +1,1108 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load tz %}
+{% load app_filters %}
+
+{% block content %}
+
+{% csrf_token %}
+
+
+
+ {% include 'components/welcome_section.html' with user=teacher %}
+
+
+
+
+
+
+
+
+
+
+
{{ total_students }}
+
Total Students
+
+
+
+
+
{{ present_students }}
+
Present Today
+
+
+
{{ absent_students }}
+
Absent Today
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ today_classes }}
+
Total Classes
+
+
+ {% if today_routines %}
+
+
+ {% for routine in today_routines|slice:":3" %}
+
+ {{ routine.name|truncatechars:15 }}
+
+ {% endfor %}
+ {% if today_routines|length > 3 %}
+
+ +{{ today_routines|length|add:"-3" }} more
+
+ {% endif %}
+
+
+ {% else %}
+
No classes scheduled for today
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
{{ avg_rating|floatformat:1 }}/5.0
+
+ {% for i in "12345"|make_list %}
+ {% if forloop.counter <= avg_rating_rounded %}
+
+ {% else %}
+
+ {% endif %}
+ {% endfor %}
+
+
Based on {{ recent_feedback.count }} student feedbacks
+
+
+
+
+
+
+
+
+
+
Pending
+
{{ pending_leaves }}
+
+
+
Approved
+
{{ approved_leaves }}
+
+
+
Rejected
+
{{ rejected_leaves }}
+
+
+ Request Leave
+
+
+
+
+
+
+
+
+
+
+
+
Today's Classes
+
+
+ {% if today_routines %}
+
+
+
+
+
+ Subject
+ Course
+ Semester/Year
+ Time
+ Attendance
+ Status
+ Action
+
+
+
+ {% for routine in today_routines %}
+
+ {{ routine.name }}
+ {{ routine.course_name }}
+ {{ routine.semester_or_year }}
+ {{ routine.start_time|time:"h:i A" }} - {{ routine.end_time|time:"h:i A" }}
+ {{ routine.attendance_count }}/{{ routine.total_students }}
+
+ {% if routine.is_completed %}
+ Completed
+ {% elif routine.is_ongoing %}
+ In Progress
+ {% else %}
+ Upcoming
+ {% endif %}
+
+
+ {% if routine.has_attendance %}
+ Recorded
+ {% else %}
+
+ Attendance
+
+ {% endif %}
+
+
+ {% endfor %}
+
+
+
+ {% else %}
+
+ No classes scheduled for today.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
Recent Feedback
+
+
+
+ {{ avg_rating|floatformat:1 }}/5.0
+
+
+ {% for i in "12345"|make_list %}
+ {% if forloop.counter <= avg_rating_rounded %}
+
+ {% else %}
+
+ {% endif %}
+ {% endfor %}
+
+
+ Based on {{ recent_feedback.count }} feedbacks
+
+
+
+
+
+ {% if recent_feedback %}
+
+ {% for feedback in recent_feedback %}
+
+
+
+
+
{{ feedback.feedback_text }}
+
+
+
+ {% endfor %}
+
+ {% else %}
+
+
+
No recent feedback yet.
+
+ {% endif %}
+
+
+
+
+
+
+
Recent Notices
+ {% if notices %}
+
+ {% for notice in notices %}
+
+
+
{{ notice.title }}
+ {{ notice.created_at|timesince }} ago
+
+
{{ notice.message }}
+
+ {% endfor %}
+
+ {% else %}
+
+ No recent notices.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
Your Class Schedule
+
+ {% if all_routines %}
+
+
+ {% for routine in all_routines %}
+
+
+
{{ routine.subject.name }}
+ {{ routine.start_time|time:"h:i A" }} - {{ routine.end_time|time:"h:i A" }}
+
+
{{ routine.course.name }} - Semester {{ routine.semester_or_year }}
+
+ {% endfor %}
+
+
+ {% else %}
+
+ No class schedules found.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
Manage Attendance
+
+ Back to Records
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Notices & Announcements
+
+ {% if notices %}
+
+ {% for notice in notices %}
+
+
+
{{ notice.title }}
+ {{ notice.created_at|date:"M d, Y" }}
+
+
{{ notice.message }}
+
Posted {{ notice.created_at|timesince }} ago
+
+ {% endfor %}
+
+ {% else %}
+
+ No notices available.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
Give Feedback to Institute
+
+ {% csrf_token %}
+
+ Feedback Type
+
+ Select Type
+ {% for value, name in feedback_types %}
+ {{ name }}
+ {% endfor %}
+
+
+
+
+
Rating
+
+
+
+
+
+
+
+
Please select a rating
+
+
+
+
+ Feedback
+
+
+
+
+
+ Submit anonymously
+
+
+
+ Submit Feedback
+
+
+
+
+
+
+
+
+
Your Recent Feedback
+
+ {% if institute_feedback %}
+
+ {% for feedback in institute_feedback %}
+
+
+
+
+
{{ feedback.feedback_text }}
+
+
+ {% if feedback.is_anonymous %}
+ Anonymous
+ {% else %}
+ Public
+ {% endif %}
+
+
+
+ {% endfor %}
+
+ {% else %}
+
+
+
You haven't submitted any feedback yet.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ Profile Information
+
+
+
+
+
+
+ {% if teacher.image %}
+
+ {% else %}
+
+ {% endif %}
+
+
{{ teacher.name }}
+
+ {{ teacher.id }}
+ {{ teacher.email }}
+ {{ teacher.phone }}
+
+
+
+ {{ teacher.is_active|yesno:"Active,Inactive" }}
+
+ {{ teacher.designation }}
+
+
+
+
+
+ Update Photo
+
+
+ Change Password
+
+
+
+
+
+
+
+
+
+
+
+
+
Full Name
+
{{ teacher.name }}
+
+
+
Gender
+
{{ teacher.gender|default:"Not provided" }}
+
+
+
Birth Date
+
{{ teacher.birth_date|date:"M d, Y"|default:"Not provided" }}
+
+
+
Marital Status
+
{{ teacher.marital_status|default:"Not provided" }}
+
+
+
Citizenship No.
+
{{ teacher.citizenship_no|default:"Not provided" }}
+
+
+
Passport
+
{{ teacher.passport|default:"Not provided" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Email Address
+
{{ teacher.email|default:"Not provided" }}
+
+
+
Phone Number
+
{{ teacher.phone|default:"Not provided" }}
+
+
+
Parent Name
+
{{ teacher.parent_name|default:"Not provided" }}
+
+
+
Parent Phone
+
{{ teacher.parent_phone|default:"Not provided" }}
+
+
+
Temporary Address
+
{{ teacher.temporary_address|default:"Not provided" }}
+
+
+
Permanent Address
+
{{ teacher.permanent_address|default:"Not provided" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Employee ID
+
{{ teacher.id }}
+
+
+
Designation
+
{{ teacher.designation|default:"Not provided" }}
+
+
+
Joining Date
+
{{ teacher.joining_date|date:"M d, Y"|default:"Not provided" }}
+
+
+
Status
+
+
+ {{ teacher.is_active|yesno:"Active,Inactive" }}
+
+
+
+
+
Permissions
+
+ {% for group in teacher.groups.all %}
+ {{ group.name }}
+ {% empty %}
+ No specific permissions
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Educational Background
+ {% if teacher.education_set.all %}
+ {% for education in teacher.education_set.all %}
+
+
{{ education.degree }}, {{ education.institution }} ({{ education.year }})
+
{{ education.specialization }}
+
+ {% endfor %}
+ {% else %}
+
No educational qualifications recorded
+ {% endif %}
+
+ {% if teacher.qualification %}
+
+
Additional Qualifications
+
{{ teacher.qualification }}
+
+ {% endif %}
+
+
+ {% if teacher.skills %}
+
+
Skills & Specialization
+
{{ teacher.specialization|default:"No specialization information available" }}
+
+
+ {% for skill in teacher.skills %}
+ {{ skill.strip }}
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Leave Management
+
+
+
+
+ Request Leave
+
+
+
+
+
+
+
+
+
+
{{ pending_leaves }}
+
Pending Requests
+
+
+
+
+
+
+
+
{{ approved_leaves }}
+
Approved Requests
+
+
+
+
+
+
+
+
{{ rejected_leaves }}
+
Rejected Requests
+
+
+
+
+
+
+
Leave History
+
+
+
+
+ Start Date
+ End Date
+ Reason
+ Status
+ Applied On
+
+
+
+ {% if recent_leaves %}
+ {% for leave in recent_leaves %}
+
+ {{ leave.start_date|date:"M d, Y" }}
+ {{ leave.end_date|date:"M d, Y" }}
+ {{ leave.message }}
+
+
+ {{ leave.get_status_display }}
+
+
+ {{ leave.created_at|date:"M d, Y" }}
+
+ {% endfor %}
+ {% else %}
+
+ No leave history available
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Your Subjects & Materials
+
+
+
+
+
+ {% for routine in teacher_subjects %}
+
+
+
+
+
+
+
+ {{ routine.start_time|time:"h:i A" }} - {{ routine.end_time|time:"h:i A" }}
+
+
+ {{ routine.students_count|default:"0" }} students
+
+
+
+
+
+
+
+
+ {% empty %}
+
+
+ You don't have any subjects assigned.
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+ Manage Subject Materials
+
+
+
+ Back to Subjects
+
+
+
+
+
+
+
+
+
+
+
+
+
Upload Study Material
+ ×
+
+
+
+ {% csrf_token %}
+
+
+ Subject
+
+ Select a subject
+ {% for subject in teacher_subjects %}
+ {{ subject.subject.name }} ({{ subject.course.name }})
+ {% endfor %}
+
+
+
+
+ Title
+
+
+
+
+ Description (Optional)
+
+
+
+
+
File
+
+
Upload PDF, DOCX, PPT or other study materials. Max file size: 10MB
+
+
+
+
+
+ Cancel
+
+ Upload
+
+
+
+
+
+
+
+
+{% include 'components/common_modals.html' with user=teacher %}
+
+
+{% include 'components/teacher_modals.html' %}
+{% include 'components/teacher_feedback_modal.html' %}
+
+
+{% include 'components/bottom_nav.html' with user=teacher %}
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block extrastyle %}
+{{ block.super }}
+
+
+
+
+{% endblock %}
\ No newline at end of file