harshdhane commited on
Commit
0a864f1
·
verified ·
1 Parent(s): bec0c09

Upload 19 files

Browse files
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> &nbsp;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> &nbsp;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>