Upload 19 files
Browse files- SY-21_timetable.xlsx +0 -0
- SY-22_timetable.xlsx +0 -0
- app.py +432 -0
- attendance_data/SY-22.xlsx +0 -0
- config.db +0 -0
- face_dataset/SY-22/ADT24SOCBD053_Harshvardhan Dhane.npy +3 -0
- haarcascade_frontalface_alt.xml +0 -0
- instance/users.db +0 -0
- requirements.txt +5 -0
- static/js/script.js +447 -0
- static/js/webcam.js +24 -0
- static/script.js +447 -0
- static/script.js.bak +360 -0
- templates/index.html +654 -0
- templates/mark.html +198 -0
- templates/register.html +182 -0
- templates/student_dashboard.html +63 -0
- templates/teacher_dashboard.html +198 -0
- templates/timetable.html +692 -0
SY-21_timetable.xlsx
ADDED
|
Binary file (5.38 kB). View file
|
|
|
SY-22_timetable.xlsx
ADDED
|
Binary file (5.36 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_file
|
| 2 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 3 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 4 |
+
import sqlite3
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import os
|
| 7 |
+
import cv2
|
| 8 |
+
import numpy as np
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import base64
|
| 11 |
+
|
| 12 |
+
app = Flask(__name__)
|
| 13 |
+
app.config['SECRET_KEY'] = 'your_secret_key_here'
|
| 14 |
+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
|
| 15 |
+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
| 16 |
+
db = SQLAlchemy(app)
|
| 17 |
+
|
| 18 |
+
# User Model
|
| 19 |
+
class User(db.Model):
|
| 20 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 21 |
+
full_name = db.Column(db.String(100), nullable=False)
|
| 22 |
+
email = db.Column(db.String(100), unique=True, nullable=False)
|
| 23 |
+
username = db.Column(db.String(50), unique=True, nullable=False)
|
| 24 |
+
password = db.Column(db.String(100), nullable=False)
|
| 25 |
+
role = db.Column(db.String(10), nullable=False)
|
| 26 |
+
enrollment_number = db.Column(db.String(20), unique=True, nullable=True)
|
| 27 |
+
|
| 28 |
+
# Create database
|
| 29 |
+
with app.app_context():
|
| 30 |
+
db.create_all()
|
| 31 |
+
|
| 32 |
+
# Timetable Database Setup
|
| 33 |
+
def get_db_connection():
|
| 34 |
+
conn = sqlite3.connect('config.db')
|
| 35 |
+
conn.row_factory = sqlite3.Row
|
| 36 |
+
return conn
|
| 37 |
+
|
| 38 |
+
def init_timetable_db():
|
| 39 |
+
conn = get_db_connection()
|
| 40 |
+
conn.execute('''
|
| 41 |
+
CREATE TABLE IF NOT EXISTS teacher_config (
|
| 42 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 43 |
+
class_name TEXT NOT NULL,
|
| 44 |
+
config_type TEXT NOT NULL,
|
| 45 |
+
name TEXT NOT NULL
|
| 46 |
+
)
|
| 47 |
+
''')
|
| 48 |
+
conn.commit()
|
| 49 |
+
conn.close()
|
| 50 |
+
|
| 51 |
+
# Attendance System Setup
|
| 52 |
+
face_cascade = cv2.CascadeClassifier("haarcascade_frontalface_alt.xml")
|
| 53 |
+
dataset_path = "./face_dataset/"
|
| 54 |
+
attendance_path = "./attendance_data/"
|
| 55 |
+
os.makedirs(dataset_path, exist_ok=True)
|
| 56 |
+
os.makedirs(attendance_path, exist_ok=True)
|
| 57 |
+
|
| 58 |
+
# Combined Routes
|
| 59 |
+
@app.route('/')
|
| 60 |
+
def index():
|
| 61 |
+
return render_template('index.html')
|
| 62 |
+
|
| 63 |
+
# Authentication Routes (from interface code)
|
| 64 |
+
@app.route('/signup', methods=['POST'])
|
| 65 |
+
def signup():
|
| 66 |
+
data = request.json
|
| 67 |
+
full_name = data.get('full_name')
|
| 68 |
+
email = data.get('email')
|
| 69 |
+
username = data.get('username')
|
| 70 |
+
password = data.get('password')
|
| 71 |
+
role = data.get('role')
|
| 72 |
+
enrollment_number = data.get('enrollment_number') if role == 'student' else None
|
| 73 |
+
|
| 74 |
+
if not all([full_name, email, username, password, role]):
|
| 75 |
+
return jsonify({'success': False, 'message': 'All fields are required!'})
|
| 76 |
+
|
| 77 |
+
if role == 'student' and not enrollment_number:
|
| 78 |
+
return jsonify({'success': False, 'message': 'Enrollment number is required for students!'})
|
| 79 |
+
|
| 80 |
+
if User.query.filter_by(username=username).first() or User.query.filter_by(email=email).first():
|
| 81 |
+
return jsonify({'success': False, 'message': 'Username or email already exists!'})
|
| 82 |
+
|
| 83 |
+
hashed_password = generate_password_hash(password)
|
| 84 |
+
new_user = User(
|
| 85 |
+
full_name=full_name,
|
| 86 |
+
email=email,
|
| 87 |
+
username=username,
|
| 88 |
+
password=hashed_password,
|
| 89 |
+
role=role,
|
| 90 |
+
enrollment_number=enrollment_number
|
| 91 |
+
)
|
| 92 |
+
db.session.add(new_user)
|
| 93 |
+
db.session.commit()
|
| 94 |
+
|
| 95 |
+
return jsonify({'success': True, 'message': 'Account created successfully! Please log in.'})
|
| 96 |
+
|
| 97 |
+
@app.route('/login', methods=['POST'])
|
| 98 |
+
def login():
|
| 99 |
+
data = request.json
|
| 100 |
+
username = data.get('username')
|
| 101 |
+
password = data.get('password')
|
| 102 |
+
role = data.get('role')
|
| 103 |
+
|
| 104 |
+
user = User.query.filter_by(username=username, role=role).first()
|
| 105 |
+
if user and check_password_hash(user.password, password):
|
| 106 |
+
session['user_id'] = user.id
|
| 107 |
+
session['role'] = user.role
|
| 108 |
+
redirect_url = '/teacher_dashboard' if role == 'teacher' else '/student_dashboard'
|
| 109 |
+
return jsonify({'success': True, 'redirect': redirect_url})
|
| 110 |
+
return jsonify({'success': False, 'message': 'Invalid username or password'})
|
| 111 |
+
|
| 112 |
+
@app.route('/logout')
|
| 113 |
+
def logout():
|
| 114 |
+
session.clear()
|
| 115 |
+
return redirect(url_for('index'))
|
| 116 |
+
|
| 117 |
+
@app.route('/teacher_dashboard')
|
| 118 |
+
def teacher_dashboard():
|
| 119 |
+
if 'user_id' not in session or session['role'] != 'teacher':
|
| 120 |
+
return redirect(url_for('index'))
|
| 121 |
+
user = User.query.get(session['user_id'])
|
| 122 |
+
return render_template('teacher_dashboard.html', user=user)
|
| 123 |
+
|
| 124 |
+
# get_attendance_data
|
| 125 |
+
@app.route('/get_attendance_data', methods=['GET'])
|
| 126 |
+
def get_attendance_data():
|
| 127 |
+
class_name = request.args.get("class")
|
| 128 |
+
if not class_name:
|
| 129 |
+
return jsonify({"error": "Class parameter is required"}), 400
|
| 130 |
+
|
| 131 |
+
filename = os.path.join(attendance_path, f"{class_name}.xlsx")
|
| 132 |
+
if not os.path.exists(filename):
|
| 133 |
+
return jsonify({"error": f"Attendance file not found for {class_name}"}), 404
|
| 134 |
+
|
| 135 |
+
try:
|
| 136 |
+
# Read Excel file with proper datetime handling
|
| 137 |
+
df = pd.read_excel(filename, parse_dates=['Date'])
|
| 138 |
+
|
| 139 |
+
# Convert datetime columns to readable strings
|
| 140 |
+
if 'Date' in df.columns:
|
| 141 |
+
df['Date'] = df['Date'].dt.strftime('%Y-%m-%d')
|
| 142 |
+
if 'CheckIn' in df.columns:
|
| 143 |
+
df['CheckIn'] = pd.to_datetime(df['CheckIn'], errors='coerce').dt.strftime('%H:%M:%S')
|
| 144 |
+
if 'CheckOut' in df.columns:
|
| 145 |
+
df['CheckOut'] = pd.to_datetime(df['CheckOut'], errors='coerce').dt.strftime('%H:%M:%S')
|
| 146 |
+
|
| 147 |
+
# Replace NaN/NaT with empty strings
|
| 148 |
+
df = df.fillna('')
|
| 149 |
+
|
| 150 |
+
# Convert to list of dictionaries
|
| 151 |
+
data = df.to_dict(orient='records')
|
| 152 |
+
return jsonify({
|
| 153 |
+
"success": True,
|
| 154 |
+
"data": data,
|
| 155 |
+
"columns": df.columns.tolist(),
|
| 156 |
+
"count": len(data)
|
| 157 |
+
})
|
| 158 |
+
except Exception as e:
|
| 159 |
+
return jsonify({
|
| 160 |
+
"error": f"Error processing attendance data: {str(e)}",
|
| 161 |
+
"success": False
|
| 162 |
+
}), 500
|
| 163 |
+
|
| 164 |
+
@app.route('/student_dashboard')
|
| 165 |
+
def student_dashboard():
|
| 166 |
+
if 'user_id' not in session or session['role'] != 'student':
|
| 167 |
+
return redirect(url_for('index'))
|
| 168 |
+
user = User.query.get(session['user_id'])
|
| 169 |
+
return render_template('student_dashboard.html', user=user)
|
| 170 |
+
|
| 171 |
+
# Timetable Routes (from timetable code)
|
| 172 |
+
def get_excel_filename(class_name):
|
| 173 |
+
if class_name:
|
| 174 |
+
return f"{class_name}_timetable.xlsx"
|
| 175 |
+
return "timetable.xlsx"
|
| 176 |
+
|
| 177 |
+
@app.route('/timetable')
|
| 178 |
+
def timetable_page():
|
| 179 |
+
if 'user_id' not in session or session['role'] != 'teacher':
|
| 180 |
+
return redirect(url_for('index'))
|
| 181 |
+
return render_template('timetable.html')
|
| 182 |
+
|
| 183 |
+
@app.route('/add_config', methods=['POST'])
|
| 184 |
+
def add_config():
|
| 185 |
+
data = request.get_json()
|
| 186 |
+
class_name = data.get("class")
|
| 187 |
+
config_type = data.get("type")
|
| 188 |
+
name = data.get("name")
|
| 189 |
+
if not (class_name and config_type and name):
|
| 190 |
+
return jsonify({"error": "Missing parameters"}), 400
|
| 191 |
+
conn = get_db_connection()
|
| 192 |
+
conn.execute('INSERT INTO teacher_config (class_name, config_type, name) VALUES (?, ?, ?)',
|
| 193 |
+
(class_name, config_type, name))
|
| 194 |
+
conn.commit()
|
| 195 |
+
conn.close()
|
| 196 |
+
return jsonify({"message": "Added successfully"})
|
| 197 |
+
|
| 198 |
+
@app.route('/delete_config', methods=['POST'])
|
| 199 |
+
def delete_config():
|
| 200 |
+
data = request.get_json()
|
| 201 |
+
class_name = data.get("class")
|
| 202 |
+
config_type = data.get("type")
|
| 203 |
+
name = data.get("name")
|
| 204 |
+
if not (class_name and config_type and name):
|
| 205 |
+
return jsonify({"error": "Missing parameters"}), 400
|
| 206 |
+
conn = get_db_connection()
|
| 207 |
+
conn.execute('DELETE FROM teacher_config WHERE class_name=? AND config_type=? AND name=?',
|
| 208 |
+
(class_name, config_type, name))
|
| 209 |
+
conn.commit()
|
| 210 |
+
conn.close()
|
| 211 |
+
return jsonify({"message": "Deleted successfully"})
|
| 212 |
+
|
| 213 |
+
@app.route('/get_config', methods=['GET'])
|
| 214 |
+
def get_config():
|
| 215 |
+
class_name = request.args.get("class")
|
| 216 |
+
config_type = request.args.get("type")
|
| 217 |
+
if not (class_name and config_type):
|
| 218 |
+
return jsonify([])
|
| 219 |
+
conn = get_db_connection()
|
| 220 |
+
configs = conn.execute('SELECT name FROM teacher_config WHERE class_name=? AND config_type=?',
|
| 221 |
+
(class_name, config_type)).fetchall()
|
| 222 |
+
conn.close()
|
| 223 |
+
return jsonify([row["name"] for row in configs])
|
| 224 |
+
|
| 225 |
+
@app.route('/load', methods=['GET'])
|
| 226 |
+
def load_timetable():
|
| 227 |
+
class_name = request.args.get('class')
|
| 228 |
+
filename = get_excel_filename(class_name)
|
| 229 |
+
if not os.path.exists(filename):
|
| 230 |
+
return jsonify({})
|
| 231 |
+
df = pd.read_excel(filename, index_col=0)
|
| 232 |
+
return df.to_json()
|
| 233 |
+
|
| 234 |
+
@app.route('/save', methods=['POST'])
|
| 235 |
+
def save_timetable():
|
| 236 |
+
payload = request.get_json()
|
| 237 |
+
teacher_class = payload.get("class", "")
|
| 238 |
+
data = payload.get("timetable", {})
|
| 239 |
+
filename = get_excel_filename(teacher_class)
|
| 240 |
+
df = pd.DataFrame(data)
|
| 241 |
+
df.to_excel(filename)
|
| 242 |
+
return jsonify({'message': f'Saved successfully to {filename}!'})
|
| 243 |
+
|
| 244 |
+
from flask import send_file
|
| 245 |
+
|
| 246 |
+
@app.route('/download_excel', methods=['GET'])
|
| 247 |
+
def download_excel():
|
| 248 |
+
class_name = request.args.get('class')
|
| 249 |
+
filename = get_excel_filename(class_name)
|
| 250 |
+
if not os.path.exists(filename):
|
| 251 |
+
return "File not found", 404
|
| 252 |
+
return send_file(filename, as_attachment=True)
|
| 253 |
+
|
| 254 |
+
# Attendance Routes (from attendance code)
|
| 255 |
+
|
| 256 |
+
# KNN distance utility
|
| 257 |
+
def distance(v1, v2):
|
| 258 |
+
return np.sqrt(((v1 - v2) ** 2).sum())
|
| 259 |
+
|
| 260 |
+
def knn(train, test, k=5):
|
| 261 |
+
dist = []
|
| 262 |
+
for i in range(train.shape[0]):
|
| 263 |
+
ix = train[i, :-1]
|
| 264 |
+
iy = train[i, -1]
|
| 265 |
+
d = distance(test, ix)
|
| 266 |
+
dist.append([d, iy])
|
| 267 |
+
dk = sorted(dist, key=lambda x: x[0])[:k]
|
| 268 |
+
labels = np.array(dk)[:, -1]
|
| 269 |
+
return np.unique(labels, return_counts=True)[0][0]
|
| 270 |
+
|
| 271 |
+
# Attendance management per class
|
| 272 |
+
class AttendanceSystem:
|
| 273 |
+
def __init__(self, class_name):
|
| 274 |
+
self.class_name = class_name
|
| 275 |
+
self.file = os.path.join(attendance_path, f"{class_name}.xlsx")
|
| 276 |
+
self.columns = ["Enrollment", "Name", "Date", "CheckIn", "CheckOut"]
|
| 277 |
+
if not os.path.exists(self.file):
|
| 278 |
+
df = pd.DataFrame(columns=self.columns)
|
| 279 |
+
df.to_excel(self.file, index=False)
|
| 280 |
+
|
| 281 |
+
def checkin(self, enrollment, name):
|
| 282 |
+
today = datetime.now().strftime("%Y-%m-%d")
|
| 283 |
+
now = datetime.now().strftime("%H:%M:%S")
|
| 284 |
+
df = pd.read_excel(self.file)
|
| 285 |
+
existing = df[(df["Enrollment"] == enrollment) & (df["Date"] == today)]
|
| 286 |
+
if existing.empty:
|
| 287 |
+
new_entry = pd.DataFrame([[enrollment, name, today, now, ""]], columns=self.columns)
|
| 288 |
+
df = pd.concat([df, new_entry], ignore_index=True)
|
| 289 |
+
df.to_excel(self.file, index=False)
|
| 290 |
+
return True, "Check-in successful"
|
| 291 |
+
return False, "Already checked in"
|
| 292 |
+
|
| 293 |
+
def checkout(self, enrollment):
|
| 294 |
+
today = datetime.now().strftime("%Y-%m-%d")
|
| 295 |
+
now = datetime.now().strftime("%H:%M:%S")
|
| 296 |
+
df = pd.read_excel(self.file)
|
| 297 |
+
idx = df[(df["Enrollment"] == enrollment) & (df["Date"] == today)].index
|
| 298 |
+
if idx.empty:
|
| 299 |
+
return False, "Please check-in first"
|
| 300 |
+
row = df.loc[idx[0]]
|
| 301 |
+
if pd.isna(row["CheckOut"]) or row["CheckOut"] == "":
|
| 302 |
+
df.at[idx[0], "CheckOut"] = now
|
| 303 |
+
df.to_excel(self.file, index=False)
|
| 304 |
+
return True, "Check-out successful"
|
| 305 |
+
return False, "Already checked out"
|
| 306 |
+
|
| 307 |
+
@app.route('/mark_attendance')
|
| 308 |
+
def mark_attendance_page():
|
| 309 |
+
return render_template('mark.html')
|
| 310 |
+
|
| 311 |
+
@app.route('/register_face')
|
| 312 |
+
def register_face_page():
|
| 313 |
+
if 'user_id' not in session or session['role'] != 'student':
|
| 314 |
+
return redirect(url_for('index'))
|
| 315 |
+
return render_template('register.html')
|
| 316 |
+
|
| 317 |
+
# ... include all attendance API routes (/api/register_face, /api/identify_face, /api/checkin, /api/checkout) ...
|
| 318 |
+
|
| 319 |
+
@app.route('/api/register_face', methods=['POST'])
|
| 320 |
+
def register_face():
|
| 321 |
+
data = request.json
|
| 322 |
+
class_name = data['class_name']
|
| 323 |
+
name = data['name']
|
| 324 |
+
enrollment = data['enrollment']
|
| 325 |
+
images = data['images'] # List of base64 images
|
| 326 |
+
|
| 327 |
+
class_folder = os.path.join(dataset_path, class_name)
|
| 328 |
+
os.makedirs(class_folder, exist_ok=True)
|
| 329 |
+
|
| 330 |
+
face_data = []
|
| 331 |
+
|
| 332 |
+
for img_data in images:
|
| 333 |
+
img_bytes = base64.b64decode(img_data.split(",")[1])
|
| 334 |
+
np_arr = np.frombuffer(img_bytes, np.uint8)
|
| 335 |
+
img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
|
| 336 |
+
|
| 337 |
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 338 |
+
gray = cv2.equalizeHist(gray)
|
| 339 |
+
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
|
| 340 |
+
|
| 341 |
+
for (x, y, w, h) in faces[:1]:
|
| 342 |
+
face = img[y:y+h, x:x+w]
|
| 343 |
+
face = cv2.resize(face, (100, 100))
|
| 344 |
+
face_data.append(face.flatten())
|
| 345 |
+
flipped = cv2.flip(face, 1)
|
| 346 |
+
face_data.append(flipped.flatten())
|
| 347 |
+
|
| 348 |
+
if face_data:
|
| 349 |
+
face_data = np.array(face_data)
|
| 350 |
+
filename = f"{enrollment}_{name}.npy"
|
| 351 |
+
np.save(os.path.join(class_folder, filename), face_data)
|
| 352 |
+
return jsonify({"status": "success", "message": f"{len(face_data)} faces saved"})
|
| 353 |
+
return jsonify({"status": "fail", "message": "No faces detected"})
|
| 354 |
+
|
| 355 |
+
# API to identify face without marking
|
| 356 |
+
@app.route('/api/identify_face', methods=['POST'])
|
| 357 |
+
def identify_face():
|
| 358 |
+
data = request.json
|
| 359 |
+
class_name = data['class_name']
|
| 360 |
+
img_data = data['image']
|
| 361 |
+
img_bytes = base64.b64decode(img_data.split(",")[1])
|
| 362 |
+
np_arr = np.frombuffer(img_bytes, np.uint8)
|
| 363 |
+
img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
|
| 364 |
+
|
| 365 |
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 366 |
+
faces = face_cascade.detectMultiScale(gray, 1.3, 5)
|
| 367 |
+
|
| 368 |
+
class_folder = os.path.join(dataset_path, class_name)
|
| 369 |
+
if not os.path.exists(class_folder):
|
| 370 |
+
return jsonify({"status": "fail", "message": "No data for this class"})
|
| 371 |
+
|
| 372 |
+
# Load training data for this class
|
| 373 |
+
face_data = []
|
| 374 |
+
labels = []
|
| 375 |
+
names = {}
|
| 376 |
+
class_id = 0
|
| 377 |
+
for file in os.listdir(class_folder):
|
| 378 |
+
if file.endswith('.npy'):
|
| 379 |
+
data_arr = np.load(os.path.join(class_folder, file))
|
| 380 |
+
face_data.append(data_arr)
|
| 381 |
+
parts = file[:-4].split('_', 1)
|
| 382 |
+
labels.extend([class_id] * data_arr.shape[0])
|
| 383 |
+
names[class_id] = {'enrollment': parts[0], 'name': parts[1]}
|
| 384 |
+
class_id += 1
|
| 385 |
+
|
| 386 |
+
if not face_data:
|
| 387 |
+
return jsonify({"status": "fail", "message": "No trained data found"})
|
| 388 |
+
|
| 389 |
+
X_train = np.concatenate(face_data, axis=0)
|
| 390 |
+
y_train = np.array(labels).reshape(-1, 1)
|
| 391 |
+
trainset = np.hstack((X_train, y_train))
|
| 392 |
+
|
| 393 |
+
for (x, y, w, h) in faces[:1]:
|
| 394 |
+
face = img[y:y+h, x:x+w]
|
| 395 |
+
face = cv2.resize(face, (100, 100)).flatten()
|
| 396 |
+
pred_id = knn(trainset, face)
|
| 397 |
+
info = names.get(pred_id)
|
| 398 |
+
if info:
|
| 399 |
+
return jsonify({
|
| 400 |
+
"status": "success",
|
| 401 |
+
"name": info['name'],
|
| 402 |
+
"enrollment": info['enrollment']
|
| 403 |
+
})
|
| 404 |
+
return jsonify({"status": "fail", "message": "Face not recognized"})
|
| 405 |
+
|
| 406 |
+
# API to check-in
|
| 407 |
+
@app.route('/api/checkin', methods=['POST'])
|
| 408 |
+
def api_checkin():
|
| 409 |
+
data = request.json
|
| 410 |
+
class_name = data['class_name']
|
| 411 |
+
enrollment = data['enrollment']
|
| 412 |
+
name = data['name']
|
| 413 |
+
attendance = AttendanceSystem(class_name)
|
| 414 |
+
ok, msg = attendance.checkin(enrollment, name)
|
| 415 |
+
status = "success" if ok else "fail"
|
| 416 |
+
return jsonify({"status": status, "message": msg})
|
| 417 |
+
|
| 418 |
+
# API to check-out
|
| 419 |
+
@app.route('/api/checkout', methods=['POST'])
|
| 420 |
+
def api_checkout():
|
| 421 |
+
data = request.json
|
| 422 |
+
class_name = data['class_name']
|
| 423 |
+
enrollment = data['enrollment']
|
| 424 |
+
attendance = AttendanceSystem(class_name)
|
| 425 |
+
ok, msg = attendance.checkout(enrollment)
|
| 426 |
+
status = "success" if ok else "fail"
|
| 427 |
+
return jsonify({"status": status, "message": msg})
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
if __name__ == '__main__':
|
| 432 |
+
app.run(host='0.0.0.0', port=5000, debug=True)
|
attendance_data/SY-22.xlsx
ADDED
|
Binary file (5.02 kB). View file
|
|
|
config.db
ADDED
|
Binary file (12.3 kB). View file
|
|
|
face_dataset/SY-22/ADT24SOCBD053_Harshvardhan Dhane.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:643abd939a003fc3dd00cbe61fdf71a04b942524674e7931f066c154be041cb0
|
| 3 |
+
size 7140128
|
haarcascade_frontalface_alt.xml
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
instance/users.db
ADDED
|
Binary file (20.5 kB). View file
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
flask-sqlalchemy
|
| 3 |
+
pandas
|
| 4 |
+
opencv-python
|
| 5 |
+
numpy
|
static/js/script.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Global teacher config arrays (populated via AJAX).
|
| 2 |
+
let teacherClass = "";
|
| 3 |
+
let subjectList = [];
|
| 4 |
+
let specSubjectList = [];
|
| 5 |
+
let batchList = [];
|
| 6 |
+
let practicalSubjectList = [];
|
| 7 |
+
|
| 8 |
+
// Timetable parameters.
|
| 9 |
+
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
| 10 |
+
const timeSlots = [
|
| 11 |
+
'08.45 - 09.40', // index 0
|
| 12 |
+
'09.40 - 10.35', // index 1
|
| 13 |
+
'10.35 - 10.50', // index 2 -> Short Break (fixed)
|
| 14 |
+
'10.50 - 11.45', // index 3
|
| 15 |
+
'11.45 - 12.40', // index 4
|
| 16 |
+
'12.40 - 01.40', // index 5 -> Lunch Break (fixed)
|
| 17 |
+
'01.40 - 02.35', // index 6
|
| 18 |
+
'02.35 - 03.30', // index 7
|
| 19 |
+
'03.30 - 03.40', // index 8 -> Short Break (fixed)
|
| 20 |
+
'03.40 - 04.30' // index 9
|
| 21 |
+
];
|
| 22 |
+
// Mapping break indices to fixed texts.
|
| 23 |
+
const breakSlots = {
|
| 24 |
+
2: "Short Break",
|
| 25 |
+
5: "Lunch Break",
|
| 26 |
+
8: "Short Break"
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
// Two-hour practical time slot definitions.
|
| 30 |
+
const practicalTimeSlots = [
|
| 31 |
+
{ label: '08.45 - 10.35', indices: [0, 1] },
|
| 32 |
+
{ label: '10.50 - 12.40', indices: [3, 4] },
|
| 33 |
+
{ label: '01.40 - 03.30', indices: [6, 7] }
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
let timetableData = {}; // Keys: "row-col-day"
|
| 37 |
+
let mergedCells = {}; // Keys: "row-col" mapped to an array of merged cell keys
|
| 38 |
+
|
| 39 |
+
// ------ Teacher Config Functions (AJAX calls) ------
|
| 40 |
+
function loadTeacherConfig() {
|
| 41 |
+
if (!teacherClass) return;
|
| 42 |
+
getConfig("subject");
|
| 43 |
+
getConfig("specSubject");
|
| 44 |
+
getConfig("batch");
|
| 45 |
+
getConfig("practical");
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function getConfig(type) {
|
| 49 |
+
fetch(`/get_config?class=${teacherClass}&type=${type}`)
|
| 50 |
+
.then(res => res.json())
|
| 51 |
+
.then(data => {
|
| 52 |
+
if (type === "subject") {
|
| 53 |
+
subjectList = data;
|
| 54 |
+
updateDisplay("subjectList", subjectList, "subject");
|
| 55 |
+
} else if (type === "specSubject") {
|
| 56 |
+
specSubjectList = data;
|
| 57 |
+
updateDisplay("specSubjectList", specSubjectList, "specSubject");
|
| 58 |
+
} else if (type === "batch") {
|
| 59 |
+
batchList = data;
|
| 60 |
+
updateDisplay("batchList", batchList);
|
| 61 |
+
} else if (type === "practical") {
|
| 62 |
+
practicalSubjectList = data;
|
| 63 |
+
updateDisplay("practicalSubjectList", practicalSubjectList);
|
| 64 |
+
}
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function addConfig(type) {
|
| 69 |
+
if (!teacherClass) {
|
| 70 |
+
alert("Please select a class first.");
|
| 71 |
+
return;
|
| 72 |
+
}
|
| 73 |
+
let inputId = "";
|
| 74 |
+
if (type === "subject") inputId = "subjectInput";
|
| 75 |
+
else if (type === "specSubject") inputId = "specSubjectInput";
|
| 76 |
+
else if (type === "batch") inputId = "batchInput";
|
| 77 |
+
else if (type === "practical") inputId = "practicalSubjectInput";
|
| 78 |
+
const value = document.getElementById(inputId).value.trim();
|
| 79 |
+
if (!value) return;
|
| 80 |
+
|
| 81 |
+
fetch('/add_config', {
|
| 82 |
+
method: 'POST',
|
| 83 |
+
headers: {'Content-Type': 'application/json'},
|
| 84 |
+
body: JSON.stringify({class: teacherClass, type: type, name: value})
|
| 85 |
+
})
|
| 86 |
+
.then(res => res.json())
|
| 87 |
+
.then(data => {
|
| 88 |
+
loadTeacherConfig();
|
| 89 |
+
document.getElementById(inputId).value = "";
|
| 90 |
+
});
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
function updateDisplay(elementId, arr, type="") {
|
| 94 |
+
const span = document.getElementById(elementId);
|
| 95 |
+
span.innerHTML = "";
|
| 96 |
+
arr.forEach(item => {
|
| 97 |
+
const spanItem = document.createElement("span");
|
| 98 |
+
spanItem.textContent = item;
|
| 99 |
+
const del = document.createElement("span");
|
| 100 |
+
del.textContent = " ✕";
|
| 101 |
+
del.className = "delete-btn";
|
| 102 |
+
del.onclick = () => deleteConfig(type, item);
|
| 103 |
+
spanItem.appendChild(del);
|
| 104 |
+
span.appendChild(spanItem);
|
| 105 |
+
span.appendChild(document.createTextNode(" | "));
|
| 106 |
+
});
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
function deleteConfig(type, name) {
|
| 110 |
+
if (!teacherClass) return;
|
| 111 |
+
fetch('/delete_config', {
|
| 112 |
+
method: 'POST',
|
| 113 |
+
headers: {'Content-Type': 'application/json'},
|
| 114 |
+
body: JSON.stringify({class: teacherClass, type: type, name: name})
|
| 115 |
+
})
|
| 116 |
+
.then(res => res.json())
|
| 117 |
+
.then(data => loadTeacherConfig());
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// When a class is selected, load both configuration and timetable.
|
| 121 |
+
function loadForSelectedClass() {
|
| 122 |
+
teacherClass = document.getElementById("classSelect").value;
|
| 123 |
+
if (!teacherClass) return;
|
| 124 |
+
loadTeacherConfig();
|
| 125 |
+
loadTimetable();
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// ------ Modal & Dropdown Functions ------
|
| 129 |
+
function openModal() {
|
| 130 |
+
if (!teacherClass) {
|
| 131 |
+
alert("Please select a class first.");
|
| 132 |
+
return;
|
| 133 |
+
}
|
| 134 |
+
populateDropdown("subjectSelect", subjectList);
|
| 135 |
+
populateDropdown("specSubjectSelect", specSubjectList);
|
| 136 |
+
populateDropdown("practicalSelect", practicalSubjectList);
|
| 137 |
+
populateDropdown("batchSelect", batchList);
|
| 138 |
+
document.getElementById("timeSlotSelect").innerHTML = "";
|
| 139 |
+
document.getElementById("optionType").value = "";
|
| 140 |
+
hideAllModalOptions();
|
| 141 |
+
document.getElementById("popupModal").style.display = "flex";
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
function closeModal() {
|
| 145 |
+
document.getElementById("popupModal").style.display = "none";
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
function hideAllModalOptions() {
|
| 149 |
+
document.getElementById("subjectDropdown").style.display = "none";
|
| 150 |
+
document.getElementById("specSubjectDropdown").style.display = "none";
|
| 151 |
+
document.getElementById("practicalDropdown").style.display = "none";
|
| 152 |
+
document.getElementById("timeSlotDiv").style.display = "none";
|
| 153 |
+
document.getElementById("submitBtn").style.display = "none";
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function populateDropdown(dropdownId, items) {
|
| 157 |
+
const dropdown = document.getElementById(dropdownId);
|
| 158 |
+
dropdown.innerHTML = '<option value="">--Select--</option>';
|
| 159 |
+
items.forEach(item => {
|
| 160 |
+
const opt = document.createElement("option");
|
| 161 |
+
opt.value = item;
|
| 162 |
+
opt.textContent = item;
|
| 163 |
+
dropdown.appendChild(opt);
|
| 164 |
+
});
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
function optionTypeChanged() {
|
| 168 |
+
hideAllModalOptions();
|
| 169 |
+
const optionType = document.getElementById("optionType").value;
|
| 170 |
+
if (optionType === "subject") {
|
| 171 |
+
document.getElementById("subjectDropdown").style.display = "block";
|
| 172 |
+
populateTimeSlotsDropdown("one-hour");
|
| 173 |
+
} else if (optionType === "specSubject") {
|
| 174 |
+
document.getElementById("specSubjectDropdown").style.display = "block";
|
| 175 |
+
populateTimeSlotsDropdown("one-hour");
|
| 176 |
+
} else if (optionType === "practical") {
|
| 177 |
+
document.getElementById("practicalDropdown").style.display = "block";
|
| 178 |
+
populateTimeSlotsDropdown("two-hour");
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
function populateTimeSlotsDropdown(type) {
|
| 183 |
+
const timeSlotSelect = document.getElementById("timeSlotSelect");
|
| 184 |
+
timeSlotSelect.innerHTML = '<option value="">--Select Time Slot--</option>';
|
| 185 |
+
if (type === "one-hour") {
|
| 186 |
+
timeSlots.forEach((slot, idx) => {
|
| 187 |
+
if (breakSlots.hasOwnProperty(idx)) return;
|
| 188 |
+
const opt = document.createElement("option");
|
| 189 |
+
opt.value = idx;
|
| 190 |
+
opt.textContent = slot;
|
| 191 |
+
timeSlotSelect.appendChild(opt);
|
| 192 |
+
});
|
| 193 |
+
} else if (type === "two-hour") {
|
| 194 |
+
practicalTimeSlots.forEach((slot, idx) => {
|
| 195 |
+
const opt = document.createElement("option");
|
| 196 |
+
opt.value = idx;
|
| 197 |
+
opt.textContent = slot.label;
|
| 198 |
+
timeSlotSelect.appendChild(opt);
|
| 199 |
+
});
|
| 200 |
+
}
|
| 201 |
+
document.getElementById("timeSlotDiv").style.display = "block";
|
| 202 |
+
document.getElementById("submitBtn").style.display = "block";
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// ------ Timetable Functions ------
|
| 206 |
+
function inferMergedCells() {
|
| 207 |
+
mergedCells = {};
|
| 208 |
+
const mergeStarts = [0, 3, 6]; // indices where merges can start
|
| 209 |
+
days.forEach((day, colIdx) => {
|
| 210 |
+
mergeStarts.forEach(startIdx => {
|
| 211 |
+
const mainKey = startIdx + "-" + colIdx + "-" + day;
|
| 212 |
+
const nextKey = (startIdx + 1) + "-" + colIdx + "-" + day;
|
| 213 |
+
if (timetableData[mainKey] && !timetableData[nextKey]) {
|
| 214 |
+
const cellKey = startIdx + "-" + colIdx;
|
| 215 |
+
const nextCellKey = (startIdx + 1) + "-" + colIdx;
|
| 216 |
+
mergedCells[cellKey] = [cellKey, nextCellKey];
|
| 217 |
+
}
|
| 218 |
+
});
|
| 219 |
+
});
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
function generateTable() {
|
| 223 |
+
const table = document.getElementById("timetable");
|
| 224 |
+
table.innerHTML = "";
|
| 225 |
+
|
| 226 |
+
const header = table.insertRow();
|
| 227 |
+
header.insertCell().textContent = "Time";
|
| 228 |
+
days.forEach(day => header.insertCell().textContent = day);
|
| 229 |
+
|
| 230 |
+
timeSlots.forEach((slot, rowIdx) => {
|
| 231 |
+
const row = table.insertRow();
|
| 232 |
+
row.insertCell().textContent = slot;
|
| 233 |
+
|
| 234 |
+
days.forEach((day, colIdx) => {
|
| 235 |
+
const cellKey = rowIdx + "-" + colIdx;
|
| 236 |
+
if (breakSlots.hasOwnProperty(rowIdx)) {
|
| 237 |
+
const cell = row.insertCell();
|
| 238 |
+
cell.innerHTML = breakSlots[rowIdx];
|
| 239 |
+
cell.style.fontWeight = "bold";
|
| 240 |
+
cell.style.backgroundColor = "#f0f0f0";
|
| 241 |
+
} else if (isMergedCell(cellKey)) {
|
| 242 |
+
return;
|
| 243 |
+
} else {
|
| 244 |
+
const cell = row.insertCell();
|
| 245 |
+
const contentKey = cellKey + "-" + day;
|
| 246 |
+
let content = timetableData[contentKey] || "";
|
| 247 |
+
|
| 248 |
+
if (mergedCells[cellKey]) {
|
| 249 |
+
cell.rowSpan = mergedCells[cellKey].length;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
if (content.includes("<br>")) {
|
| 253 |
+
const items = content.split("<br>");
|
| 254 |
+
cell.innerHTML = "";
|
| 255 |
+
items.forEach((batch, i) => {
|
| 256 |
+
const div = document.createElement("div");
|
| 257 |
+
div.innerHTML = batch;
|
| 258 |
+
const del = document.createElement("span");
|
| 259 |
+
del.textContent = " [✕]";
|
| 260 |
+
del.className = "delete-btn";
|
| 261 |
+
del.onclick = () => {
|
| 262 |
+
const filtered = items.filter(it => it !== batch);
|
| 263 |
+
timetableData[contentKey] = filtered.join("<br>");
|
| 264 |
+
if (!timetableData[contentKey]) delete timetableData[contentKey];
|
| 265 |
+
autoSaveTimetable();
|
| 266 |
+
generateTable();
|
| 267 |
+
};
|
| 268 |
+
div.appendChild(del);
|
| 269 |
+
cell.appendChild(div);
|
| 270 |
+
});
|
| 271 |
+
const clearAll = document.createElement("button");
|
| 272 |
+
clearAll.textContent = "Clear all";
|
| 273 |
+
clearAll.onclick = () => {
|
| 274 |
+
delete timetableData[contentKey];
|
| 275 |
+
autoSaveTimetable();
|
| 276 |
+
generateTable();
|
| 277 |
+
};
|
| 278 |
+
cell.appendChild(clearAll);
|
| 279 |
+
} else {
|
| 280 |
+
cell.innerHTML = content;
|
| 281 |
+
if (content) {
|
| 282 |
+
const del = document.createElement("span");
|
| 283 |
+
del.textContent = " [✕]";
|
| 284 |
+
del.className = "delete-btn";
|
| 285 |
+
del.onclick = () => {
|
| 286 |
+
delete timetableData[contentKey];
|
| 287 |
+
autoSaveTimetable();
|
| 288 |
+
generateTable();
|
| 289 |
+
};
|
| 290 |
+
cell.appendChild(del);
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
});
|
| 295 |
+
});
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
function isMergedCell(cellKey) {
|
| 299 |
+
for (const mainKey in mergedCells) {
|
| 300 |
+
if (mergedCells[mainKey].includes(cellKey) && mainKey !== cellKey)
|
| 301 |
+
return true;
|
| 302 |
+
}
|
| 303 |
+
return false;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function submitTimetableEntry() {
|
| 307 |
+
const day = document.getElementById("daySelect").value;
|
| 308 |
+
if (!day) { alert("Please select a day."); return; }
|
| 309 |
+
const optionType = document.getElementById("optionType").value;
|
| 310 |
+
if (!optionType) { alert("Please select an option type."); return; }
|
| 311 |
+
let entry = "";
|
| 312 |
+
if (optionType === "subject") {
|
| 313 |
+
const subj = document.getElementById("subjectSelect").value;
|
| 314 |
+
if (!subj) { alert("Please select a subject."); return; }
|
| 315 |
+
entry = subj;
|
| 316 |
+
const timeIdx = document.getElementById("timeSlotSelect").value;
|
| 317 |
+
if (timeIdx === "") { alert("Please select a time slot."); return; }
|
| 318 |
+
updateTimetableCell(day, parseInt(timeIdx), entry, optionType);
|
| 319 |
+
} else if (optionType === "specSubject") {
|
| 320 |
+
const spec = document.getElementById("specSubjectSelect").value;
|
| 321 |
+
if (!spec) { alert("Please select a specialization subject."); return; }
|
| 322 |
+
entry = spec;
|
| 323 |
+
const timeIdx = document.getElementById("timeSlotSelect").value;
|
| 324 |
+
if (timeIdx === "") { alert("Please select a time slot."); return; }
|
| 325 |
+
updateTimetableCell(day, parseInt(timeIdx), entry, optionType);
|
| 326 |
+
} else if (optionType === "practical") {
|
| 327 |
+
const prac = document.getElementById("practicalSelect").value;
|
| 328 |
+
if (!prac) { alert("Please select a practical subject."); return; }
|
| 329 |
+
const batch = document.getElementById("batchSelect").value;
|
| 330 |
+
if (!batch) { alert("Please select a batch."); return; }
|
| 331 |
+
entry = "Batch " + batch + ": " + prac;
|
| 332 |
+
const slotIdx = document.getElementById("timeSlotSelect").value;
|
| 333 |
+
if (slotIdx === "") { alert("Please select a time slot."); return; }
|
| 334 |
+
const indices = practicalTimeSlots[parseInt(slotIdx)].indices;
|
| 335 |
+
updateTimetableCellPractical(day, indices, entry);
|
| 336 |
+
}
|
| 337 |
+
autoSaveTimetable();
|
| 338 |
+
closeModal();
|
| 339 |
+
generateTable();
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
// For one-hour updates (for specialization subjects, append if same cell).
|
| 343 |
+
function updateTimetableCell(day, timeIndex, content, optionType) {
|
| 344 |
+
const dayIdx = days.indexOf(day);
|
| 345 |
+
const cellKey = timeIndex + "-" + dayIdx;
|
| 346 |
+
const key = cellKey + "-" + day;
|
| 347 |
+
if (timetableData[key] && optionType === "specSubject") {
|
| 348 |
+
if (!timetableData[key].includes(content))
|
| 349 |
+
timetableData[key] += "<br>" + content;
|
| 350 |
+
} else {
|
| 351 |
+
timetableData[key] = content;
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// For practical subject: update two adjacent cells (merged).
|
| 356 |
+
function updateTimetableCellPractical(day, indices, content) {
|
| 357 |
+
const dayIdx = days.indexOf(day);
|
| 358 |
+
const mainCellKey = indices[0] + "-" + dayIdx;
|
| 359 |
+
const key = mainCellKey + "-" + day;
|
| 360 |
+
let current = timetableData[key] || "";
|
| 361 |
+
|
| 362 |
+
const [newBatchLabel, newSubject] = content.split(":").map(x => x.trim());
|
| 363 |
+
|
| 364 |
+
if (current) {
|
| 365 |
+
const entries = current.split("<br>");
|
| 366 |
+
const existingBatches = entries.map(entry => entry.split(":")[0].trim());
|
| 367 |
+
|
| 368 |
+
if (existingBatches.includes(newBatchLabel)) {
|
| 369 |
+
alert(`Cannot add: Batch ${newBatchLabel} already exists in this block.`);
|
| 370 |
+
return;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
timetableData[key] += "<br>" + content;
|
| 374 |
+
} else {
|
| 375 |
+
timetableData[key] = content;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
const mergedGroup = indices.map(idx => idx + "-" + dayIdx);
|
| 379 |
+
mergedCells[mergedGroup[0]] = mergedGroup;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
// ------ Save/Load Timetable Functions ------
|
| 385 |
+
function autoSaveTimetable() {
|
| 386 |
+
saveTimetable();
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
function saveTimetable() {
|
| 390 |
+
const data = {};
|
| 391 |
+
timeSlots.forEach((slot, rowIdx) => {
|
| 392 |
+
data[slot] = {};
|
| 393 |
+
days.forEach((day, colIdx) => {
|
| 394 |
+
const cellKey = rowIdx + "-" + colIdx;
|
| 395 |
+
const key = cellKey + "-" + day;
|
| 396 |
+
if (breakSlots.hasOwnProperty(rowIdx)) {
|
| 397 |
+
data[slot][day] = breakSlots[rowIdx];
|
| 398 |
+
} else {
|
| 399 |
+
data[slot][day] = timetableData[key] || "";
|
| 400 |
+
}
|
| 401 |
+
});
|
| 402 |
+
});
|
| 403 |
+
const payload = {
|
| 404 |
+
class: teacherClass,
|
| 405 |
+
timetable: data
|
| 406 |
+
};
|
| 407 |
+
fetch('/save', {
|
| 408 |
+
method: 'POST',
|
| 409 |
+
headers: {'Content-Type': 'application/json'},
|
| 410 |
+
body: JSON.stringify(payload)
|
| 411 |
+
})
|
| 412 |
+
.then(res => res.json())
|
| 413 |
+
.then(res => alert(res.message));
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
function loadTimetable() {
|
| 417 |
+
if (!teacherClass) return;
|
| 418 |
+
fetch('/load?class=' + teacherClass)
|
| 419 |
+
.then(res => res.json())
|
| 420 |
+
.then(data => {
|
| 421 |
+
timetableData = {};
|
| 422 |
+
for (const slot in data) {
|
| 423 |
+
for (const day in data[slot]) {
|
| 424 |
+
const rowIdx = timeSlots.indexOf(slot);
|
| 425 |
+
const colIdx = days.indexOf(day);
|
| 426 |
+
if (rowIdx >= 0 && colIdx >= 0) {
|
| 427 |
+
const key = rowIdx + "-" + colIdx + "-" + day;
|
| 428 |
+
timetableData[key] = data[slot][day];
|
| 429 |
+
}
|
| 430 |
+
}
|
| 431 |
+
}
|
| 432 |
+
inferMergedCells();
|
| 433 |
+
generateTable();
|
| 434 |
+
});
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
function downloadExcel() {
|
| 438 |
+
if (!teacherClass) {
|
| 439 |
+
alert("Please select a class.");
|
| 440 |
+
return;
|
| 441 |
+
}
|
| 442 |
+
window.location.href = `/download_excel?class=${teacherClass}`;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
window.onload = function() {
|
| 446 |
+
generateTable();
|
| 447 |
+
}
|
static/js/webcam.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function initWebcam(videoId) {
|
| 2 |
+
const video = document.getElementById(videoId);
|
| 3 |
+
navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
|
| 4 |
+
video.srcObject = stream;
|
| 5 |
+
});
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
function captureFrame(frameArray, single = false) {
|
| 9 |
+
return new Promise((resolve) => {
|
| 10 |
+
const video = document.getElementById('video');
|
| 11 |
+
const canvas = document.createElement('canvas');
|
| 12 |
+
canvas.width = video.videoWidth;
|
| 13 |
+
canvas.height = video.videoHeight;
|
| 14 |
+
const ctx = canvas.getContext('2d');
|
| 15 |
+
ctx.drawImage(video, 0, 0);
|
| 16 |
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
| 17 |
+
const isEmpty = !imageData.data.some(channel => channel !== 0);
|
| 18 |
+
if (!isEmpty) {
|
| 19 |
+
const image = canvas.toDataURL('image/jpeg');
|
| 20 |
+
if (!single) frameArray.push(image);
|
| 21 |
+
resolve(image);
|
| 22 |
+
} else resolve(null);
|
| 23 |
+
});
|
| 24 |
+
}
|
static/script.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Global teacher config arrays (populated via AJAX).
|
| 2 |
+
let teacherClass = "";
|
| 3 |
+
let subjectList = [];
|
| 4 |
+
let specSubjectList = [];
|
| 5 |
+
let batchList = [];
|
| 6 |
+
let practicalSubjectList = [];
|
| 7 |
+
|
| 8 |
+
// Timetable parameters.
|
| 9 |
+
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
| 10 |
+
const timeSlots = [
|
| 11 |
+
'08.45 - 09.40', // index 0
|
| 12 |
+
'09.40 - 10.35', // index 1
|
| 13 |
+
'10.35 - 10.50', // index 2 -> Short Break (fixed)
|
| 14 |
+
'10.50 - 11.45', // index 3
|
| 15 |
+
'11.45 - 12.40', // index 4
|
| 16 |
+
'12.40 - 01.40', // index 5 -> Lunch Break (fixed)
|
| 17 |
+
'01.40 - 02.35', // index 6
|
| 18 |
+
'02.35 - 03.30', // index 7
|
| 19 |
+
'03.30 - 03.40', // index 8 -> Short Break (fixed)
|
| 20 |
+
'03.40 - 04.30' // index 9
|
| 21 |
+
];
|
| 22 |
+
// Mapping break indices to fixed texts.
|
| 23 |
+
const breakSlots = {
|
| 24 |
+
2: "Short Break",
|
| 25 |
+
5: "Lunch Break",
|
| 26 |
+
8: "Short Break"
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
// Two-hour practical time slot definitions.
|
| 30 |
+
const practicalTimeSlots = [
|
| 31 |
+
{ label: '08.45 - 10.35', indices: [0, 1] },
|
| 32 |
+
{ label: '10.50 - 12.40', indices: [3, 4] },
|
| 33 |
+
{ label: '01.40 - 03.30', indices: [6, 7] }
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
let timetableData = {}; // Keys: "row-col-day"
|
| 37 |
+
let mergedCells = {}; // Keys: "row-col" mapped to an array of merged cell keys
|
| 38 |
+
|
| 39 |
+
// ------ Teacher Config Functions (AJAX calls) ------
|
| 40 |
+
function loadTeacherConfig() {
|
| 41 |
+
if (!teacherClass) return;
|
| 42 |
+
getConfig("subject");
|
| 43 |
+
getConfig("specSubject");
|
| 44 |
+
getConfig("batch");
|
| 45 |
+
getConfig("practical");
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function getConfig(type) {
|
| 49 |
+
fetch(`/get_config?class=${teacherClass}&type=${type}`)
|
| 50 |
+
.then(res => res.json())
|
| 51 |
+
.then(data => {
|
| 52 |
+
if (type === "subject") {
|
| 53 |
+
subjectList = data;
|
| 54 |
+
updateDisplay("subjectList", subjectList, "subject");
|
| 55 |
+
} else if (type === "specSubject") {
|
| 56 |
+
specSubjectList = data;
|
| 57 |
+
updateDisplay("specSubjectList", specSubjectList, "specSubject");
|
| 58 |
+
} else if (type === "batch") {
|
| 59 |
+
batchList = data;
|
| 60 |
+
updateDisplay("batchList", batchList);
|
| 61 |
+
} else if (type === "practical") {
|
| 62 |
+
practicalSubjectList = data;
|
| 63 |
+
updateDisplay("practicalSubjectList", practicalSubjectList);
|
| 64 |
+
}
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
function addConfig(type) {
|
| 69 |
+
if (!teacherClass) {
|
| 70 |
+
alert("Please select a class first.");
|
| 71 |
+
return;
|
| 72 |
+
}
|
| 73 |
+
let inputId = "";
|
| 74 |
+
if (type === "subject") inputId = "subjectInput";
|
| 75 |
+
else if (type === "specSubject") inputId = "specSubjectInput";
|
| 76 |
+
else if (type === "batch") inputId = "batchInput";
|
| 77 |
+
else if (type === "practical") inputId = "practicalSubjectInput";
|
| 78 |
+
const value = document.getElementById(inputId).value.trim();
|
| 79 |
+
if (!value) return;
|
| 80 |
+
|
| 81 |
+
fetch('/add_config', {
|
| 82 |
+
method: 'POST',
|
| 83 |
+
headers: {'Content-Type': 'application/json'},
|
| 84 |
+
body: JSON.stringify({class: teacherClass, type: type, name: value})
|
| 85 |
+
})
|
| 86 |
+
.then(res => res.json())
|
| 87 |
+
.then(data => {
|
| 88 |
+
loadTeacherConfig();
|
| 89 |
+
document.getElementById(inputId).value = "";
|
| 90 |
+
});
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
function updateDisplay(elementId, arr, type="") {
|
| 94 |
+
const span = document.getElementById(elementId);
|
| 95 |
+
span.innerHTML = "";
|
| 96 |
+
arr.forEach(item => {
|
| 97 |
+
const spanItem = document.createElement("span");
|
| 98 |
+
spanItem.textContent = item;
|
| 99 |
+
const del = document.createElement("span");
|
| 100 |
+
del.textContent = " ✕";
|
| 101 |
+
del.className = "delete-btn";
|
| 102 |
+
del.onclick = () => deleteConfig(type, item);
|
| 103 |
+
spanItem.appendChild(del);
|
| 104 |
+
span.appendChild(spanItem);
|
| 105 |
+
span.appendChild(document.createTextNode(" | "));
|
| 106 |
+
});
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
function deleteConfig(type, name) {
|
| 110 |
+
if (!teacherClass) return;
|
| 111 |
+
fetch('/delete_config', {
|
| 112 |
+
method: 'POST',
|
| 113 |
+
headers: {'Content-Type': 'application/json'},
|
| 114 |
+
body: JSON.stringify({class: teacherClass, type: type, name: name})
|
| 115 |
+
})
|
| 116 |
+
.then(res => res.json())
|
| 117 |
+
.then(data => loadTeacherConfig());
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
// When a class is selected, load both configuration and timetable.
|
| 121 |
+
function loadForSelectedClass() {
|
| 122 |
+
teacherClass = document.getElementById("classSelect").value;
|
| 123 |
+
if (!teacherClass) return;
|
| 124 |
+
loadTeacherConfig();
|
| 125 |
+
loadTimetable();
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// ------ Modal & Dropdown Functions ------
|
| 129 |
+
function openModal() {
|
| 130 |
+
if (!teacherClass) {
|
| 131 |
+
alert("Please select a class first.");
|
| 132 |
+
return;
|
| 133 |
+
}
|
| 134 |
+
populateDropdown("subjectSelect", subjectList);
|
| 135 |
+
populateDropdown("specSubjectSelect", specSubjectList);
|
| 136 |
+
populateDropdown("practicalSelect", practicalSubjectList);
|
| 137 |
+
populateDropdown("batchSelect", batchList);
|
| 138 |
+
document.getElementById("timeSlotSelect").innerHTML = "";
|
| 139 |
+
document.getElementById("optionType").value = "";
|
| 140 |
+
hideAllModalOptions();
|
| 141 |
+
document.getElementById("popupModal").style.display = "flex";
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
function closeModal() {
|
| 145 |
+
document.getElementById("popupModal").style.display = "none";
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
function hideAllModalOptions() {
|
| 149 |
+
document.getElementById("subjectDropdown").style.display = "none";
|
| 150 |
+
document.getElementById("specSubjectDropdown").style.display = "none";
|
| 151 |
+
document.getElementById("practicalDropdown").style.display = "none";
|
| 152 |
+
document.getElementById("timeSlotDiv").style.display = "none";
|
| 153 |
+
document.getElementById("submitBtn").style.display = "none";
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function populateDropdown(dropdownId, items) {
|
| 157 |
+
const dropdown = document.getElementById(dropdownId);
|
| 158 |
+
dropdown.innerHTML = '<option value="">--Select--</option>';
|
| 159 |
+
items.forEach(item => {
|
| 160 |
+
const opt = document.createElement("option");
|
| 161 |
+
opt.value = item;
|
| 162 |
+
opt.textContent = item;
|
| 163 |
+
dropdown.appendChild(opt);
|
| 164 |
+
});
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
function optionTypeChanged() {
|
| 168 |
+
hideAllModalOptions();
|
| 169 |
+
const optionType = document.getElementById("optionType").value;
|
| 170 |
+
if (optionType === "subject") {
|
| 171 |
+
document.getElementById("subjectDropdown").style.display = "block";
|
| 172 |
+
populateTimeSlotsDropdown("one-hour");
|
| 173 |
+
} else if (optionType === "specSubject") {
|
| 174 |
+
document.getElementById("specSubjectDropdown").style.display = "block";
|
| 175 |
+
populateTimeSlotsDropdown("one-hour");
|
| 176 |
+
} else if (optionType === "practical") {
|
| 177 |
+
document.getElementById("practicalDropdown").style.display = "block";
|
| 178 |
+
populateTimeSlotsDropdown("two-hour");
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
function populateTimeSlotsDropdown(type) {
|
| 183 |
+
const timeSlotSelect = document.getElementById("timeSlotSelect");
|
| 184 |
+
timeSlotSelect.innerHTML = '<option value="">--Select Time Slot--</option>';
|
| 185 |
+
if (type === "one-hour") {
|
| 186 |
+
timeSlots.forEach((slot, idx) => {
|
| 187 |
+
if (breakSlots.hasOwnProperty(idx)) return;
|
| 188 |
+
const opt = document.createElement("option");
|
| 189 |
+
opt.value = idx;
|
| 190 |
+
opt.textContent = slot;
|
| 191 |
+
timeSlotSelect.appendChild(opt);
|
| 192 |
+
});
|
| 193 |
+
} else if (type === "two-hour") {
|
| 194 |
+
practicalTimeSlots.forEach((slot, idx) => {
|
| 195 |
+
const opt = document.createElement("option");
|
| 196 |
+
opt.value = idx;
|
| 197 |
+
opt.textContent = slot.label;
|
| 198 |
+
timeSlotSelect.appendChild(opt);
|
| 199 |
+
});
|
| 200 |
+
}
|
| 201 |
+
document.getElementById("timeSlotDiv").style.display = "block";
|
| 202 |
+
document.getElementById("submitBtn").style.display = "block";
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// ------ Timetable Functions ------
|
| 206 |
+
function inferMergedCells() {
|
| 207 |
+
mergedCells = {};
|
| 208 |
+
const mergeStarts = [0, 3, 6]; // indices where merges can start
|
| 209 |
+
days.forEach((day, colIdx) => {
|
| 210 |
+
mergeStarts.forEach(startIdx => {
|
| 211 |
+
const mainKey = startIdx + "-" + colIdx + "-" + day;
|
| 212 |
+
const nextKey = (startIdx + 1) + "-" + colIdx + "-" + day;
|
| 213 |
+
if (timetableData[mainKey] && !timetableData[nextKey]) {
|
| 214 |
+
const cellKey = startIdx + "-" + colIdx;
|
| 215 |
+
const nextCellKey = (startIdx + 1) + "-" + colIdx;
|
| 216 |
+
mergedCells[cellKey] = [cellKey, nextCellKey];
|
| 217 |
+
}
|
| 218 |
+
});
|
| 219 |
+
});
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
function generateTable() {
|
| 223 |
+
const table = document.getElementById("timetable");
|
| 224 |
+
table.innerHTML = "";
|
| 225 |
+
|
| 226 |
+
const header = table.insertRow();
|
| 227 |
+
header.insertCell().textContent = "Time";
|
| 228 |
+
days.forEach(day => header.insertCell().textContent = day);
|
| 229 |
+
|
| 230 |
+
timeSlots.forEach((slot, rowIdx) => {
|
| 231 |
+
const row = table.insertRow();
|
| 232 |
+
row.insertCell().textContent = slot;
|
| 233 |
+
|
| 234 |
+
days.forEach((day, colIdx) => {
|
| 235 |
+
const cellKey = rowIdx + "-" + colIdx;
|
| 236 |
+
if (breakSlots.hasOwnProperty(rowIdx)) {
|
| 237 |
+
const cell = row.insertCell();
|
| 238 |
+
cell.innerHTML = breakSlots[rowIdx];
|
| 239 |
+
cell.style.fontWeight = "bold";
|
| 240 |
+
cell.style.backgroundColor = "#f0f0f0";
|
| 241 |
+
} else if (isMergedCell(cellKey)) {
|
| 242 |
+
return;
|
| 243 |
+
} else {
|
| 244 |
+
const cell = row.insertCell();
|
| 245 |
+
const contentKey = cellKey + "-" + day;
|
| 246 |
+
let content = timetableData[contentKey] || "";
|
| 247 |
+
|
| 248 |
+
if (mergedCells[cellKey]) {
|
| 249 |
+
cell.rowSpan = mergedCells[cellKey].length;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
if (content.includes("<br>")) {
|
| 253 |
+
const items = content.split("<br>");
|
| 254 |
+
cell.innerHTML = "";
|
| 255 |
+
items.forEach((batch, i) => {
|
| 256 |
+
const div = document.createElement("div");
|
| 257 |
+
div.innerHTML = batch;
|
| 258 |
+
const del = document.createElement("span");
|
| 259 |
+
del.textContent = " [✕]";
|
| 260 |
+
del.className = "delete-btn";
|
| 261 |
+
del.onclick = () => {
|
| 262 |
+
const filtered = items.filter(it => it !== batch);
|
| 263 |
+
timetableData[contentKey] = filtered.join("<br>");
|
| 264 |
+
if (!timetableData[contentKey]) delete timetableData[contentKey];
|
| 265 |
+
autoSaveTimetable();
|
| 266 |
+
generateTable();
|
| 267 |
+
};
|
| 268 |
+
div.appendChild(del);
|
| 269 |
+
cell.appendChild(div);
|
| 270 |
+
});
|
| 271 |
+
const clearAll = document.createElement("button");
|
| 272 |
+
clearAll.textContent = "Clear all";
|
| 273 |
+
clearAll.onclick = () => {
|
| 274 |
+
delete timetableData[contentKey];
|
| 275 |
+
autoSaveTimetable();
|
| 276 |
+
generateTable();
|
| 277 |
+
};
|
| 278 |
+
cell.appendChild(clearAll);
|
| 279 |
+
} else {
|
| 280 |
+
cell.innerHTML = content;
|
| 281 |
+
if (content) {
|
| 282 |
+
const del = document.createElement("span");
|
| 283 |
+
del.textContent = " [✕]";
|
| 284 |
+
del.className = "delete-btn";
|
| 285 |
+
del.onclick = () => {
|
| 286 |
+
delete timetableData[contentKey];
|
| 287 |
+
autoSaveTimetable();
|
| 288 |
+
generateTable();
|
| 289 |
+
};
|
| 290 |
+
cell.appendChild(del);
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
});
|
| 295 |
+
});
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
function isMergedCell(cellKey) {
|
| 299 |
+
for (const mainKey in mergedCells) {
|
| 300 |
+
if (mergedCells[mainKey].includes(cellKey) && mainKey !== cellKey)
|
| 301 |
+
return true;
|
| 302 |
+
}
|
| 303 |
+
return false;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function submitTimetableEntry() {
|
| 307 |
+
const day = document.getElementById("daySelect").value;
|
| 308 |
+
if (!day) { alert("Please select a day."); return; }
|
| 309 |
+
const optionType = document.getElementById("optionType").value;
|
| 310 |
+
if (!optionType) { alert("Please select an option type."); return; }
|
| 311 |
+
let entry = "";
|
| 312 |
+
if (optionType === "subject") {
|
| 313 |
+
const subj = document.getElementById("subjectSelect").value;
|
| 314 |
+
if (!subj) { alert("Please select a subject."); return; }
|
| 315 |
+
entry = subj;
|
| 316 |
+
const timeIdx = document.getElementById("timeSlotSelect").value;
|
| 317 |
+
if (timeIdx === "") { alert("Please select a time slot."); return; }
|
| 318 |
+
updateTimetableCell(day, parseInt(timeIdx), entry, optionType);
|
| 319 |
+
} else if (optionType === "specSubject") {
|
| 320 |
+
const spec = document.getElementById("specSubjectSelect").value;
|
| 321 |
+
if (!spec) { alert("Please select a specialization subject."); return; }
|
| 322 |
+
entry = spec;
|
| 323 |
+
const timeIdx = document.getElementById("timeSlotSelect").value;
|
| 324 |
+
if (timeIdx === "") { alert("Please select a time slot."); return; }
|
| 325 |
+
updateTimetableCell(day, parseInt(timeIdx), entry, optionType);
|
| 326 |
+
} else if (optionType === "practical") {
|
| 327 |
+
const prac = document.getElementById("practicalSelect").value;
|
| 328 |
+
if (!prac) { alert("Please select a practical subject."); return; }
|
| 329 |
+
const batch = document.getElementById("batchSelect").value;
|
| 330 |
+
if (!batch) { alert("Please select a batch."); return; }
|
| 331 |
+
entry = "Batch " + batch + ": " + prac;
|
| 332 |
+
const slotIdx = document.getElementById("timeSlotSelect").value;
|
| 333 |
+
if (slotIdx === "") { alert("Please select a time slot."); return; }
|
| 334 |
+
const indices = practicalTimeSlots[parseInt(slotIdx)].indices;
|
| 335 |
+
updateTimetableCellPractical(day, indices, entry);
|
| 336 |
+
}
|
| 337 |
+
autoSaveTimetable();
|
| 338 |
+
closeModal();
|
| 339 |
+
generateTable();
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
// For one-hour updates (for specialization subjects, append if same cell).
|
| 343 |
+
function updateTimetableCell(day, timeIndex, content, optionType) {
|
| 344 |
+
const dayIdx = days.indexOf(day);
|
| 345 |
+
const cellKey = timeIndex + "-" + dayIdx;
|
| 346 |
+
const key = cellKey + "-" + day;
|
| 347 |
+
if (timetableData[key] && optionType === "specSubject") {
|
| 348 |
+
if (!timetableData[key].includes(content))
|
| 349 |
+
timetableData[key] += "<br>" + content;
|
| 350 |
+
} else {
|
| 351 |
+
timetableData[key] = content;
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// For practical subject: update two adjacent cells (merged).
|
| 356 |
+
function updateTimetableCellPractical(day, indices, content) {
|
| 357 |
+
const dayIdx = days.indexOf(day);
|
| 358 |
+
const mainCellKey = indices[0] + "-" + dayIdx;
|
| 359 |
+
const key = mainCellKey + "-" + day;
|
| 360 |
+
let current = timetableData[key] || "";
|
| 361 |
+
|
| 362 |
+
const [newBatchLabel, newSubject] = content.split(":").map(x => x.trim());
|
| 363 |
+
|
| 364 |
+
if (current) {
|
| 365 |
+
const entries = current.split("<br>");
|
| 366 |
+
const existingBatches = entries.map(entry => entry.split(":")[0].trim());
|
| 367 |
+
|
| 368 |
+
if (existingBatches.includes(newBatchLabel)) {
|
| 369 |
+
alert(`Cannot add: Batch ${newBatchLabel} already exists in this block.`);
|
| 370 |
+
return;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
timetableData[key] += "<br>" + content;
|
| 374 |
+
} else {
|
| 375 |
+
timetableData[key] = content;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
const mergedGroup = indices.map(idx => idx + "-" + dayIdx);
|
| 379 |
+
mergedCells[mergedGroup[0]] = mergedGroup;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
// ------ Save/Load Timetable Functions ------
|
| 385 |
+
function autoSaveTimetable() {
|
| 386 |
+
saveTimetable();
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
function saveTimetable() {
|
| 390 |
+
const data = {};
|
| 391 |
+
timeSlots.forEach((slot, rowIdx) => {
|
| 392 |
+
data[slot] = {};
|
| 393 |
+
days.forEach((day, colIdx) => {
|
| 394 |
+
const cellKey = rowIdx + "-" + colIdx;
|
| 395 |
+
const key = cellKey + "-" + day;
|
| 396 |
+
if (breakSlots.hasOwnProperty(rowIdx)) {
|
| 397 |
+
data[slot][day] = breakSlots[rowIdx];
|
| 398 |
+
} else {
|
| 399 |
+
data[slot][day] = timetableData[key] || "";
|
| 400 |
+
}
|
| 401 |
+
});
|
| 402 |
+
});
|
| 403 |
+
const payload = {
|
| 404 |
+
class: teacherClass,
|
| 405 |
+
timetable: data
|
| 406 |
+
};
|
| 407 |
+
fetch('/save', {
|
| 408 |
+
method: 'POST',
|
| 409 |
+
headers: {'Content-Type': 'application/json'},
|
| 410 |
+
body: JSON.stringify(payload)
|
| 411 |
+
})
|
| 412 |
+
.then(res => res.json())
|
| 413 |
+
.then(res => alert(res.message));
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
function loadTimetable() {
|
| 417 |
+
if (!teacherClass) return;
|
| 418 |
+
fetch('/load?class=' + teacherClass)
|
| 419 |
+
.then(res => res.json())
|
| 420 |
+
.then(data => {
|
| 421 |
+
timetableData = {};
|
| 422 |
+
for (const slot in data) {
|
| 423 |
+
for (const day in data[slot]) {
|
| 424 |
+
const rowIdx = timeSlots.indexOf(slot);
|
| 425 |
+
const colIdx = days.indexOf(day);
|
| 426 |
+
if (rowIdx >= 0 && colIdx >= 0) {
|
| 427 |
+
const key = rowIdx + "-" + colIdx + "-" + day;
|
| 428 |
+
timetableData[key] = data[slot][day];
|
| 429 |
+
}
|
| 430 |
+
}
|
| 431 |
+
}
|
| 432 |
+
inferMergedCells();
|
| 433 |
+
generateTable();
|
| 434 |
+
});
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
function downloadExcel() {
|
| 438 |
+
if (!teacherClass) {
|
| 439 |
+
alert("Please select a class.");
|
| 440 |
+
return;
|
| 441 |
+
}
|
| 442 |
+
window.location.href = `/download_excel?class=${teacherClass}`;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
window.onload = function() {
|
| 446 |
+
generateTable();
|
| 447 |
+
}
|
static/script.js.bak
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Global teacher config arrays (populated via AJAX).
|
| 2 |
+
let teacherClass = "";
|
| 3 |
+
let subjectList = [];
|
| 4 |
+
let specSubjectList = [];
|
| 5 |
+
let batchList = [];
|
| 6 |
+
let practicalSubjectList = [];
|
| 7 |
+
|
| 8 |
+
// Timetable parameters.
|
| 9 |
+
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
| 10 |
+
const timeSlots = [
|
| 11 |
+
'08.45 - 09.40','09.40 - 10.35','10.35 - 10.50','10.50 - 11.45',
|
| 12 |
+
'11.45 - 12.40','12.40 - 01.40','01.40 - 02.35',
|
| 13 |
+
'02.35 - 03.30','03.30 - 03.40','03.40 - 04.30'
|
| 14 |
+
];
|
| 15 |
+
// Two-hour practical time slot definitions.
|
| 16 |
+
const practicalTimeSlots = [
|
| 17 |
+
{ label: '08.45 - 10.35', indices: [0, 1] },
|
| 18 |
+
{ label: '10.35 - 12.40', indices: [2, 3] },
|
| 19 |
+
{ label: '12.40 - 01.40', indices: [4, 5] },
|
| 20 |
+
{ label: '01.40 - 03.30', indices: [6, 7] }
|
| 21 |
+
];
|
| 22 |
+
|
| 23 |
+
let timetableData = {}; // Keys: "row-col-day"
|
| 24 |
+
let mergedCells = {}; // Keys: "row-col" mapped to an array of merged cell keys
|
| 25 |
+
|
| 26 |
+
// ------ Teacher Config Functions (AJAX calls) ------
|
| 27 |
+
function loadTeacherConfig() {
|
| 28 |
+
if (!teacherClass) return;
|
| 29 |
+
getConfig("subject");
|
| 30 |
+
getConfig("specSubject");
|
| 31 |
+
getConfig("batch");
|
| 32 |
+
getConfig("practical");
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function getConfig(type) {
|
| 36 |
+
fetch(`/get_config?class=${teacherClass}&type=${type}`)
|
| 37 |
+
.then(res => res.json())
|
| 38 |
+
.then(data => {
|
| 39 |
+
if (type === "subject") {
|
| 40 |
+
subjectList = data;
|
| 41 |
+
updateDisplay("subjectList", subjectList, "subject");
|
| 42 |
+
} else if (type === "specSubject") {
|
| 43 |
+
specSubjectList = data;
|
| 44 |
+
updateDisplay("specSubjectList", specSubjectList, "specSubject");
|
| 45 |
+
} else if (type === "batch") {
|
| 46 |
+
batchList = data;
|
| 47 |
+
updateDisplay("batchList", batchList);
|
| 48 |
+
} else if (type === "practical") {
|
| 49 |
+
practicalSubjectList = data;
|
| 50 |
+
updateDisplay("practicalSubjectList", practicalSubjectList);
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
function addConfig(type) {
|
| 56 |
+
if (!teacherClass) {
|
| 57 |
+
alert("Please select a class first.");
|
| 58 |
+
return;
|
| 59 |
+
}
|
| 60 |
+
let inputId = "";
|
| 61 |
+
if (type === "subject") inputId = "subjectInput";
|
| 62 |
+
else if (type === "specSubject") inputId = "specSubjectInput";
|
| 63 |
+
else if (type === "batch") inputId = "batchInput";
|
| 64 |
+
else if (type === "practical") inputId = "practicalSubjectInput";
|
| 65 |
+
const value = document.getElementById(inputId).value.trim();
|
| 66 |
+
if (!value) return;
|
| 67 |
+
|
| 68 |
+
fetch('/add_config', {
|
| 69 |
+
method: 'POST',
|
| 70 |
+
headers: {'Content-Type': 'application/json'},
|
| 71 |
+
body: JSON.stringify({class: teacherClass, type: type, name: value})
|
| 72 |
+
})
|
| 73 |
+
.then(res => res.json())
|
| 74 |
+
.then(data => {
|
| 75 |
+
loadTeacherConfig();
|
| 76 |
+
document.getElementById(inputId).value = "";
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function updateDisplay(elementId, arr, type="") {
|
| 81 |
+
const span = document.getElementById(elementId);
|
| 82 |
+
span.innerHTML = "";
|
| 83 |
+
arr.forEach(item => {
|
| 84 |
+
const spanItem = document.createElement("span");
|
| 85 |
+
spanItem.textContent = item;
|
| 86 |
+
const del = document.createElement("span");
|
| 87 |
+
del.textContent = " ✕";
|
| 88 |
+
del.className = "delete-btn";
|
| 89 |
+
del.onclick = () => deleteConfig(type, item);
|
| 90 |
+
spanItem.appendChild(del);
|
| 91 |
+
span.appendChild(spanItem);
|
| 92 |
+
span.appendChild(document.createTextNode(" | "));
|
| 93 |
+
});
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
function deleteConfig(type, name) {
|
| 97 |
+
if (!teacherClass) return;
|
| 98 |
+
fetch('/delete_config', {
|
| 99 |
+
method: 'POST',
|
| 100 |
+
headers: {'Content-Type': 'application/json'},
|
| 101 |
+
body: JSON.stringify({class: teacherClass, type: type, name: name})
|
| 102 |
+
})
|
| 103 |
+
.then(res => res.json())
|
| 104 |
+
.then(data => loadTeacherConfig());
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// When a class is selected, load both configuration and timetable.
|
| 108 |
+
function loadForSelectedClass() {
|
| 109 |
+
teacherClass = document.getElementById("classSelect").value;
|
| 110 |
+
if (!teacherClass) return;
|
| 111 |
+
loadTeacherConfig();
|
| 112 |
+
loadTimetable();
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
function openModal() {
|
| 116 |
+
if (!teacherClass) {
|
| 117 |
+
alert("Please select a class first.");
|
| 118 |
+
return;
|
| 119 |
+
}
|
| 120 |
+
populateDropdown("subjectSelect", subjectList);
|
| 121 |
+
populateDropdown("specSubjectSelect", specSubjectList);
|
| 122 |
+
populateDropdown("practicalSelect", practicalSubjectList);
|
| 123 |
+
populateDropdown("batchSelect", batchList);
|
| 124 |
+
document.getElementById("timeSlotSelect").innerHTML = "";
|
| 125 |
+
document.getElementById("optionType").value = "";
|
| 126 |
+
hideAllModalOptions();
|
| 127 |
+
document.getElementById("popupModal").style.display = "flex";
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function closeModal() {
|
| 131 |
+
document.getElementById("popupModal").style.display = "none";
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
function hideAllModalOptions() {
|
| 135 |
+
document.getElementById("subjectDropdown").style.display = "none";
|
| 136 |
+
document.getElementById("specSubjectDropdown").style.display = "none";
|
| 137 |
+
document.getElementById("practicalDropdown").style.display = "none";
|
| 138 |
+
document.getElementById("timeSlotDiv").style.display = "none";
|
| 139 |
+
document.getElementById("submitBtn").style.display = "none";
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
function populateDropdown(dropdownId, items) {
|
| 143 |
+
const dropdown = document.getElementById(dropdownId);
|
| 144 |
+
dropdown.innerHTML = '<option value="">--Select--</option>';
|
| 145 |
+
items.forEach(item => {
|
| 146 |
+
const opt = document.createElement("option");
|
| 147 |
+
opt.value = item;
|
| 148 |
+
opt.textContent = item;
|
| 149 |
+
dropdown.appendChild(opt);
|
| 150 |
+
});
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
function optionTypeChanged() {
|
| 154 |
+
hideAllModalOptions();
|
| 155 |
+
const optionType = document.getElementById("optionType").value;
|
| 156 |
+
if (optionType === "subject") {
|
| 157 |
+
document.getElementById("subjectDropdown").style.display = "block";
|
| 158 |
+
populateTimeSlotsDropdown("one-hour");
|
| 159 |
+
} else if (optionType === "specSubject") {
|
| 160 |
+
document.getElementById("specSubjectDropdown").style.display = "block";
|
| 161 |
+
populateTimeSlotsDropdown("one-hour");
|
| 162 |
+
} else if (optionType === "practical") {
|
| 163 |
+
document.getElementById("practicalDropdown").style.display = "block";
|
| 164 |
+
populateTimeSlotsDropdown("two-hour");
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function populateTimeSlotsDropdown(type) {
|
| 169 |
+
const timeSlotSelect = document.getElementById("timeSlotSelect");
|
| 170 |
+
timeSlotSelect.innerHTML = '<option value="">--Select Time Slot--</option>';
|
| 171 |
+
if (type === "one-hour") {
|
| 172 |
+
timeSlots.forEach((slot, idx) => {
|
| 173 |
+
const opt = document.createElement("option");
|
| 174 |
+
opt.value = idx;
|
| 175 |
+
opt.textContent = slot;
|
| 176 |
+
timeSlotSelect.appendChild(opt);
|
| 177 |
+
});
|
| 178 |
+
} else if (type === "two-hour") {
|
| 179 |
+
practicalTimeSlots.forEach((slot, idx) => {
|
| 180 |
+
const opt = document.createElement("option");
|
| 181 |
+
opt.value = idx;
|
| 182 |
+
opt.textContent = slot.label;
|
| 183 |
+
timeSlotSelect.appendChild(opt);
|
| 184 |
+
});
|
| 185 |
+
}
|
| 186 |
+
document.getElementById("timeSlotDiv").style.display = "block";
|
| 187 |
+
document.getElementById("submitBtn").style.display = "block";
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// ------ Timetable Functions ------
|
| 191 |
+
function generateTable() {
|
| 192 |
+
const table = document.getElementById("timetable");
|
| 193 |
+
table.innerHTML = "";
|
| 194 |
+
// Header row.
|
| 195 |
+
const header = table.insertRow();
|
| 196 |
+
header.insertCell().textContent = "Time";
|
| 197 |
+
days.forEach(day => {
|
| 198 |
+
const th = document.createElement("th");
|
| 199 |
+
th.textContent = day;
|
| 200 |
+
header.appendChild(th);
|
| 201 |
+
});
|
| 202 |
+
// One-hour rows.
|
| 203 |
+
timeSlots.forEach((slot, rowIdx) => {
|
| 204 |
+
const row = table.insertRow();
|
| 205 |
+
const timeCell = row.insertCell();
|
| 206 |
+
timeCell.textContent = slot;
|
| 207 |
+
days.forEach((day, colIdx) => {
|
| 208 |
+
const cellKey = rowIdx + "-" + colIdx;
|
| 209 |
+
if (isMergedCell(cellKey)) return;
|
| 210 |
+
const cell = row.insertCell();
|
| 211 |
+
if (mergedCells[cellKey] && mergedCells[cellKey].length > 1)
|
| 212 |
+
cell.rowSpan = mergedCells[cellKey].length;
|
| 213 |
+
const contentKey = cellKey + "-" + day;
|
| 214 |
+
cell.innerHTML = timetableData[contentKey] || "";
|
| 215 |
+
// Append delete button if cell has content.
|
| 216 |
+
if (timetableData[contentKey]) {
|
| 217 |
+
const del = document.createElement("span");
|
| 218 |
+
del.textContent = " [✕]";
|
| 219 |
+
del.className = "delete-btn";
|
| 220 |
+
del.onclick = () => { deleteTimetableCell(day, rowIdx, colIdx); };
|
| 221 |
+
cell.appendChild(del);
|
| 222 |
+
}
|
| 223 |
+
cell.className = "editable";
|
| 224 |
+
});
|
| 225 |
+
});
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
function isMergedCell(cellKey) {
|
| 229 |
+
for (const mainKey in mergedCells) {
|
| 230 |
+
if (mergedCells[mainKey].includes(cellKey) && mainKey !== cellKey)
|
| 231 |
+
return true;
|
| 232 |
+
}
|
| 233 |
+
return false;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
function deleteTimetableCell(day, rowIdx, colIdx) {
|
| 237 |
+
if (confirm("Delete this entry?")) {
|
| 238 |
+
const cellKey = rowIdx + "-" + colIdx + "-" + day;
|
| 239 |
+
timetableData[cellKey] = "";
|
| 240 |
+
autoSaveTimetable();
|
| 241 |
+
generateTable();
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
function submitTimetableEntry() {
|
| 246 |
+
const day = document.getElementById("daySelect").value;
|
| 247 |
+
if (!day) { alert("Please select a day."); return; }
|
| 248 |
+
const optionType = document.getElementById("optionType").value;
|
| 249 |
+
if (!optionType) { alert("Please select an option type."); return; }
|
| 250 |
+
let entry = "";
|
| 251 |
+
if (optionType === "subject") {
|
| 252 |
+
const subj = document.getElementById("subjectSelect").value;
|
| 253 |
+
if (!subj) { alert("Please select a subject."); return; }
|
| 254 |
+
entry = subj;
|
| 255 |
+
const timeIdx = document.getElementById("timeSlotSelect").value;
|
| 256 |
+
if (timeIdx === "") { alert("Please select a time slot."); return; }
|
| 257 |
+
updateTimetableCell(day, parseInt(timeIdx), entry, optionType);
|
| 258 |
+
} else if (optionType === "specSubject") {
|
| 259 |
+
const spec = document.getElementById("specSubjectSelect").value;
|
| 260 |
+
if (!spec) { alert("Please select a specialization subject."); return; }
|
| 261 |
+
entry = spec;
|
| 262 |
+
const timeIdx = document.getElementById("timeSlotSelect").value;
|
| 263 |
+
if (timeIdx === "") { alert("Please select a time slot."); return; }
|
| 264 |
+
updateTimetableCell(day, parseInt(timeIdx), entry, optionType);
|
| 265 |
+
} else if (optionType === "practical") {
|
| 266 |
+
const prac = document.getElementById("practicalSelect").value;
|
| 267 |
+
if (!prac) { alert("Please select a practical subject."); return; }
|
| 268 |
+
const batch = document.getElementById("batchSelect").value;
|
| 269 |
+
if (!batch) { alert("Please select a batch."); return; }
|
| 270 |
+
entry = "Batch " + batch + ": " + prac;
|
| 271 |
+
const slotIdx = document.getElementById("timeSlotSelect").value;
|
| 272 |
+
if (slotIdx === "") { alert("Please select a time slot."); return; }
|
| 273 |
+
const indices = practicalTimeSlots[parseInt(slotIdx)].indices;
|
| 274 |
+
updateTimetableCellPractical(day, indices, entry);
|
| 275 |
+
}
|
| 276 |
+
autoSaveTimetable();
|
| 277 |
+
closeModal();
|
| 278 |
+
generateTable();
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// For one-hour updates (for specialization subjects, append if same cell).
|
| 282 |
+
function updateTimetableCell(day, timeIndex, content, optionType) {
|
| 283 |
+
const dayIdx = days.indexOf(day);
|
| 284 |
+
const cellKey = timeIndex + "-" + dayIdx;
|
| 285 |
+
const key = cellKey + "-" + day;
|
| 286 |
+
if (timetableData[key] && optionType === "specSubject") {
|
| 287 |
+
if (!timetableData[key].includes(content))
|
| 288 |
+
timetableData[key] += "<br>" + content;
|
| 289 |
+
} else {
|
| 290 |
+
timetableData[key] = content;
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// For practical subject: update two adjacent cells (merged).
|
| 295 |
+
function updateTimetableCellPractical(day, indices, content) {
|
| 296 |
+
const dayIdx = days.indexOf(day);
|
| 297 |
+
const mainCellKey = indices[0] + "-" + dayIdx;
|
| 298 |
+
const key = mainCellKey + "-" + day;
|
| 299 |
+
if (timetableData[key])
|
| 300 |
+
timetableData[key] += "<br>" + content;
|
| 301 |
+
else
|
| 302 |
+
timetableData[key] = content;
|
| 303 |
+
const mergedGroup = indices.map(idx => idx + "-" + dayIdx);
|
| 304 |
+
mergedCells[mergedGroup[0]] = mergedGroup;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// ------ Save/Load Timetable Functions ------
|
| 308 |
+
function autoSaveTimetable() {
|
| 309 |
+
saveTimetable();
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
function saveTimetable() {
|
| 313 |
+
const data = {};
|
| 314 |
+
timeSlots.forEach((slot, rowIdx) => {
|
| 315 |
+
data[slot] = {};
|
| 316 |
+
days.forEach((day, colIdx) => {
|
| 317 |
+
const cellKey = rowIdx + "-" + colIdx;
|
| 318 |
+
const key = cellKey + "-" + day;
|
| 319 |
+
data[slot][day] = timetableData[key] || "";
|
| 320 |
+
});
|
| 321 |
+
});
|
| 322 |
+
const payload = {
|
| 323 |
+
class: teacherClass,
|
| 324 |
+
timetable: data
|
| 325 |
+
};
|
| 326 |
+
fetch('/save', {
|
| 327 |
+
method: 'POST',
|
| 328 |
+
headers: {'Content-Type': 'application/json'},
|
| 329 |
+
body: JSON.stringify(payload)
|
| 330 |
+
})
|
| 331 |
+
.then(res => res.json())
|
| 332 |
+
.then(res => alert(res.message));
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
function loadTimetable() {
|
| 336 |
+
if (!teacherClass) return;
|
| 337 |
+
fetch('/load?class=' + teacherClass)
|
| 338 |
+
.then(res => res.json())
|
| 339 |
+
.then(data => {
|
| 340 |
+
timetableData = {};
|
| 341 |
+
for (const slot in data) {
|
| 342 |
+
for (const day in data[slot]) {
|
| 343 |
+
const rowIdx = timeSlots.indexOf(slot);
|
| 344 |
+
const colIdx = days.indexOf(day);
|
| 345 |
+
if (rowIdx >= 0 && colIdx >= 0) {
|
| 346 |
+
const key = rowIdx + "-" + colIdx + "-" + day;
|
| 347 |
+
timetableData[key] = data[slot][day];
|
| 348 |
+
}
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
mergedCells = {};
|
| 352 |
+
generateTable();
|
| 353 |
+
});
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
window.onload = function() {
|
| 357 |
+
// When the page loads, no class is selected yet.
|
| 358 |
+
// loadTeacherConfig(); and loadTimetable() will be called when a class is selected.
|
| 359 |
+
generateTable();
|
| 360 |
+
}
|
templates/index.html
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
| 6 |
+
<title>MIT Art, Design and Technology University</title>
|
| 7 |
+
|
| 8 |
+
<!-- Bootstrap CSS -->
|
| 9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap.min.css" />
|
| 10 |
+
|
| 11 |
+
<!-- Font Awesome -->
|
| 12 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" />
|
| 13 |
+
|
| 14 |
+
<style>
|
| 15 |
+
/* ====== Reset & Base ====== */
|
| 16 |
+
* {
|
| 17 |
+
box-sizing: border-box;
|
| 18 |
+
margin: 0;
|
| 19 |
+
padding: 0;
|
| 20 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
body {
|
| 24 |
+
background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%);
|
| 25 |
+
min-height: 100vh;
|
| 26 |
+
display: flex;
|
| 27 |
+
flex-direction: column;
|
| 28 |
+
align-items: center;
|
| 29 |
+
padding: 20px;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/* ====== Header ====== */
|
| 33 |
+
header {
|
| 34 |
+
width: 100%;
|
| 35 |
+
background: #fff;
|
| 36 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
| 37 |
+
padding: 15px 0;
|
| 38 |
+
margin-bottom: 40px;
|
| 39 |
+
border-bottom: 3px solid #1a4a7c;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
header .container {
|
| 43 |
+
display: flex;
|
| 44 |
+
align-items: center;
|
| 45 |
+
justify-content: center;
|
| 46 |
+
flex-wrap: wrap;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
header img {
|
| 50 |
+
height: 70px;
|
| 51 |
+
margin-right: 20px;
|
| 52 |
+
border-radius: 5px;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
header h1 {
|
| 56 |
+
font-size: 1.7rem;
|
| 57 |
+
font-weight: 600;
|
| 58 |
+
color: #1a4a7c;
|
| 59 |
+
margin: 0;
|
| 60 |
+
text-align: center;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* ====== Login Card ====== */
|
| 64 |
+
.login-box {
|
| 65 |
+
background-color: #ffffff;
|
| 66 |
+
border-radius: 12px;
|
| 67 |
+
padding: 35px;
|
| 68 |
+
width: 100%;
|
| 69 |
+
max-width: 480px;
|
| 70 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
|
| 71 |
+
margin-bottom: 40px;
|
| 72 |
+
border-top: 4px solid #1a4a7c;
|
| 73 |
+
position: relative;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.login-box:before {
|
| 77 |
+
content: "";
|
| 78 |
+
position: absolute;
|
| 79 |
+
top: 0;
|
| 80 |
+
left: 0;
|
| 81 |
+
right: 0;
|
| 82 |
+
height: 8px;
|
| 83 |
+
background: linear-gradient(90deg, #1a4a7c, #3498db);
|
| 84 |
+
border-radius: 12px 12px 0 0;
|
| 85 |
+
opacity: 0.8;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.login-box h2 {
|
| 89 |
+
color: #1a4a7c;
|
| 90 |
+
margin-bottom: 25px;
|
| 91 |
+
font-weight: 600;
|
| 92 |
+
text-align: center;
|
| 93 |
+
position: relative;
|
| 94 |
+
padding-bottom: 12px;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.login-box h2:after {
|
| 98 |
+
content: "";
|
| 99 |
+
position: absolute;
|
| 100 |
+
bottom: 0;
|
| 101 |
+
left: 50%;
|
| 102 |
+
transform: translateX(-50%);
|
| 103 |
+
width: 80px;
|
| 104 |
+
height: 3px;
|
| 105 |
+
background: linear-gradient(90deg, #1a4a7c, #3498db);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.section-buttons {
|
| 109 |
+
display: flex;
|
| 110 |
+
justify-content: space-between;
|
| 111 |
+
margin-bottom: 30px;
|
| 112 |
+
border-radius: 8px;
|
| 113 |
+
overflow: hidden;
|
| 114 |
+
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.section-buttons button {
|
| 118 |
+
flex: 1;
|
| 119 |
+
padding: 14px 0;
|
| 120 |
+
background-color: #f5f9ff;
|
| 121 |
+
border: none;
|
| 122 |
+
cursor: pointer;
|
| 123 |
+
font-weight: 500;
|
| 124 |
+
color: #555;
|
| 125 |
+
transition: all 0.3s;
|
| 126 |
+
border-right: 1px solid #e0e0e0;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.section-buttons button:last-child {
|
| 130 |
+
border-right: none;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.section-buttons button:hover {
|
| 134 |
+
background-color: #e6f0ff;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.section-buttons button.active {
|
| 138 |
+
background-color: #1a4a7c;
|
| 139 |
+
color: #fff;
|
| 140 |
+
transform: translateY(-2px);
|
| 141 |
+
box-shadow: 0 4px 8px rgba(26, 74, 124, 0.3);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.form-section {
|
| 145 |
+
display: none;
|
| 146 |
+
margin-top: 20px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.form-section h3 {
|
| 150 |
+
color: #1a4a7c;
|
| 151 |
+
margin-bottom: 20px;
|
| 152 |
+
font-weight: 500;
|
| 153 |
+
text-align: center;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.input-group {
|
| 157 |
+
margin-bottom: 20px;
|
| 158 |
+
position: relative;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.input-group label {
|
| 162 |
+
display: block;
|
| 163 |
+
margin-bottom: 8px;
|
| 164 |
+
color: #444;
|
| 165 |
+
font-size: 15px;
|
| 166 |
+
font-weight: 500;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.input-group input {
|
| 170 |
+
width: 100%;
|
| 171 |
+
padding: 14px 15px 14px 40px;
|
| 172 |
+
border: 1px solid #ddd;
|
| 173 |
+
border-radius: 8px;
|
| 174 |
+
font-size: 15px;
|
| 175 |
+
transition: all 0.3s;
|
| 176 |
+
background-color: #f9fbff;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.input-group input:focus {
|
| 180 |
+
border-color: #3498db;
|
| 181 |
+
outline: none;
|
| 182 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
|
| 183 |
+
background-color: #fff;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.input-group i {
|
| 187 |
+
position: absolute;
|
| 188 |
+
left: 15px;
|
| 189 |
+
top: 38px;
|
| 190 |
+
color: #aaa;
|
| 191 |
+
transition: color 0.3s;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.input-group input:focus + i {
|
| 195 |
+
color: #3498db;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.form-section button {
|
| 199 |
+
width: 100%;
|
| 200 |
+
padding: 15px;
|
| 201 |
+
border: none;
|
| 202 |
+
border-radius: 8px;
|
| 203 |
+
cursor: pointer;
|
| 204 |
+
font-size: 16px;
|
| 205 |
+
font-weight: 600;
|
| 206 |
+
color: #fff;
|
| 207 |
+
background: linear-gradient(to right, #1a4a7c, #3498db);
|
| 208 |
+
transition: all 0.3s;
|
| 209 |
+
margin-top: 15px;
|
| 210 |
+
box-shadow: 0 4px 10px rgba(26, 74, 124, 0.3);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.form-section button:hover {
|
| 214 |
+
transform: translateY(-2px);
|
| 215 |
+
box-shadow: 0 6px 15px rgba(26, 74, 124, 0.4);
|
| 216 |
+
background: linear-gradient(to right, #155785, #2980b9);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
/* ====== Modal ====== */
|
| 220 |
+
.modal-overlay {
|
| 221 |
+
position: fixed;
|
| 222 |
+
top: 0; left: 0; right: 0; bottom: 0;
|
| 223 |
+
background-color: rgba(0,0,0,0.6);
|
| 224 |
+
display: none;
|
| 225 |
+
justify-content: center;
|
| 226 |
+
align-items: center;
|
| 227 |
+
z-index: 1000;
|
| 228 |
+
backdrop-filter: blur(3px);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.modal-container {
|
| 232 |
+
background: #fff;
|
| 233 |
+
border-radius: 12px;
|
| 234 |
+
padding: 35px;
|
| 235 |
+
width: 100%;
|
| 236 |
+
max-width: 480px;
|
| 237 |
+
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
|
| 238 |
+
position: relative;
|
| 239 |
+
border-top: 4px solid #1a4a7c;
|
| 240 |
+
animation: modalFadeIn 0.3s;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
@keyframes modalFadeIn {
|
| 244 |
+
from { opacity: 0; transform: translateY(-20px); }
|
| 245 |
+
to { opacity: 1; transform: translateY(0); }
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.close-button {
|
| 249 |
+
position: absolute;
|
| 250 |
+
top: 15px; right: 15px;
|
| 251 |
+
background: #f0f4f8;
|
| 252 |
+
border: none;
|
| 253 |
+
font-size: 20px;
|
| 254 |
+
color: #555;
|
| 255 |
+
cursor: pointer;
|
| 256 |
+
width: 30px;
|
| 257 |
+
height: 30px;
|
| 258 |
+
border-radius: 50%;
|
| 259 |
+
display: flex;
|
| 260 |
+
align-items: center;
|
| 261 |
+
justify-content: center;
|
| 262 |
+
transition: all 0.3s;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.close-button:hover {
|
| 266 |
+
background: #e0e6ed;
|
| 267 |
+
color: #1a4a7c;
|
| 268 |
+
transform: rotate(90deg);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
/* ====== Notes Card ====== */
|
| 272 |
+
.notes-card {
|
| 273 |
+
width: 100%;
|
| 274 |
+
margin-bottom: 20px;
|
| 275 |
+
border: none;
|
| 276 |
+
border-radius: 12px;
|
| 277 |
+
box-shadow: 0 8px 20px rgba(0,0,0,0.1);
|
| 278 |
+
overflow: hidden;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.notes-card .card-header {
|
| 282 |
+
background-color: #1a4a7c;
|
| 283 |
+
color: white;
|
| 284 |
+
padding: 15px 20px;
|
| 285 |
+
font-weight: 600;
|
| 286 |
+
border: none;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.notes-card .card-body {
|
| 290 |
+
padding: 25px;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.notes-card .card-body h5 {
|
| 294 |
+
font-weight: 600;
|
| 295 |
+
margin-top: 15px;
|
| 296 |
+
color: #1a4a7c;
|
| 297 |
+
display: flex;
|
| 298 |
+
align-items: center;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.notes-card .card-body h5 i {
|
| 302 |
+
margin-right: 10px;
|
| 303 |
+
color: #3498db;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.notes-card ul {
|
| 307 |
+
margin-top: 15px;
|
| 308 |
+
padding-left: 20px;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.notes-card ul li {
|
| 312 |
+
margin-bottom: 10px;
|
| 313 |
+
line-height: 1.6;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.notes-card a {
|
| 317 |
+
color: #3498db;
|
| 318 |
+
text-decoration: none;
|
| 319 |
+
transition: color 0.3s;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.notes-card a:hover {
|
| 323 |
+
color: #1a4a7c;
|
| 324 |
+
text-decoration: underline;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/* ====== Responsive Adjustments ====== */
|
| 328 |
+
@media (max-width: 768px) {
|
| 329 |
+
header h1 { font-size: 1.4rem; margin-top: 10px; }
|
| 330 |
+
.login-box { padding: 25px; }
|
| 331 |
+
.section-buttons button { font-size: 14px; padding: 12px 0; }
|
| 332 |
+
.modal-container { padding: 25px; margin: 0 15px; }
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
@media (max-width: 576px) {
|
| 336 |
+
header img { height: 50px; }
|
| 337 |
+
header h1 { font-size: 1.2rem; }
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/* Toast message */
|
| 341 |
+
.toast-container {
|
| 342 |
+
position: fixed;
|
| 343 |
+
bottom: 20px;
|
| 344 |
+
right: 20px;
|
| 345 |
+
z-index: 1001;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.toast {
|
| 349 |
+
background-color: rgba(26, 74, 124, 0.9);
|
| 350 |
+
color: white;
|
| 351 |
+
padding: 15px 25px;
|
| 352 |
+
border-radius: 8px;
|
| 353 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
| 354 |
+
margin-top: 10px;
|
| 355 |
+
display: flex;
|
| 356 |
+
align-items: center;
|
| 357 |
+
animation: toastFadeIn 0.3s;
|
| 358 |
+
backdrop-filter: blur(5px);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.toast i {
|
| 362 |
+
margin-right: 10px;
|
| 363 |
+
font-size: 20px;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
@keyframes toastFadeIn {
|
| 367 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 368 |
+
to { opacity: 1; transform: translateY(0); }
|
| 369 |
+
}
|
| 370 |
+
</style>
|
| 371 |
+
</head>
|
| 372 |
+
|
| 373 |
+
<body>
|
| 374 |
+
<!-- Header -->
|
| 375 |
+
<header>
|
| 376 |
+
<div class="container">
|
| 377 |
+
<img src="https://exam.mitapps.in/site/static/images/mit.jpg" alt="MIT-ADT Logo">
|
| 378 |
+
</div>
|
| 379 |
+
<br>
|
| 380 |
+
<h1>MIT Art, Design and Technology University, Pune</h1>
|
| 381 |
+
</header>
|
| 382 |
+
|
| 383 |
+
<!-- Login Card -->
|
| 384 |
+
<div class="login-box">
|
| 385 |
+
<h2>Welcome to Attendance Portal</h2>
|
| 386 |
+
|
| 387 |
+
<div class="section-buttons">
|
| 388 |
+
<button id="teacherBtn" onclick="showForm('teacher')">
|
| 389 |
+
<i class="fas fa-chalkboard-teacher"></i> Teacher
|
| 390 |
+
</button>
|
| 391 |
+
<button id="studentBtn" onclick="showForm('student')">
|
| 392 |
+
<i class="fas fa-user-graduate"></i> Student
|
| 393 |
+
</button>
|
| 394 |
+
<button id="attendanceBtn" onclick="showForm('attendance')">
|
| 395 |
+
<i class="fas fa-clipboard-check"></i> Attendance
|
| 396 |
+
</button>
|
| 397 |
+
</div>
|
| 398 |
+
|
| 399 |
+
<div id="teacher" class="form-section">
|
| 400 |
+
<h3>Teacher Login</h3>
|
| 401 |
+
<div class="input-group">
|
| 402 |
+
<label for="teacherUsername">Username</label>
|
| 403 |
+
<input type="text" id="teacherUsername" placeholder="Enter your username" />
|
| 404 |
+
<i class="fas fa-user"></i>
|
| 405 |
+
</div>
|
| 406 |
+
<div class="input-group">
|
| 407 |
+
<label for="teacherPassword">Password</label>
|
| 408 |
+
<input type="password" id="teacherPassword" placeholder="Enter your password" />
|
| 409 |
+
<i class="fas fa-lock"></i>
|
| 410 |
+
</div>
|
| 411 |
+
<button onclick="attemptLogin('teacher')">
|
| 412 |
+
<i class="fas fa-sign-in-alt"></i> Login
|
| 413 |
+
</button>
|
| 414 |
+
<div class="link">
|
| 415 |
+
<a href="#" onclick="showSignup('Teacher'); return false;">Don't have an account? Create one</a>
|
| 416 |
+
</div>
|
| 417 |
+
</div>
|
| 418 |
+
|
| 419 |
+
<div id="student" class="form-section">
|
| 420 |
+
<h3>Student Login</h3>
|
| 421 |
+
<div class="input-group">
|
| 422 |
+
<label for="studentUsername">Username</label>
|
| 423 |
+
<input type="text" id="studentUsername" placeholder="Enter your username" />
|
| 424 |
+
<i class="fas fa-user"></i>
|
| 425 |
+
</div>
|
| 426 |
+
<div class="input-group">
|
| 427 |
+
<label for="studentPassword">Password</label>
|
| 428 |
+
<input type="password" id="studentPassword" placeholder="Enter your password" />
|
| 429 |
+
<i class="fas fa-lock"></i>
|
| 430 |
+
</div>
|
| 431 |
+
<button onclick="attemptLogin('student')">
|
| 432 |
+
<i class="fas fa-sign-in-alt"></i> Login
|
| 433 |
+
</button>
|
| 434 |
+
<div class="link">
|
| 435 |
+
<a href="#" onclick="showSignup('Student'); return false;">Don't have an account? Create one</a>
|
| 436 |
+
</div>
|
| 437 |
+
</div>
|
| 438 |
+
|
| 439 |
+
<div id="attendance" class="form-section">
|
| 440 |
+
<h3>Mark Attendance</h3>
|
| 441 |
+
<button onclick="window.location.href='/mark_attendance'">
|
| 442 |
+
<i class="fas fa-clipboard-check"></i> Mark Attendance
|
| 443 |
+
</button>
|
| 444 |
+
</div>
|
| 445 |
+
</div>
|
| 446 |
+
|
| 447 |
+
<!-- Signup Modal -->
|
| 448 |
+
<div class="modal-overlay" id="signupModal">
|
| 449 |
+
<div class="modal-container">
|
| 450 |
+
<button class="close-button" onclick="closeSignup()">×</button>
|
| 451 |
+
<h3 id="signupTitle">Create Account</h3>
|
| 452 |
+
<div class="input-group">
|
| 453 |
+
<label for="signupFullname">Full Name</label>
|
| 454 |
+
<input type="text" id="signupFullname" placeholder="Enter your full name" />
|
| 455 |
+
<i class="fas fa-user"></i>
|
| 456 |
+
</div>
|
| 457 |
+
<!-- Enrollment Number field, hidden by default -->
|
| 458 |
+
<div class="input-group" id="enrollmentGroup" style="display: none;">
|
| 459 |
+
<label for="signupEnrollment">Enrollment Number</label>
|
| 460 |
+
<input type="text" id="signupEnrollment" placeholder="e.g, ADT24SOCBD053" />
|
| 461 |
+
<i class="fas fa-id-card"></i>
|
| 462 |
+
</div>
|
| 463 |
+
<div class="input-group">
|
| 464 |
+
<label for="signupEmail">Email Address</label>
|
| 465 |
+
<input type="email" id="signupEmail" placeholder="Enter your email address" />
|
| 466 |
+
<i class="fas fa-envelope"></i>
|
| 467 |
+
</div>
|
| 468 |
+
<div class="input-group">
|
| 469 |
+
<label for="signupUsername">Username</label>
|
| 470 |
+
<input type="text" id="signupUsername" placeholder="Choose a username" />
|
| 471 |
+
<i class="fas fa-user-circle"></i>
|
| 472 |
+
</div>
|
| 473 |
+
<div class="input-group">
|
| 474 |
+
<label for="signupPassword">Password</label>
|
| 475 |
+
<input type="password" id="signupPassword" placeholder="Choose a password" />
|
| 476 |
+
<i class="fas fa-lock"></i>
|
| 477 |
+
</div>
|
| 478 |
+
<div class="input-group">
|
| 479 |
+
<label for="confirmPassword">Confirm Password</label>
|
| 480 |
+
<input type="password" id="confirmPassword" placeholder="Confirm your password" />
|
| 481 |
+
<i class="fas fa-check-circle"></i>
|
| 482 |
+
</div>
|
| 483 |
+
<button onclick="createAccount()">
|
| 484 |
+
<i class="fas fa-user-plus"></i> Create Account
|
| 485 |
+
</button>
|
| 486 |
+
<div class="link">
|
| 487 |
+
<a href="#" onclick="closeSignup(); return false;">Already have an account? Login</a>
|
| 488 |
+
</div>
|
| 489 |
+
</div>
|
| 490 |
+
</div>
|
| 491 |
+
|
| 492 |
+
<!-- Toast messages container -->
|
| 493 |
+
<div class="toast-container" id="toastContainer"></div>
|
| 494 |
+
|
| 495 |
+
<!-- Important Notes Card -->
|
| 496 |
+
<div class="container">
|
| 497 |
+
<div class="card notes-card">
|
| 498 |
+
<div class="card-header">
|
| 499 |
+
<i class="fas fa-info-circle"></i> Important Information
|
| 500 |
+
</div>
|
| 501 |
+
<div class="card-body">
|
| 502 |
+
<h5><i class="fas fa-exclamation-circle"></i> Important Note:</h5>
|
| 503 |
+
<ul>
|
| 504 |
+
<li>Upload a clear front-facing photo in your student profile to activate Face Recognition Attendance.</li>
|
| 505 |
+
<li>Be present in class on time – attendance is auto-marked using your face.</li>
|
| 506 |
+
<li>Manual attendance is not allowed – ensure your face is visible and unobstructed.</li>
|
| 507 |
+
<li>Minimum 75% attendance (via face recognition) is mandatory for exam eligibility.</li>
|
| 508 |
+
</ul>
|
| 509 |
+
|
| 510 |
+
<h5><i class="fas fa-link"></i> Weblinks:</h5>
|
| 511 |
+
<ul>
|
| 512 |
+
<li><i class="fas fa-external-link-alt"></i> University Website: <a href="https://www.mituniversity.ac.in/" target="_blank">www.mituniversity.ac.in</a></li>
|
| 513 |
+
<li><i class="fas fa-external-link-alt"></i> A B o C: <a href="#" target="_blank">www.abc.gov.in</a> (<a href="#" target="_blank">A I C G</a>)</li>
|
| 514 |
+
<li><i class="fas fa-external-link-alt"></i> D N: <a href="#" target="_blank">ni</a></li>
|
| 515 |
+
<li><i class="fas fa-external-link-alt"></i> A P: <a href="#" target="_blank">ain</a></li>
|
| 516 |
+
</ul>
|
| 517 |
+
</div>
|
| 518 |
+
</div>
|
| 519 |
+
</div>
|
| 520 |
+
|
| 521 |
+
<!-- JS -->
|
| 522 |
+
<script>
|
| 523 |
+
document.addEventListener('DOMContentLoaded', () => showForm('teacher'));
|
| 524 |
+
|
| 525 |
+
function showForm(section) {
|
| 526 |
+
document.querySelectorAll('.form-section').forEach(el => el.style.display = 'none');
|
| 527 |
+
document.querySelectorAll('.section-buttons button').forEach(btn => btn.classList.remove('active'));
|
| 528 |
+
document.getElementById(section).style.display = 'block';
|
| 529 |
+
document.getElementById(section + 'Btn').classList.add('active');
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
function showSignup(role) {
|
| 533 |
+
window.currentSignupRole = role;
|
| 534 |
+
document.getElementById('signupTitle').textContent = role + " Signup";
|
| 535 |
+
const enrollmentGroup = document.getElementById('enrollmentGroup');
|
| 536 |
+
enrollmentGroup.style.display = role === 'Student' ? 'block' : 'none';
|
| 537 |
+
document.getElementById('signupModal').style.display = 'flex';
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
function closeSignup() {
|
| 541 |
+
document.getElementById('signupModal').style.display = 'none';
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
function createAccount() {
|
| 545 |
+
const fullname = document.getElementById('signupFullname').value;
|
| 546 |
+
const email = document.getElementById('signupEmail').value;
|
| 547 |
+
const username = document.getElementById('signupUsername').value;
|
| 548 |
+
const password = document.getElementById('signupPassword').value;
|
| 549 |
+
const confirmPassword = document.getElementById('confirmPassword').value;
|
| 550 |
+
const role = window.currentSignupRole.toLowerCase();
|
| 551 |
+
let enrollment_number = null;
|
| 552 |
+
|
| 553 |
+
if (role === 'student') {
|
| 554 |
+
enrollment_number = document.getElementById('signupEnrollment').value;
|
| 555 |
+
if (!enrollment_number) {
|
| 556 |
+
showToast('Please enter your enrollment number', 'error');
|
| 557 |
+
return;
|
| 558 |
+
}
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
if (!fullname || !email || !username || !password || !confirmPassword) {
|
| 562 |
+
showToast('Please fill all fields', 'error');
|
| 563 |
+
return;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
if (password !== confirmPassword) {
|
| 567 |
+
showToast('Passwords do not match!', 'error');
|
| 568 |
+
return;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
const data = {
|
| 572 |
+
full_name: fullname,
|
| 573 |
+
email: email,
|
| 574 |
+
username: username,
|
| 575 |
+
password: password,
|
| 576 |
+
role: role,
|
| 577 |
+
enrollment_number: enrollment_number
|
| 578 |
+
};
|
| 579 |
+
|
| 580 |
+
fetch('/signup', {
|
| 581 |
+
method: 'POST',
|
| 582 |
+
headers: { 'Content-Type': 'application/json' },
|
| 583 |
+
body: JSON.stringify(data),
|
| 584 |
+
})
|
| 585 |
+
.then(response => response.json())
|
| 586 |
+
.then(data => {
|
| 587 |
+
if (data.success) {
|
| 588 |
+
showToast('Account created successfully!', 'success');
|
| 589 |
+
setTimeout(() => closeSignup(), 1500);
|
| 590 |
+
} else {
|
| 591 |
+
showToast(data.message, 'error');
|
| 592 |
+
}
|
| 593 |
+
})
|
| 594 |
+
.catch(() => showToast('An error occurred. Please try again.', 'error'));
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
function attemptLogin(type) {
|
| 598 |
+
const username = document.getElementById(type + 'Username').value;
|
| 599 |
+
const password = document.getElementById(type + 'Password').value;
|
| 600 |
+
|
| 601 |
+
if (!username || !password) {
|
| 602 |
+
showToast('Please enter both username and password', 'error');
|
| 603 |
+
return;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
const data = { username, password, role: type };
|
| 607 |
+
|
| 608 |
+
fetch('/login', {
|
| 609 |
+
method: 'POST',
|
| 610 |
+
headers: { 'Content-Type': 'application/json' },
|
| 611 |
+
body: JSON.stringify(data),
|
| 612 |
+
})
|
| 613 |
+
.then(response => response.json())
|
| 614 |
+
.then(data => {
|
| 615 |
+
if (data.success) {
|
| 616 |
+
showToast('Login successful! Redirecting...', 'success');
|
| 617 |
+
setTimeout(() => window.location.href = data.redirect, 1500);
|
| 618 |
+
} else {
|
| 619 |
+
showToast(data.message, 'error');
|
| 620 |
+
}
|
| 621 |
+
})
|
| 622 |
+
.catch(() => showToast('An error occurred. Please try again.', 'error'));
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
function markAttendance() {
|
| 626 |
+
showToast('Attendance marked successfully!', 'success');
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
function showToast(message, type = 'info') {
|
| 630 |
+
const toastContainer = document.getElementById('toastContainer');
|
| 631 |
+
const toast = document.createElement('div');
|
| 632 |
+
toast.className = 'toast';
|
| 633 |
+
let icon = type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-triangle' : 'info-circle';
|
| 634 |
+
toast.innerHTML = `<i class="fas fa-${icon}"></i> ${message}`;
|
| 635 |
+
toastContainer.appendChild(toast);
|
| 636 |
+
setTimeout(() => {
|
| 637 |
+
toast.style.opacity = '0';
|
| 638 |
+
toast.style.transform = 'translateY(20px)';
|
| 639 |
+
toast.style.transition = 'all 0.3s';
|
| 640 |
+
setTimeout(() => toastContainer.removeChild(toast), 300);
|
| 641 |
+
}, 8000);
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
window.onclick = function(e) {
|
| 645 |
+
if (e.target === document.getElementById('signupModal')) closeSignup();
|
| 646 |
+
}
|
| 647 |
+
</script>
|
| 648 |
+
|
| 649 |
+
<!-- Bootstrap JS -->
|
| 650 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
| 651 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.1/umd/popper.min.js"></script>
|
| 652 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/js/bootstrap.min.js"></script>
|
| 653 |
+
</body>
|
| 654 |
+
</html>
|
templates/mark.html
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Mark Attendance</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
background-color: #f5f5f5;
|
| 13 |
+
color: #333;
|
| 14 |
+
line-height: 1.6;
|
| 15 |
+
}
|
| 16 |
+
.container {
|
| 17 |
+
width: 80%;
|
| 18 |
+
max-width: 800px;
|
| 19 |
+
margin: 30px auto;
|
| 20 |
+
padding: 30px;
|
| 21 |
+
background-color: #fff;
|
| 22 |
+
border-radius: 8px;
|
| 23 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
| 24 |
+
text-align: center;
|
| 25 |
+
}
|
| 26 |
+
h2 {
|
| 27 |
+
color: #2c3e50;
|
| 28 |
+
margin-bottom: 25px;
|
| 29 |
+
font-size: 28px;
|
| 30 |
+
}
|
| 31 |
+
.btn {
|
| 32 |
+
display: inline-block;
|
| 33 |
+
padding: 12px 24px;
|
| 34 |
+
background-color: #27ae60;
|
| 35 |
+
color: white;
|
| 36 |
+
text-decoration: none;
|
| 37 |
+
border-radius: 5px;
|
| 38 |
+
font-weight: 500;
|
| 39 |
+
transition: background-color 0.3s;
|
| 40 |
+
border: none;
|
| 41 |
+
font-size: 16px;
|
| 42 |
+
cursor: pointer;
|
| 43 |
+
margin: 15px 0;
|
| 44 |
+
}
|
| 45 |
+
.btn:hover {
|
| 46 |
+
background-color: #219653;
|
| 47 |
+
}
|
| 48 |
+
video {
|
| 49 |
+
margin: 20px auto;
|
| 50 |
+
border-radius: 8px;
|
| 51 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 52 |
+
background-color: #eee;
|
| 53 |
+
max-width: 100%;
|
| 54 |
+
}
|
| 55 |
+
#status {
|
| 56 |
+
margin-top: 20px;
|
| 57 |
+
font-size: 18px;
|
| 58 |
+
padding: 15px;
|
| 59 |
+
border-radius: 5px;
|
| 60 |
+
background-color: #f8f9fa;
|
| 61 |
+
display: inline-block;
|
| 62 |
+
min-width: 60%;
|
| 63 |
+
}
|
| 64 |
+
.success {
|
| 65 |
+
color: #27ae60;
|
| 66 |
+
font-weight: 600;
|
| 67 |
+
}
|
| 68 |
+
.error {
|
| 69 |
+
color: #e74c3c;
|
| 70 |
+
font-weight: 600;
|
| 71 |
+
}
|
| 72 |
+
.header {
|
| 73 |
+
display: flex;
|
| 74 |
+
justify-content: space-between;
|
| 75 |
+
align-items: center;
|
| 76 |
+
margin-bottom: 20px;
|
| 77 |
+
}
|
| 78 |
+
.back-link {
|
| 79 |
+
color: #3498db;
|
| 80 |
+
text-decoration: none;
|
| 81 |
+
display: flex;
|
| 82 |
+
align-items: center;
|
| 83 |
+
font-weight: 500;
|
| 84 |
+
}
|
| 85 |
+
.back-link:hover {
|
| 86 |
+
text-decoration: underline;
|
| 87 |
+
}
|
| 88 |
+
.video-container {
|
| 89 |
+
position: relative;
|
| 90 |
+
margin: 0 auto;
|
| 91 |
+
width: 320px;
|
| 92 |
+
max-width: 100%;
|
| 93 |
+
}
|
| 94 |
+
.camera-overlay {
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: 0;
|
| 97 |
+
left: 0;
|
| 98 |
+
right: 0;
|
| 99 |
+
bottom: 0;
|
| 100 |
+
border: 2px solid rgba(52, 152, 219, 0.5);
|
| 101 |
+
border-radius: 8px;
|
| 102 |
+
pointer-events: none;
|
| 103 |
+
}
|
| 104 |
+
.loading {
|
| 105 |
+
display: none;
|
| 106 |
+
margin: 20px auto;
|
| 107 |
+
border: 5px solid #f3f3f3;
|
| 108 |
+
border-radius: 50%;
|
| 109 |
+
border-top: 5px solid #3498db;
|
| 110 |
+
width: 40px;
|
| 111 |
+
height: 40px;
|
| 112 |
+
animation: spin 1s linear infinite;
|
| 113 |
+
}
|
| 114 |
+
@keyframes spin {
|
| 115 |
+
0% { transform: rotate(0deg); }
|
| 116 |
+
100% { transform: rotate(360deg); }
|
| 117 |
+
}
|
| 118 |
+
</style>
|
| 119 |
+
</head>
|
| 120 |
+
<body>
|
| 121 |
+
<div class="container">
|
| 122 |
+
<div class="header">
|
| 123 |
+
<a href="/" class="back-link">
|
| 124 |
+
<i class="fas fa-arrow-left"></i> Back to Home
|
| 125 |
+
</a>
|
| 126 |
+
<h2>Mark Attendance</h2>
|
| 127 |
+
</div>
|
| 128 |
+
<select id="class-select-mark">
|
| 129 |
+
<option value="">Select Class</option>
|
| 130 |
+
<option value="SY-19">SY-19</option>
|
| 131 |
+
<option value="SY-20">SY-20</option>
|
| 132 |
+
<option value="SY-21">SY-21</option>
|
| 133 |
+
<option value="SY-22">SY-22</option>
|
| 134 |
+
</select>
|
| 135 |
+
<div class="video-container">
|
| 136 |
+
<video id="video" width="300" autoplay></video>
|
| 137 |
+
<div class="camera-overlay"></div>
|
| 138 |
+
</div>
|
| 139 |
+
<button onclick="scanFace()" class="btn">
|
| 140 |
+
<i class="fas fa-user-check"></i> Scan Face
|
| 141 |
+
</button>
|
| 142 |
+
<div id="result"></div>
|
| 143 |
+
<p id="status">Select class and click "Scan Face"</p>
|
| 144 |
+
</div>
|
| 145 |
+
|
| 146 |
+
<script src="/static/js/webcam.js"></script>
|
| 147 |
+
<script>
|
| 148 |
+
function scanFace() {
|
| 149 |
+
const className = document.getElementById('class-select-mark').value;
|
| 150 |
+
if (!className) return alert('Please select a class');
|
| 151 |
+
document.getElementById('status').innerText = 'Scanning...';
|
| 152 |
+
captureFrame([], true).then(image => {
|
| 153 |
+
fetch('/api/identify_face', {
|
| 154 |
+
method: 'POST',
|
| 155 |
+
headers: {'Content-Type': 'application/json'},
|
| 156 |
+
body: JSON.stringify({class_name: className, image: image})
|
| 157 |
+
})
|
| 158 |
+
.then(res => res.json())
|
| 159 |
+
.then(data => {
|
| 160 |
+
const result = document.getElementById('result');
|
| 161 |
+
if (data.status === 'success') {
|
| 162 |
+
result.innerHTML = `
|
| 163 |
+
<p>Is this you? <strong>${data.name}</strong> (${data.enrollment})</p>
|
| 164 |
+
<button id="checkin-btn" class="btn">Check In</button>
|
| 165 |
+
<button id="checkout-btn" class="btn">Check Out</button>
|
| 166 |
+
<button id="rescan-btn" class="btn">Rescan</button>
|
| 167 |
+
`;
|
| 168 |
+
document.getElementById('status').innerText = '';
|
| 169 |
+
document.getElementById('checkin-btn').onclick = () => handleAction('checkin', className, data.enrollment, data.name);
|
| 170 |
+
document.getElementById('checkout-btn').onclick = () => handleAction('checkout', className, data.enrollment, data.name);
|
| 171 |
+
document.getElementById('rescan-btn').onclick = () => { result.innerHTML = ''; document.getElementById('status').innerText = 'Select class and click "Scan Face"'; };
|
| 172 |
+
} else {
|
| 173 |
+
result.innerHTML = `<span class="error">${data.message}</span><br><button id="rescan-btn" class="btn">Rescan</button>`;
|
| 174 |
+
document.getElementById('rescan-btn').onclick = () => { result.innerHTML = ''; document.getElementById('status').innerText = 'Select class and click "Scan Face"'; };
|
| 175 |
+
}
|
| 176 |
+
});
|
| 177 |
+
});
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
function handleAction(action, className, enrollment, name) {
|
| 181 |
+
fetch(`/api/${action}`, {
|
| 182 |
+
method: 'POST',
|
| 183 |
+
headers: {'Content-Type': 'application/json'},
|
| 184 |
+
body: JSON.stringify({class_name: className, enrollment: enrollment, name: name})
|
| 185 |
+
})
|
| 186 |
+
.then(res => res.json())
|
| 187 |
+
.then(data => {
|
| 188 |
+
alert(data.message);
|
| 189 |
+
document.getElementById('result').innerHTML = '';
|
| 190 |
+
document.getElementById('status').innerText = 'Select class and click "Scan Face"';
|
| 191 |
+
});
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
initWebcam('video');
|
| 195 |
+
</script>
|
| 196 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js"></script>
|
| 197 |
+
</body>
|
| 198 |
+
</html>
|
templates/register.html
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Register Face</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
background-color: #f5f5f5;
|
| 13 |
+
color: #333;
|
| 14 |
+
line-height: 1.6;
|
| 15 |
+
}
|
| 16 |
+
.container {
|
| 17 |
+
width: 80%;
|
| 18 |
+
max-width: 800px;
|
| 19 |
+
margin: 30px auto;
|
| 20 |
+
padding: 30px;
|
| 21 |
+
background-color: #fff;
|
| 22 |
+
border-radius: 8px;
|
| 23 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
| 24 |
+
text-align: center;
|
| 25 |
+
}
|
| 26 |
+
h2 {
|
| 27 |
+
color: #2c3e50;
|
| 28 |
+
margin-bottom: 25px;
|
| 29 |
+
font-size: 28px;
|
| 30 |
+
}
|
| 31 |
+
.btn {
|
| 32 |
+
display: inline-block;
|
| 33 |
+
padding: 12px 24px;
|
| 34 |
+
background-color: #3498db;
|
| 35 |
+
color: white;
|
| 36 |
+
text-decoration: none;
|
| 37 |
+
border-radius: 5px;
|
| 38 |
+
font-weight: 500;
|
| 39 |
+
transition: background-color 0.3s;
|
| 40 |
+
border: none;
|
| 41 |
+
font-size: 16px;
|
| 42 |
+
cursor: pointer;
|
| 43 |
+
margin-top: 15px;
|
| 44 |
+
}
|
| 45 |
+
.btn:hover {
|
| 46 |
+
background-color: #2980b9;
|
| 47 |
+
}
|
| 48 |
+
input[type="text"] {
|
| 49 |
+
width: 100%;
|
| 50 |
+
padding: 12px;
|
| 51 |
+
margin: 10px 0;
|
| 52 |
+
border: 1px solid #ddd;
|
| 53 |
+
border-radius: 4px;
|
| 54 |
+
box-sizing: border-box;
|
| 55 |
+
font-size: 16px;
|
| 56 |
+
}
|
| 57 |
+
video {
|
| 58 |
+
margin: 20px auto;
|
| 59 |
+
border-radius: 8px;
|
| 60 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
| 61 |
+
background-color: #eee;
|
| 62 |
+
max-width: 100%;
|
| 63 |
+
}
|
| 64 |
+
#status {
|
| 65 |
+
margin-top: 15px;
|
| 66 |
+
font-weight: 500;
|
| 67 |
+
color: #2c3e50;
|
| 68 |
+
}
|
| 69 |
+
.header {
|
| 70 |
+
display: flex;
|
| 71 |
+
justify-content: space-between;
|
| 72 |
+
align-items: center;
|
| 73 |
+
margin-bottom: 20px;
|
| 74 |
+
}
|
| 75 |
+
.back-link {
|
| 76 |
+
color: #3498db;
|
| 77 |
+
text-decoration: none;
|
| 78 |
+
display: flex;
|
| 79 |
+
align-items: center;
|
| 80 |
+
font-weight: 500;
|
| 81 |
+
}
|
| 82 |
+
.back-link:hover {
|
| 83 |
+
text-decoration: underline;
|
| 84 |
+
}
|
| 85 |
+
.form-container {
|
| 86 |
+
margin-top: 20px;
|
| 87 |
+
}
|
| 88 |
+
.progress {
|
| 89 |
+
margin-top: 10px;
|
| 90 |
+
height: 10px;
|
| 91 |
+
background-color: #eee;
|
| 92 |
+
border-radius: 5px;
|
| 93 |
+
overflow: hidden;
|
| 94 |
+
position: relative;
|
| 95 |
+
margin-bottom: 15px;
|
| 96 |
+
}
|
| 97 |
+
.progress-bar {
|
| 98 |
+
height: 100%;
|
| 99 |
+
background-color: #3498db;
|
| 100 |
+
width: 0%;
|
| 101 |
+
transition: width 0.5s ease;
|
| 102 |
+
}
|
| 103 |
+
</style>
|
| 104 |
+
</head>
|
| 105 |
+
<body>
|
| 106 |
+
<div class="container">
|
| 107 |
+
<div class="header">
|
| 108 |
+
<a href="/" class="back-link">
|
| 109 |
+
<i class="fas fa-arrow-left"></i> Back to Home
|
| 110 |
+
</a>
|
| 111 |
+
<h2>Register Face</h2>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="form-container">
|
| 114 |
+
<select id="class-select" required>
|
| 115 |
+
<option value="">Select Class</option>
|
| 116 |
+
<option value="SY-19">SY-19</option>
|
| 117 |
+
<option value="SY-20">SY-20</option>
|
| 118 |
+
<option value="SY-21">SY-21</option>
|
| 119 |
+
<option value="SY-22">SY-22</option>
|
| 120 |
+
</select>
|
| 121 |
+
<input type="text" id="name" placeholder="Enter your name" required>
|
| 122 |
+
<input type="text" id="enrollment" placeholder="Enter Enrollment Number" required>
|
| 123 |
+
<button onclick="startCapture()" class="btn">
|
| 124 |
+
<i class="fas fa-camera"></i> Start Capture
|
| 125 |
+
</button>
|
| 126 |
+
<div class="video-container">
|
| 127 |
+
<video id="video" width="300" autoplay></video>
|
| 128 |
+
</div>
|
| 129 |
+
<div class="progress">
|
| 130 |
+
<div class="progress-bar" id="progress-bar"></div>
|
| 131 |
+
</div>
|
| 132 |
+
<p id="status">Select class, enter details, and click "Start Capture"</p>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
<script src="/static/js/webcam.js"></script>
|
| 136 |
+
<script>
|
| 137 |
+
let frames = [];
|
| 138 |
+
let interval;
|
| 139 |
+
const TOTAL_FRAMES = 120;
|
| 140 |
+
function startCapture() {
|
| 141 |
+
const className = document.getElementById('class-select').value;
|
| 142 |
+
const name = document.getElementById('name').value.trim();
|
| 143 |
+
const enrollment = document.getElementById('enrollment').value.trim();
|
| 144 |
+
if (!className) return alert('Please select a class');
|
| 145 |
+
if (!name) return alert('Please enter your name');
|
| 146 |
+
if (!enrollment) return alert('Please enter enrollment number');
|
| 147 |
+
frames = [];
|
| 148 |
+
document.getElementById('status').innerHTML = 'Capturing...<br><small>Slowly move your head</small>';
|
| 149 |
+
document.getElementById('progress-bar').style.width = '0%';
|
| 150 |
+
interval = setInterval(() => {
|
| 151 |
+
captureFrame(frames);
|
| 152 |
+
const progress = (frames.length / TOTAL_FRAMES) * 100;
|
| 153 |
+
document.getElementById('progress-bar').style.width = progress + '%';
|
| 154 |
+
if (frames.length >= TOTAL_FRAMES) {
|
| 155 |
+
clearInterval(interval);
|
| 156 |
+
document.getElementById('status').innerHTML = 'Processing...';
|
| 157 |
+
fetch('/api/register_face', {
|
| 158 |
+
method: 'POST',
|
| 159 |
+
headers: {'Content-Type': 'application/json'},
|
| 160 |
+
body: JSON.stringify({
|
| 161 |
+
class_name: className,
|
| 162 |
+
name: name,
|
| 163 |
+
enrollment: enrollment,
|
| 164 |
+
images: frames
|
| 165 |
+
})
|
| 166 |
+
})
|
| 167 |
+
.then(res => res.json())
|
| 168 |
+
.then(data => {
|
| 169 |
+
let msg = data.message;
|
| 170 |
+
if (data.status === 'success') {
|
| 171 |
+
msg += '. Please maintain consistent lighting.';
|
| 172 |
+
}
|
| 173 |
+
document.getElementById('status').innerHTML = msg;
|
| 174 |
+
});
|
| 175 |
+
}
|
| 176 |
+
}, 1000);
|
| 177 |
+
}
|
| 178 |
+
initWebcam('video');
|
| 179 |
+
</script>
|
| 180 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js"></script>
|
| 181 |
+
</body>
|
| 182 |
+
</html>
|
templates/student_dashboard.html
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Student Dashboard</title>
|
| 7 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap.min.css">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
body {
|
| 11 |
+
background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%);
|
| 12 |
+
min-height: 100vh;
|
| 13 |
+
display: flex;
|
| 14 |
+
justify-content: center;
|
| 15 |
+
align-items: center;
|
| 16 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 17 |
+
}
|
| 18 |
+
.dashboard-box {
|
| 19 |
+
background-color: #ffffff;
|
| 20 |
+
border-radius: 12px;
|
| 21 |
+
padding: 35px;
|
| 22 |
+
width: 100%;
|
| 23 |
+
max-width: 480px;
|
| 24 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
|
| 25 |
+
border-top: 4px solid #1a4a7c;
|
| 26 |
+
}
|
| 27 |
+
h1 {
|
| 28 |
+
color: #1a4a7c;
|
| 29 |
+
margin-bottom: 25px;
|
| 30 |
+
text-align: center;
|
| 31 |
+
}
|
| 32 |
+
p {
|
| 33 |
+
font-size: 16px;
|
| 34 |
+
color: #444;
|
| 35 |
+
margin-bottom: 15px;
|
| 36 |
+
}
|
| 37 |
+
.btn-primary {
|
| 38 |
+
background: linear-gradient(to right, #1a4a7c, #3498db);
|
| 39 |
+
border: none;
|
| 40 |
+
width: 100%;
|
| 41 |
+
padding: 15px;
|
| 42 |
+
margin-top: 20px;
|
| 43 |
+
}
|
| 44 |
+
.btn-primary:hover {
|
| 45 |
+
background: linear-gradient(to right, #155785, #2980b9);
|
| 46 |
+
}
|
| 47 |
+
.btn-secondary {
|
| 48 |
+
width: 100%;
|
| 49 |
+
padding: 15px;
|
| 50 |
+
margin-top: 10px;
|
| 51 |
+
}
|
| 52 |
+
</style>
|
| 53 |
+
</head>
|
| 54 |
+
<body>
|
| 55 |
+
<div class="dashboard-box">
|
| 56 |
+
<h1>Welcome, {{ user.full_name }}</h1>
|
| 57 |
+
<p>Enrollment Number: {{ user.enrollment_number }}</p>
|
| 58 |
+
<p>Email: {{ user.email }}</p>
|
| 59 |
+
<button class="btn btn-primary" onclick="window.location.href='/register_face'">Register Face</button>
|
| 60 |
+
<a href="/logout" class="btn btn-secondary">Logout</a>
|
| 61 |
+
</div>
|
| 62 |
+
</body>
|
| 63 |
+
</html>
|
templates/teacher_dashboard.html
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Teacher Dashboard</title>
|
| 7 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap.min.css">
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
body {
|
| 11 |
+
background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%);
|
| 12 |
+
min-height: 100vh;
|
| 13 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 14 |
+
padding: 20px;
|
| 15 |
+
}
|
| 16 |
+
.dashboard-box {
|
| 17 |
+
background-color: #ffffff;
|
| 18 |
+
border-radius: 12px;
|
| 19 |
+
padding: 35px;
|
| 20 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
|
| 21 |
+
border-top: 4px solid #1a4a7c;
|
| 22 |
+
margin-bottom: 20px;
|
| 23 |
+
}
|
| 24 |
+
h1 {
|
| 25 |
+
color: #1a4a7c;
|
| 26 |
+
margin-bottom: 25px;
|
| 27 |
+
text-align: center;
|
| 28 |
+
}
|
| 29 |
+
p {
|
| 30 |
+
font-size: 16px;
|
| 31 |
+
color: #444;
|
| 32 |
+
margin-bottom: 15px;
|
| 33 |
+
}
|
| 34 |
+
.btn-primary {
|
| 35 |
+
background: linear-gradient(to right, #1a4a7c, #3498db);
|
| 36 |
+
border: none;
|
| 37 |
+
width: 100%;
|
| 38 |
+
padding: 15px;
|
| 39 |
+
margin-top: 20px;
|
| 40 |
+
}
|
| 41 |
+
.btn-primary:hover {
|
| 42 |
+
background: linear-gradient(to right, #155785, #2980b9);
|
| 43 |
+
}
|
| 44 |
+
.btn-secondary {
|
| 45 |
+
width: 100%;
|
| 46 |
+
padding: 15px;
|
| 47 |
+
margin-top: 10px;
|
| 48 |
+
}
|
| 49 |
+
.attendance-table {
|
| 50 |
+
width: 100%;
|
| 51 |
+
border-collapse: collapse;
|
| 52 |
+
margin-top: 20px;
|
| 53 |
+
}
|
| 54 |
+
.attendance-table th {
|
| 55 |
+
background-color: #1a4a7c;
|
| 56 |
+
color: white;
|
| 57 |
+
padding: 12px;
|
| 58 |
+
text-align: left;
|
| 59 |
+
}
|
| 60 |
+
.attendance-table td {
|
| 61 |
+
padding: 10px;
|
| 62 |
+
border-bottom: 1px solid #ddd;
|
| 63 |
+
}
|
| 64 |
+
.attendance-table tr:nth-child(even) {
|
| 65 |
+
background-color: #f9f9f9;
|
| 66 |
+
}
|
| 67 |
+
.attendance-table tr:hover {
|
| 68 |
+
background-color: #f1f1f1;
|
| 69 |
+
}
|
| 70 |
+
.status-badge {
|
| 71 |
+
padding: 4px 8px;
|
| 72 |
+
border-radius: 4px;
|
| 73 |
+
font-size: 12px;
|
| 74 |
+
font-weight: bold;
|
| 75 |
+
}
|
| 76 |
+
.badge-present {
|
| 77 |
+
background-color: #28a745;
|
| 78 |
+
color: white;
|
| 79 |
+
}
|
| 80 |
+
.badge-absent {
|
| 81 |
+
background-color: #dc3545;
|
| 82 |
+
color: white;
|
| 83 |
+
}
|
| 84 |
+
</style>
|
| 85 |
+
</head>
|
| 86 |
+
<body>
|
| 87 |
+
<div class="container">
|
| 88 |
+
<div class="dashboard-box">
|
| 89 |
+
<h1>Welcome, {{ user.full_name }}</h1>
|
| 90 |
+
<p>Email: {{ user.email }}</p>
|
| 91 |
+
<button class="btn btn-primary" onclick="window.location.href='/timetable'"><i class="fas fa-calendar-alt"></i> Timetable</button>
|
| 92 |
+
<a href="/logout" class="btn btn-secondary">Logout</a>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div class="dashboard-box">
|
| 96 |
+
<h2><i class="fas fa-clipboard-check"></i> Attendance Records</h2>
|
| 97 |
+
<div class="form-group">
|
| 98 |
+
<label for="classSelect">Select Class:</label>
|
| 99 |
+
<select id="classSelect" class="form-control">
|
| 100 |
+
<option value="">-- Select Class --</option>
|
| 101 |
+
<option value="SY-19">SY-19</option>
|
| 102 |
+
<option value="SY-20">SY-20</option>
|
| 103 |
+
<option value="SY-21">SY-21</option>
|
| 104 |
+
<option value="SY-22">SY-22</option>
|
| 105 |
+
</select>
|
| 106 |
+
</div>
|
| 107 |
+
<div id="attendanceData" class="mt-4">
|
| 108 |
+
<!-- Attendance data will be loaded here -->
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<script>
|
| 114 |
+
document.getElementById('classSelect').addEventListener('change', function() {
|
| 115 |
+
const classSelected = this.value;
|
| 116 |
+
const attendanceDataDiv = document.getElementById('attendanceData');
|
| 117 |
+
|
| 118 |
+
if (!classSelected) {
|
| 119 |
+
attendanceDataDiv.innerHTML = '';
|
| 120 |
+
return;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// Show loading state
|
| 124 |
+
attendanceDataDiv.innerHTML = `
|
| 125 |
+
<div class="text-center py-4">
|
| 126 |
+
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
| 127 |
+
<p>Loading attendance data for ${classSelected}...</p>
|
| 128 |
+
</div>
|
| 129 |
+
`;
|
| 130 |
+
|
| 131 |
+
fetch(`/get_attendance_data?class=${classSelected}`)
|
| 132 |
+
.then(response => {
|
| 133 |
+
if (!response.ok) {
|
| 134 |
+
return response.json().then(err => { throw new Error(err.error || 'Server error'); });
|
| 135 |
+
}
|
| 136 |
+
return response.json();
|
| 137 |
+
})
|
| 138 |
+
.then(result => {
|
| 139 |
+
if (!result.success) {
|
| 140 |
+
throw new Error(result.error || 'Failed to load data');
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
const { data, columns, count } = result;
|
| 144 |
+
|
| 145 |
+
if (count === 0) {
|
| 146 |
+
attendanceDataDiv.innerHTML = `
|
| 147 |
+
<div class="alert alert-info mt-3">
|
| 148 |
+
No attendance records found for ${classSelected}
|
| 149 |
+
</div>
|
| 150 |
+
`;
|
| 151 |
+
return;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
// Create table HTML
|
| 155 |
+
let tableHTML = `
|
| 156 |
+
<div class="alert alert-success">
|
| 157 |
+
Showing ${count} records for ${classSelected}
|
| 158 |
+
</div>
|
| 159 |
+
<div class="table-responsive">
|
| 160 |
+
<table class="attendance-table">
|
| 161 |
+
<thead>
|
| 162 |
+
<tr>
|
| 163 |
+
${columns.map(col => `<th>${col}</th>`).join('')}
|
| 164 |
+
<th>Status</th>
|
| 165 |
+
</tr>
|
| 166 |
+
</thead>
|
| 167 |
+
<tbody>
|
| 168 |
+
${data.map(row => `
|
| 169 |
+
<tr>
|
| 170 |
+
${columns.map(col => `
|
| 171 |
+
<td>${row[col] || '-'}</td>
|
| 172 |
+
`).join('')}
|
| 173 |
+
<td>
|
| 174 |
+
<span class="status-badge ${row['CheckOut'] ? 'badge-present' : 'badge-absent'}">
|
| 175 |
+
${row['CheckOut'] ? 'Present' : 'Absent'}
|
| 176 |
+
</span>
|
| 177 |
+
</td>
|
| 178 |
+
</tr>
|
| 179 |
+
`).join('')}
|
| 180 |
+
</tbody>
|
| 181 |
+
</table>
|
| 182 |
+
</div>
|
| 183 |
+
`;
|
| 184 |
+
|
| 185 |
+
attendanceDataDiv.innerHTML = tableHTML;
|
| 186 |
+
})
|
| 187 |
+
.catch(error => {
|
| 188 |
+
attendanceDataDiv.innerHTML = `
|
| 189 |
+
<div class="alert alert-danger">
|
| 190 |
+
<i class="fas fa-exclamation-triangle"></i> ${error.message}
|
| 191 |
+
</div>
|
| 192 |
+
`;
|
| 193 |
+
console.error('Error:', error);
|
| 194 |
+
});
|
| 195 |
+
});
|
| 196 |
+
</script>
|
| 197 |
+
</body>
|
| 198 |
+
</html>
|
templates/timetable.html
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Smart Timetable Editor</title>
|
| 7 |
+
<script src="/static/script.js" defer></script>
|
| 8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
| 9 |
+
<style>
|
| 10 |
+
/* ====== Reset & Base ====== */
|
| 11 |
+
* {
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
margin: 0;
|
| 14 |
+
padding: 0;
|
| 15 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
body {
|
| 19 |
+
background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%);
|
| 20 |
+
min-height: 100vh;
|
| 21 |
+
display: flex;
|
| 22 |
+
flex-direction: column;
|
| 23 |
+
align-items: center;
|
| 24 |
+
padding: 30px 20px;
|
| 25 |
+
color: #333;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.container {
|
| 29 |
+
width: 100%;
|
| 30 |
+
max-width: 1000px;
|
| 31 |
+
margin: 0 auto;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/* ====== Header ====== */
|
| 35 |
+
header {
|
| 36 |
+
width: 100%;
|
| 37 |
+
background: #fff;
|
| 38 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
| 39 |
+
padding: 15px 0;
|
| 40 |
+
margin-bottom: 30px;
|
| 41 |
+
border-bottom: 3px solid #1a4a7c;
|
| 42 |
+
border-radius: 10px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
header .header-content {
|
| 46 |
+
display: flex;
|
| 47 |
+
align-items: center;
|
| 48 |
+
justify-content: center;
|
| 49 |
+
padding: 0 20px;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
header h1 {
|
| 53 |
+
font-size: 1.8rem;
|
| 54 |
+
font-weight: 600;
|
| 55 |
+
color: #1a4a7c;
|
| 56 |
+
margin: 0;
|
| 57 |
+
text-align: center;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ====== Card Styles ====== */
|
| 61 |
+
.card {
|
| 62 |
+
background-color: #ffffff;
|
| 63 |
+
border-radius: 12px;
|
| 64 |
+
padding: 25px;
|
| 65 |
+
width: 100%;
|
| 66 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.12);
|
| 67 |
+
margin-bottom: 30px;
|
| 68 |
+
border-top: 4px solid #1a4a7c;
|
| 69 |
+
position: relative;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.card h3 {
|
| 73 |
+
color: #1a4a7c;
|
| 74 |
+
margin-bottom: 20px;
|
| 75 |
+
font-weight: 600;
|
| 76 |
+
position: relative;
|
| 77 |
+
padding-bottom: 10px;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.card h3:after {
|
| 81 |
+
content: "";
|
| 82 |
+
position: absolute;
|
| 83 |
+
bottom: 0;
|
| 84 |
+
left: 0;
|
| 85 |
+
width: 80px;
|
| 86 |
+
height: 3px;
|
| 87 |
+
background: linear-gradient(90deg, #1a4a7c, #3498db);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/* ====== Form Elements ====== */
|
| 91 |
+
.input-group {
|
| 92 |
+
margin-bottom: 15px;
|
| 93 |
+
position: relative;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.input-group label {
|
| 97 |
+
display: block;
|
| 98 |
+
margin-bottom: 8px;
|
| 99 |
+
color: #444;
|
| 100 |
+
font-size: 14px;
|
| 101 |
+
font-weight: 500;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.input-group select,
|
| 105 |
+
.input-group input {
|
| 106 |
+
width: 100%;
|
| 107 |
+
padding: 12px 15px;
|
| 108 |
+
border: 1px solid #ddd;
|
| 109 |
+
border-radius: 8px;
|
| 110 |
+
font-size: 14px;
|
| 111 |
+
transition: all 0.3s;
|
| 112 |
+
background-color: #f9fbff;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.input-group select:focus,
|
| 116 |
+
.input-group input:focus {
|
| 117 |
+
border-color: #3498db;
|
| 118 |
+
outline: none;
|
| 119 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15);
|
| 120 |
+
background-color: #fff;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* Button Styles */
|
| 124 |
+
.btn {
|
| 125 |
+
display: inline-block;
|
| 126 |
+
padding: 10px 18px;
|
| 127 |
+
border: none;
|
| 128 |
+
border-radius: 8px;
|
| 129 |
+
cursor: pointer;
|
| 130 |
+
font-size: 14px;
|
| 131 |
+
font-weight: 600;
|
| 132 |
+
text-align: center;
|
| 133 |
+
transition: all 0.3s;
|
| 134 |
+
margin: 5px;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.btn-primary {
|
| 138 |
+
color: #fff;
|
| 139 |
+
background: linear-gradient(to right, #1a4a7c, #3498db);
|
| 140 |
+
box-shadow: 0 4px 8px rgba(26, 74, 124, 0.25);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.btn-primary:hover {
|
| 144 |
+
transform: translateY(-2px);
|
| 145 |
+
box-shadow: 0 6px 12px rgba(26, 74, 124, 0.35);
|
| 146 |
+
background: linear-gradient(to right, #155785, #2980b9);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.btn-secondary {
|
| 150 |
+
color: #1a4a7c;
|
| 151 |
+
background: #e6f0ff;
|
| 152 |
+
border: 1px solid #c5d9f1;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.btn-secondary:hover {
|
| 156 |
+
background: #d1e3ff;
|
| 157 |
+
transform: translateY(-1px);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.btn-danger {
|
| 161 |
+
color: #fff;
|
| 162 |
+
background: linear-gradient(to right, #e74c3c, #c0392b);
|
| 163 |
+
box-shadow: 0 4px 8px rgba(231, 76, 60, 0.25);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.btn-danger:hover {
|
| 167 |
+
transform: translateY(-2px);
|
| 168 |
+
box-shadow: 0 6px 12px rgba(231, 76, 60, 0.35);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.btn-action {
|
| 172 |
+
display: flex;
|
| 173 |
+
align-items: center;
|
| 174 |
+
justify-content: center;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.btn-action i {
|
| 178 |
+
margin-right: 5px;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/* ====== Config Items ====== */
|
| 182 |
+
.config-item {
|
| 183 |
+
display: inline-block;
|
| 184 |
+
background-color: #f0f7ff;
|
| 185 |
+
border: 1px solid #c5d9f1;
|
| 186 |
+
padding: 5px 10px;
|
| 187 |
+
border-radius: 15px;
|
| 188 |
+
margin: 5px;
|
| 189 |
+
font-size: 13px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.config-list {
|
| 193 |
+
margin: 10px 0;
|
| 194 |
+
min-height: 30px;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.delete-btn {
|
| 198 |
+
color: #e74c3c;
|
| 199 |
+
cursor: pointer;
|
| 200 |
+
margin-left: 5px;
|
| 201 |
+
font-size: 12px;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/* ====== Timetable ====== */
|
| 205 |
+
.timetable-container {
|
| 206 |
+
overflow-x: auto;
|
| 207 |
+
width: 100%;
|
| 208 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
| 209 |
+
border-radius: 10px;
|
| 210 |
+
background: white;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
table {
|
| 214 |
+
border-collapse: collapse;
|
| 215 |
+
width: 100%;
|
| 216 |
+
margin-top: 5px;
|
| 217 |
+
background-color: white;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
th, td {
|
| 221 |
+
border: 1px solid #e0e0e0;
|
| 222 |
+
padding: 12px 15px;
|
| 223 |
+
text-align: center;
|
| 224 |
+
vertical-align: middle;
|
| 225 |
+
font-size: 14px;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
th {
|
| 229 |
+
background-color: #1a4a7c;
|
| 230 |
+
color: white;
|
| 231 |
+
font-weight: 600;
|
| 232 |
+
position: sticky;
|
| 233 |
+
top: 0;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
tr:nth-child(even) {
|
| 237 |
+
background-color: #f9fbff;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
tr:hover {
|
| 241 |
+
background-color: #f0f7ff;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
/* ====== Modal ====== */
|
| 245 |
+
.modal {
|
| 246 |
+
display: none;
|
| 247 |
+
position: fixed;
|
| 248 |
+
top: 0; left: 0; width: 100%; height: 100%;
|
| 249 |
+
background: rgba(0,0,0,0.6);
|
| 250 |
+
justify-content: center;
|
| 251 |
+
align-items: center;
|
| 252 |
+
z-index: 1000;
|
| 253 |
+
backdrop-filter: blur(3px);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.modal-content {
|
| 257 |
+
background: #fff;
|
| 258 |
+
padding: 25px;
|
| 259 |
+
border-radius: 12px;
|
| 260 |
+
width: 90%;
|
| 261 |
+
max-width: 450px;
|
| 262 |
+
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
|
| 263 |
+
position: relative;
|
| 264 |
+
border-top: 4px solid #1a4a7c;
|
| 265 |
+
animation: modalFadeIn 0.3s;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
@keyframes modalFadeIn {
|
| 269 |
+
from { opacity: 0; transform: translateY(-20px); }
|
| 270 |
+
to { opacity: 1; transform: translateY(0); }
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.close-modal {
|
| 274 |
+
position: absolute;
|
| 275 |
+
top: 15px; right: 15px;
|
| 276 |
+
background: #f0f4f8;
|
| 277 |
+
border: none;
|
| 278 |
+
font-size: 18px;
|
| 279 |
+
color: #555;
|
| 280 |
+
cursor: pointer;
|
| 281 |
+
width: 30px;
|
| 282 |
+
height: 30px;
|
| 283 |
+
border-radius: 50%;
|
| 284 |
+
display: flex;
|
| 285 |
+
align-items: center;
|
| 286 |
+
justify-content: center;
|
| 287 |
+
transition: all 0.3s;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.close-modal:hover {
|
| 291 |
+
background: #e0e6ed;
|
| 292 |
+
color: #1a4a7c;
|
| 293 |
+
transform: rotate(90deg);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.modal-content h3 {
|
| 297 |
+
color: #1a4a7c;
|
| 298 |
+
margin-bottom: 20px;
|
| 299 |
+
font-weight: 600;
|
| 300 |
+
text-align: center;
|
| 301 |
+
position: relative;
|
| 302 |
+
padding-bottom: 10px;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.modal-content h3:after {
|
| 306 |
+
content: "";
|
| 307 |
+
position: absolute;
|
| 308 |
+
bottom: 0;
|
| 309 |
+
left: 50%;
|
| 310 |
+
transform: translateX(-50%);
|
| 311 |
+
width: 80px;
|
| 312 |
+
height: 3px;
|
| 313 |
+
background: linear-gradient(90deg, #1a4a7c, #3498db);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
/* ====== Action Bar ====== */
|
| 317 |
+
.action-bar {
|
| 318 |
+
display: flex;
|
| 319 |
+
justify-content: space-between;
|
| 320 |
+
margin-bottom: 20px;
|
| 321 |
+
flex-wrap: wrap;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.action-group {
|
| 325 |
+
display: flex;
|
| 326 |
+
gap: 10px;
|
| 327 |
+
margin: 5px 0;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
/* ====== Responsive Adjustments ====== */
|
| 331 |
+
@media (max-width: 768px) {
|
| 332 |
+
header h1 { font-size: 1.4rem; }
|
| 333 |
+
.card { padding: 20px; }
|
| 334 |
+
.input-group label { font-size: 13px; }
|
| 335 |
+
.input-group select, .input-group input { padding: 10px; font-size: 13px; }
|
| 336 |
+
.modal-content { padding: 20px; }
|
| 337 |
+
.btn { padding: 8px 15px; font-size: 13px; }
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
@media (max-width: 576px) {
|
| 341 |
+
header h1 { font-size: 1.2rem; }
|
| 342 |
+
.action-bar { flex-direction: column; }
|
| 343 |
+
.action-group { width: 100%; justify-content: center; }
|
| 344 |
+
th, td { padding: 8px; font-size: 12px; }
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
/* Toast message */
|
| 348 |
+
.toast-container {
|
| 349 |
+
position: fixed;
|
| 350 |
+
bottom: 20px;
|
| 351 |
+
right: 20px;
|
| 352 |
+
z-index: 1001;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.toast {
|
| 356 |
+
background-color: rgba(26, 74, 124, 0.9);
|
| 357 |
+
color: white;
|
| 358 |
+
padding: 15px 25px;
|
| 359 |
+
border-radius: 8px;
|
| 360 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
| 361 |
+
margin-top: 10px;
|
| 362 |
+
display: flex;
|
| 363 |
+
align-items: center;
|
| 364 |
+
animation: toastFadeIn 0.3s;
|
| 365 |
+
backdrop-filter: blur(5px);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.toast i {
|
| 369 |
+
margin-right: 10px;
|
| 370 |
+
font-size: 20px;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
@keyframes toastFadeIn {
|
| 374 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 375 |
+
to { opacity: 1; transform: translateY(0); }
|
| 376 |
+
}
|
| 377 |
+
</style>
|
| 378 |
+
</head>
|
| 379 |
+
<body>
|
| 380 |
+
<div class="container">
|
| 381 |
+
<header>
|
| 382 |
+
<div class="header-content">
|
| 383 |
+
<h1><i class="fas fa-calendar-alt"></i> Smart Timetable Editor</h1>
|
| 384 |
+
</div>
|
| 385 |
+
</header>
|
| 386 |
+
|
| 387 |
+
<div class="card">
|
| 388 |
+
<h3>Class Configuration</h3>
|
| 389 |
+
<div class="input-group">
|
| 390 |
+
<label for="classSelect"><i class="fas fa-users"></i> Select Class:</label>
|
| 391 |
+
<select id="classSelect" onchange="loadForSelectedClass()">
|
| 392 |
+
<option value="">--Select Class--</option>
|
| 393 |
+
<option value="SY-19">SY-19</option>
|
| 394 |
+
<option value="SY-20">SY-20</option>
|
| 395 |
+
<option value="SY-21">SY-21</option>
|
| 396 |
+
<option value="SY-22">SY-22</option>
|
| 397 |
+
</select>
|
| 398 |
+
</div>
|
| 399 |
+
</div>
|
| 400 |
+
|
| 401 |
+
<div class="card">
|
| 402 |
+
<h3>Subject Configuration</h3>
|
| 403 |
+
|
| 404 |
+
<!-- Regular Subjects -->
|
| 405 |
+
<div class="input-group">
|
| 406 |
+
<label for="subjectInput"><i class="fas fa-book"></i> Subject:</label>
|
| 407 |
+
<div style="display: flex; gap: 10px;">
|
| 408 |
+
<input type="text" id="subjectInput" placeholder="Enter subject name">
|
| 409 |
+
<button class="btn btn-primary" onclick="addConfig('subject')">Add</button>
|
| 410 |
+
</div>
|
| 411 |
+
</div>
|
| 412 |
+
<div class="config-list" id="subjectList"></div>
|
| 413 |
+
|
| 414 |
+
<!-- Specialization Subjects -->
|
| 415 |
+
<div class="input-group">
|
| 416 |
+
<label for="specSubjectInput"><i class="fas fa-book-reader"></i> Specialization Subject:</label>
|
| 417 |
+
<div style="display: flex; gap: 10px;">
|
| 418 |
+
<input type="text" id="specSubjectInput" placeholder="Enter specialization subject">
|
| 419 |
+
<button class="btn btn-primary" onclick="addConfig('specSubject')">Add</button>
|
| 420 |
+
</div>
|
| 421 |
+
</div>
|
| 422 |
+
<div class="config-list" id="specSubjectList"></div>
|
| 423 |
+
|
| 424 |
+
<!-- Batches -->
|
| 425 |
+
<div class="input-group">
|
| 426 |
+
<label for="batchInput"><i class="fas fa-layer-group"></i> Batch:</label>
|
| 427 |
+
<div style="display: flex; gap: 10px;">
|
| 428 |
+
<input type="text" id="batchInput" placeholder="Enter batch name">
|
| 429 |
+
<button class="btn btn-primary" onclick="addConfig('batch')">Add</button>
|
| 430 |
+
</div>
|
| 431 |
+
</div>
|
| 432 |
+
<div class="config-list" id="batchList"></div>
|
| 433 |
+
|
| 434 |
+
<!-- Practical Subjects -->
|
| 435 |
+
<div class="input-group">
|
| 436 |
+
<label for="practicalSubjectInput"><i class="fas fa-flask"></i> Practical Subject:</label>
|
| 437 |
+
<div style="display: flex; gap: 10px;">
|
| 438 |
+
<input type="text" id="practicalSubjectInput" placeholder="Enter practical subject">
|
| 439 |
+
<button class="btn btn-primary" onclick="addConfig('practical')">Add</button>
|
| 440 |
+
</div>
|
| 441 |
+
</div>
|
| 442 |
+
<div class="config-list" id="practicalSubjectList"></div>
|
| 443 |
+
</div>
|
| 444 |
+
|
| 445 |
+
<div class="action-bar">
|
| 446 |
+
<div class="action-group">
|
| 447 |
+
<button class="btn btn-primary btn-action" onclick="openModal()">
|
| 448 |
+
<i class="fas fa-plus-circle"></i> Create/Update Timetable
|
| 449 |
+
</button>
|
| 450 |
+
</div>
|
| 451 |
+
<div class="action-group">
|
| 452 |
+
<button class="btn btn-secondary btn-action" onclick="downloadExcel()">
|
| 453 |
+
<i class="fas fa-file-download"></i> Download Excel
|
| 454 |
+
</button>
|
| 455 |
+
</div>
|
| 456 |
+
</div>
|
| 457 |
+
|
| 458 |
+
<div class="timetable-container">
|
| 459 |
+
<table id="timetable"></table>
|
| 460 |
+
</div>
|
| 461 |
+
</div>
|
| 462 |
+
|
| 463 |
+
<!-- Modal Dialog -->
|
| 464 |
+
<div class="modal" id="popupModal">
|
| 465 |
+
<div class="modal-content">
|
| 466 |
+
<span class="close-modal" onclick="closeModal()">✕</span>
|
| 467 |
+
<h3>Update Timetable</h3>
|
| 468 |
+
|
| 469 |
+
<div class="input-group">
|
| 470 |
+
<label for="daySelect"><i class="fas fa-calendar-day"></i> Select Day:</label>
|
| 471 |
+
<select id="daySelect">
|
| 472 |
+
<option value="">--Select Day--</option>
|
| 473 |
+
<option value="Monday">Monday</option>
|
| 474 |
+
<option value="Tuesday">Tuesday</option>
|
| 475 |
+
<option value="Wednesday">Wednesday</option>
|
| 476 |
+
<option value="Thursday">Thursday</option>
|
| 477 |
+
<option value="Friday">Friday</option>
|
| 478 |
+
<option value="Saturday">Saturday</option>
|
| 479 |
+
<option value="Sunday">Sunday</option>
|
| 480 |
+
</select>
|
| 481 |
+
</div>
|
| 482 |
+
|
| 483 |
+
<div class="input-group">
|
| 484 |
+
<label for="optionType"><i class="fas fa-list-alt"></i> Choose Option:</label>
|
| 485 |
+
<select id="optionType" onchange="optionTypeChanged()">
|
| 486 |
+
<option value="">--Select Option--</option>
|
| 487 |
+
<option value="subject">Subject</option>
|
| 488 |
+
<option value="specSubject">Specialization Subject</option>
|
| 489 |
+
<option value="practical">Practical Subject</option>
|
| 490 |
+
</select>
|
| 491 |
+
</div>
|
| 492 |
+
|
| 493 |
+
<div id="subjectDropdown" class="input-group" style="display:none;">
|
| 494 |
+
<label for="subjectSelect"><i class="fas fa-book"></i> Select Subject:</label>
|
| 495 |
+
<select id="subjectSelect"></select>
|
| 496 |
+
</div>
|
| 497 |
+
|
| 498 |
+
<div id="specSubjectDropdown" class="input-group" style="display:none;">
|
| 499 |
+
<label for="specSubjectSelect"><i class="fas fa-book-reader"></i> Select Specialization Subject:</label>
|
| 500 |
+
<select id="specSubjectSelect"></select>
|
| 501 |
+
</div>
|
| 502 |
+
|
| 503 |
+
<div id="practicalDropdown" class="input-group" style="display:none;">
|
| 504 |
+
<label for="practicalSelect"><i class="fas fa-flask"></i> Select Practical Subject:</label>
|
| 505 |
+
<select id="practicalSelect"></select>
|
| 506 |
+
|
| 507 |
+
<label for="batchSelect" style="margin-top:10px;"><i class="fas fa-layer-group"></i> Select Batch:</label>
|
| 508 |
+
<select id="batchSelect"></select>
|
| 509 |
+
</div>
|
| 510 |
+
|
| 511 |
+
<div id="timeSlotDiv" class="input-group" style="display:none;">
|
| 512 |
+
<label for="timeSlotSelect"><i class="fas fa-clock"></i> Select Time Slot:</label>
|
| 513 |
+
<select id="timeSlotSelect"></select>
|
| 514 |
+
</div>
|
| 515 |
+
|
| 516 |
+
<button id="submitBtn" class="btn btn-primary" style="display:none; width:100%; margin-top:20px;" onclick="submitTimetableEntry()">
|
| 517 |
+
<i class="fas fa-check-circle"></i> Submit
|
| 518 |
+
</button>
|
| 519 |
+
</div>
|
| 520 |
+
</div>
|
| 521 |
+
|
| 522 |
+
<!-- Toast container -->
|
| 523 |
+
<div class="toast-container" id="toastContainer"></div>
|
| 524 |
+
|
| 525 |
+
<script>
|
| 526 |
+
// This script will be replaced by your /static/script.js
|
| 527 |
+
// We're keeping the original script.js file reference at the top
|
| 528 |
+
|
| 529 |
+
// Just adding a few helper functions to demonstrate the toast
|
| 530 |
+
function showToast(message, type = 'info') {
|
| 531 |
+
const toastContainer = document.getElementById('toastContainer');
|
| 532 |
+
const toast = document.createElement('div');
|
| 533 |
+
toast.className = 'toast';
|
| 534 |
+
|
| 535 |
+
let icon = 'info-circle';
|
| 536 |
+
if (type === 'success') icon = 'check-circle';
|
| 537 |
+
if (type === 'error') icon = 'exclamation-circle';
|
| 538 |
+
|
| 539 |
+
toast.innerHTML = `<i class="fas fa-${icon}"></i> ${message}`;
|
| 540 |
+
toastContainer.appendChild(toast);
|
| 541 |
+
|
| 542 |
+
setTimeout(() => {
|
| 543 |
+
toast.style.opacity = '0';
|
| 544 |
+
toast.style.transform = 'translateY(20px)';
|
| 545 |
+
toast.style.transition = 'all 0.3s ease';
|
| 546 |
+
setTimeout(() => toast.remove(), 300);
|
| 547 |
+
}, 3000);
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
// Placeholder functions to prevent console errors
|
| 551 |
+
function loadForSelectedClass() {
|
| 552 |
+
const selectedClass = document.getElementById('classSelect').value;
|
| 553 |
+
if(selectedClass) {
|
| 554 |
+
showToast(`Loading data for class ${selectedClass}`, 'info');
|
| 555 |
+
}
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
function addConfig(type) {
|
| 559 |
+
let inputId, listId;
|
| 560 |
+
switch(type) {
|
| 561 |
+
case 'subject':
|
| 562 |
+
inputId = 'subjectInput';
|
| 563 |
+
listId = 'subjectList';
|
| 564 |
+
break;
|
| 565 |
+
case 'specSubject':
|
| 566 |
+
inputId = 'specSubjectInput';
|
| 567 |
+
listId = 'specSubjectList';
|
| 568 |
+
break;
|
| 569 |
+
case 'batch':
|
| 570 |
+
inputId = 'batchInput';
|
| 571 |
+
listId = 'batchList';
|
| 572 |
+
break;
|
| 573 |
+
case 'practical':
|
| 574 |
+
inputId = 'practicalSubjectInput';
|
| 575 |
+
listId = 'practicalSubjectList';
|
| 576 |
+
break;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
const input = document.getElementById(inputId);
|
| 580 |
+
const value = input.value.trim();
|
| 581 |
+
|
| 582 |
+
if (value) {
|
| 583 |
+
const list = document.getElementById(listId);
|
| 584 |
+
const item = document.createElement('span');
|
| 585 |
+
item.className = 'config-item';
|
| 586 |
+
item.innerHTML = `${value} <span class="delete-btn" onclick="this.parentElement.remove()"><i class="fas fa-times"></i></span>`;
|
| 587 |
+
list.appendChild(item);
|
| 588 |
+
input.value = '';
|
| 589 |
+
showToast(`Added ${value} to ${type}s`, 'success');
|
| 590 |
+
}
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
function openModal() {
|
| 594 |
+
document.getElementById('popupModal').style.display = 'flex';
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
function closeModal() {
|
| 598 |
+
document.getElementById('popupModal').style.display = 'none';
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
function optionTypeChanged() {
|
| 602 |
+
const optionType = document.getElementById('optionType').value;
|
| 603 |
+
document.getElementById('subjectDropdown').style.display = 'none';
|
| 604 |
+
document.getElementById('specSubjectDropdown').style.display = 'none';
|
| 605 |
+
document.getElementById('practicalDropdown').style.display = 'none';
|
| 606 |
+
document.getElementById('timeSlotDiv').style.display = 'none';
|
| 607 |
+
document.getElementById('submitBtn').style.display = 'none';
|
| 608 |
+
|
| 609 |
+
if (optionType === 'subject') {
|
| 610 |
+
document.getElementById('subjectDropdown').style.display = 'block';
|
| 611 |
+
document.getElementById('timeSlotDiv').style.display = 'block';
|
| 612 |
+
document.getElementById('submitBtn').style.display = 'block';
|
| 613 |
+
} else if (optionType === 'specSubject') {
|
| 614 |
+
document.getElementById('specSubjectDropdown').style.display = 'block';
|
| 615 |
+
document.getElementById('timeSlotDiv').style.display = 'block';
|
| 616 |
+
document.getElementById('submitBtn').style.display = 'block';
|
| 617 |
+
} else if (optionType === 'practical') {
|
| 618 |
+
document.getElementById('practicalDropdown').style.display = 'block';
|
| 619 |
+
document.getElementById('timeSlotDiv').style.display = 'block';
|
| 620 |
+
document.getElementById('submitBtn').style.display = 'block';
|
| 621 |
+
}
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
function submitTimetableEntry() {
|
| 625 |
+
const day = document.getElementById('daySelect').value;
|
| 626 |
+
const optionType = document.getElementById('optionType').value;
|
| 627 |
+
const timeSlot = document.getElementById('timeSlotSelect').value;
|
| 628 |
+
|
| 629 |
+
if (!day || !optionType || !timeSlot) {
|
| 630 |
+
showToast('Please fill all required fields', 'error');
|
| 631 |
+
return;
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
showToast('Timetable entry added successfully', 'success');
|
| 635 |
+
closeModal();
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
function downloadExcel() {
|
| 639 |
+
showToast('Downloading Excel file...', 'info');
|
| 640 |
+
// Implement actual Excel download logic
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
// Initialize a sample timetable
|
| 644 |
+
window.addEventListener('DOMContentLoaded', () => {
|
| 645 |
+
const table = document.getElementById('timetable');
|
| 646 |
+
|
| 647 |
+
// Sample timetable data
|
| 648 |
+
const days = ['Time', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
|
| 649 |
+
const times = ['9:00-10:00', '10:00-11:00', '11:00-12:00', '12:00-1:00', '1:00-2:00'];
|
| 650 |
+
|
| 651 |
+
// Create header row
|
| 652 |
+
const headerRow = document.createElement('tr');
|
| 653 |
+
days.forEach(day => {
|
| 654 |
+
const th = document.createElement('th');
|
| 655 |
+
th.textContent = day;
|
| 656 |
+
headerRow.appendChild(th);
|
| 657 |
+
});
|
| 658 |
+
table.appendChild(headerRow);
|
| 659 |
+
|
| 660 |
+
// Create time rows
|
| 661 |
+
times.forEach(time => {
|
| 662 |
+
const row = document.createElement('tr');
|
| 663 |
+
|
| 664 |
+
// Add time cell
|
| 665 |
+
const timeCell = document.createElement('td');
|
| 666 |
+
timeCell.textContent = time;
|
| 667 |
+
timeCell.style.fontWeight = 'bold';
|
| 668 |
+
timeCell.style.backgroundColor = '#f0f7ff';
|
| 669 |
+
row.appendChild(timeCell);
|
| 670 |
+
|
| 671 |
+
// Add empty cells for each day
|
| 672 |
+
for (let i = 1; i < days.length; i++) {
|
| 673 |
+
const td = document.createElement('td');
|
| 674 |
+
td.textContent = '';
|
| 675 |
+
row.appendChild(td);
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
table.appendChild(row);
|
| 679 |
+
});
|
| 680 |
+
|
| 681 |
+
// Populate dropdown for time slots
|
| 682 |
+
const timeSlotSelect = document.getElementById('timeSlotSelect');
|
| 683 |
+
times.forEach(time => {
|
| 684 |
+
const option = document.createElement('option');
|
| 685 |
+
option.value = time;
|
| 686 |
+
option.textContent = time;
|
| 687 |
+
timeSlotSelect.appendChild(option);
|
| 688 |
+
});
|
| 689 |
+
});
|
| 690 |
+
</script>
|
| 691 |
+
</body>
|
| 692 |
+
</html>
|