harshdhane commited on
Commit
8ad39f4
·
verified ·
1 Parent(s): cdc9786

Upload 21 files

Browse files
SY-20_timetable.xlsx ADDED
Binary file (5.36 kB). View file
 
SY-21_timetable.xlsx ADDED
Binary file (5.34 kB). View file
 
SY-22_timetable.xlsx ADDED
Binary file (8.91 kB). View file
 
app.py ADDED
@@ -0,0 +1,809 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, session, redirect, url_for, send_file
2
+ from flask_sqlalchemy import SQLAlchemy
3
+ from flask_mail import Mail, Message
4
+ from werkzeug.security import generate_password_hash, check_password_hash
5
+ import sqlite3
6
+ import pandas as pd
7
+ import os
8
+ import cv2
9
+ import numpy as np
10
+ from datetime import datetime, time, date
11
+ import base64
12
+ import json
13
+ import random
14
+
15
+ app = Flask(__name__)
16
+ app.config['SECRET_KEY'] = 'your_secret_key_here'
17
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
18
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
19
+
20
+ # Email configuration
21
+ app.config['MAIL_SERVER'] = 'smtp.gmail.com'
22
+ app.config['MAIL_PORT'] = 465
23
+ app.config['MAIL_USERNAME'] = 'your_email@gmail.com' # Replace with your email
24
+ app.config['MAIL_PASSWORD'] = 'your_password' # Replace with your password
25
+ app.config['MAIL_USE_TLS'] = False
26
+ app.config['MAIL_USE_SSL'] = True
27
+
28
+ db = SQLAlchemy(app)
29
+ mail = Mail(app)
30
+
31
+ # User Model
32
+ class User(db.Model):
33
+ id = db.Column(db.Integer, primary_key=True)
34
+ full_name = db.Column(db.String(100), nullable=False)
35
+ email = db.Column(db.String(100), unique=True, nullable=False)
36
+ username = db.Column(db.String(50), unique=True, nullable=False)
37
+ password = db.Column(db.String(100), nullable=False)
38
+ role = db.Column(db.String(10), nullable=False)
39
+ enrollment_number = db.Column(db.String(20), unique=True, nullable=True)
40
+ phone_number = db.Column(db.String(20), nullable=True)
41
+ student_number = db.Column(db.String(20), unique=True, nullable=True)
42
+ parent_number = db.Column(db.String(20), nullable=True)
43
+ parent_email = db.Column(db.String(100), nullable=True)
44
+ selected_class = db.Column(db.String(10), nullable=True)
45
+ selected_regular_subjects = db.Column(db.Text, nullable=True) # JSON list
46
+ selected_specializations = db.Column(db.Text, nullable=True) # JSON list
47
+ selected_batch = db.Column(db.String(20), nullable=True)
48
+ selected_practicals = db.Column(db.Text, nullable=True) # JSON list
49
+
50
+ # Daily PIN Model
51
+ class DailyPin(db.Model):
52
+ id = db.Column(db.Integer, primary_key=True)
53
+ class_name = db.Column(db.String(10), nullable=False)
54
+ pin = db.Column(db.String(4), nullable=False)
55
+ date = db.Column(db.Date, nullable=False)
56
+
57
+ # Timetable Database Setup
58
+ def get_db_connection():
59
+ conn = sqlite3.connect('config.db')
60
+ conn.row_factory = sqlite3.Row
61
+ return conn
62
+
63
+ def init_timetable_db():
64
+ conn = get_db_connection()
65
+ conn.execute('''
66
+ CREATE TABLE IF NOT EXISTS teacher_config (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ class_name TEXT NOT NULL,
69
+ config_type TEXT NOT NULL,
70
+ name TEXT NOT NULL
71
+ )
72
+ ''')
73
+ conn.commit()
74
+ conn.close()
75
+
76
+ # Create databases
77
+ with app.app_context():
78
+ db.create_all()
79
+ init_timetable_db()
80
+
81
+ # Attendance System Setup
82
+ face_cascade = cv2.CascadeClassifier("haarcascade_frontalface_alt.xml")
83
+ dataset_path = "./face_dataset/"
84
+ attendance_path = "./attendance_data/"
85
+ os.makedirs(dataset_path, exist_ok=True)
86
+ os.makedirs(attendance_path, exist_ok=True)
87
+
88
+ # Function to get or generate daily PIN for a class
89
+ def get_daily_pin(class_name):
90
+ today = date.today()
91
+ pin_entry = DailyPin.query.filter_by(class_name=class_name, date=today).first()
92
+ if not pin_entry:
93
+ pin = f"{random.randint(0, 9999):04d}" # Generate 4-digit PIN
94
+ pin_entry = DailyPin(class_name=class_name, pin=pin, date=today)
95
+ db.session.add(pin_entry)
96
+ db.session.commit()
97
+ return pin_entry.pin
98
+
99
+ # Combined Routes
100
+ @app.route('/')
101
+ def index():
102
+ return render_template('index.html')
103
+
104
+ @app.route('/signup', methods=['POST'])
105
+ def signup():
106
+ data = request.json
107
+ full_name = data.get('full_name')
108
+ email = data.get('email')
109
+ username = data.get('username')
110
+ password = data.get('password')
111
+ role = data.get('role')
112
+ enrollment = data.get('enrollment_number') if role == 'student' else None
113
+ phone_number = data.get('phone_number') if role == 'teacher' else None
114
+ student_number = data.get('student_number') if role == 'student' else None
115
+ parent_number = data.get('parent_number') if role == 'student' else None
116
+ parent_email = data.get('parent_email') if role == 'student' else None
117
+
118
+ if not all([full_name, email, username, password, role]):
119
+ return jsonify({'success': False, 'message': 'All fields are required!'})
120
+ if role == 'student' and not enrollment:
121
+ return jsonify({'success': False, 'message': 'Enrollment number is required for students!'})
122
+ if role == 'teacher' and not phone_number:
123
+ return jsonify({'success': False, 'message': 'Phone number is required for teachers!'})
124
+
125
+ if User.query.filter((User.username == username) | (User.email == email) |
126
+ (User.enrollment_number == enrollment) | (User.student_number == student_number)).first():
127
+ return jsonify({'success': False, 'message': 'Username, email, enrollment, or student number already exists!'})
128
+
129
+ hashed = generate_password_hash(password)
130
+ u = User(
131
+ full_name=full_name, email=email,
132
+ username=username, password=hashed, role=role,
133
+ enrollment_number=enrollment,
134
+ phone_number=phone_number,
135
+ student_number=student_number,
136
+ parent_number=parent_number,
137
+ parent_email=parent_email
138
+ )
139
+ db.session.add(u)
140
+ db.session.commit()
141
+ return jsonify({'success': True, 'message': 'Account created successfully! Please log in.'})
142
+
143
+ @app.route('/login', methods=['POST'])
144
+ def login():
145
+ data = request.json
146
+ username = data.get('username')
147
+ password = data.get('password')
148
+ role = data.get('role')
149
+
150
+ user = User.query.filter_by(username=username, role=role).first()
151
+ if user and check_password_hash(user.password, password):
152
+ session['user_id'] = user.id
153
+ session['role'] = user.role
154
+ redirect_url = '/teacher_dashboard' if role == 'teacher' else '/student_dashboard'
155
+ return jsonify({'success': True, 'redirect': redirect_url})
156
+ return jsonify({'success': False, 'message': 'Invalid username or password'})
157
+
158
+ @app.route('/logout')
159
+ def logout():
160
+ session.clear()
161
+ return redirect(url_for('index'))
162
+
163
+ @app.route('/teacher_dashboard')
164
+ def teacher_dashboard():
165
+ if 'user_id' not in session or session['role'] != 'teacher':
166
+ return redirect(url_for('index'))
167
+ user = db.session.get(User, session['user_id'])
168
+ pin = get_daily_pin(user.selected_class) if user.selected_class else None
169
+ return render_template('teacher_dashboard.html', user=user, selected_class=user.selected_class, daily_pin=pin)
170
+
171
+ @app.route('/api/update_teacher_profile', methods=['POST'])
172
+ def update_teacher_profile():
173
+ if 'user_id' not in session or session['role'] != 'teacher':
174
+ return jsonify({'success': False, 'message': 'Unauthorized'}), 401
175
+ data = request.json
176
+ user = db.session.get(User, session['user_id'])
177
+ user.full_name = data['full_name']
178
+ user.email = data['email']
179
+ user.phone_number = data['phone_number']
180
+ db.session.commit()
181
+ return jsonify({'success': True, 'message': 'Profile updated'})
182
+
183
+ @app.route('/get_students', methods=['GET'])
184
+ def get_students():
185
+ class_name = request.args.get('class')
186
+ students = User.query.filter_by(role='student', selected_class=class_name).all()
187
+ students_data = [{'id': s.id, 'full_name': s.full_name, 'enrollment_number': s.enrollment_number} for s in students]
188
+ return jsonify({'students': students_data})
189
+
190
+ @app.route('/api/select_teacher_class', methods=['POST'])
191
+ def select_teacher_class():
192
+ if 'user_id' not in session or session['role'] != 'teacher':
193
+ return jsonify({'success': False}), 401
194
+ data = request.json
195
+ cls = data.get('class')
196
+ user = db.session.get(User, session['user_id'])
197
+ user.selected_class = cls
198
+ db.session.commit()
199
+ return jsonify({'success': True})
200
+
201
+ @app.route('/get_student_details', methods=['GET'])
202
+ def get_student_details():
203
+ student_id = request.args.get('id')
204
+ student = db.session.get(User, student_id)
205
+ if not student or student.role != 'student':
206
+ return jsonify({'error': 'Student not found'}), 404
207
+
208
+ subject_attendance = get_subject_attendance(student)
209
+
210
+ regular_subjects = [{'name': subj, 'attendance_count': subject_attendance['regular'].get(subj, {'attendance': 0})['attendance'], 'total_count': subject_attendance['regular'].get(subj, {'total': 0})['total']} for subj in json.loads(student.selected_regular_subjects or '[]')]
211
+ specialization_subjects = [{'name': subj, 'attendance_count': subject_attendance['specialization'].get(subj, {'attendance': 0})['attendance'], 'total_count': subject_attendance['specialization'].get(subj, {'total': 0})['total']} for subj in json.loads(student.selected_specializations or '[]')]
212
+ practical_subjects = [{'name': subj, 'batch': student.selected_batch or 'N/A', 'attendance_count': subject_attendance['practical'].get(subj, {'attendance': 0})['attendance'], 'total_count': subject_attendance['practical'].get(subj, {'total': 0})['total']} for subj in json.loads(student.selected_practicals or '[]')]
213
+
214
+ face_folder = os.path.join(dataset_path, student.selected_class or '')
215
+ face_file = f"{student.enrollment_number}_{student.full_name}.npy"
216
+ face_registered = os.path.exists(os.path.join(face_folder, face_file))
217
+
218
+ student_data = {
219
+ 'enrollment_number': student.enrollment_number,
220
+ 'email': student.email,
221
+ 'student_number': student.student_number,
222
+ 'parent_number': student.parent_number,
223
+ 'parent_email': student.parent_email,
224
+ 'regular_subjects': regular_subjects,
225
+ 'specialization_subjects': specialization_subjects,
226
+ 'practical_subjects': practical_subjects,
227
+ 'face_registered': face_registered
228
+ }
229
+ return jsonify(student_data)
230
+
231
+ @app.route('/student_dashboard')
232
+ def student_dashboard():
233
+ if 'user_id' not in session or session['role'] != 'student':
234
+ return redirect(url_for('index'))
235
+ user = db.session.get(User, session['user_id'])
236
+ face_folder = os.path.join(dataset_path, (user.selected_class or ''))
237
+ face_file = f"{user.enrollment_number}_{user.full_name}.npy"
238
+ face_exists = os.path.exists(os.path.join(face_folder, face_file))
239
+ subject_attendance = get_subject_attendance(user)
240
+ teachers = User.query.filter_by(role='teacher', selected_class=user.selected_class).all()
241
+ return render_template('student_dashboard.html',
242
+ user=user,
243
+ selected_regular_subjects=json.loads(user.selected_regular_subjects or '[]'),
244
+ selected_specializations=json.loads(user.selected_specializations or '[]'),
245
+ selected_practicals=json.loads(user.selected_practicals or '[]'),
246
+ subject_attendance=subject_attendance,
247
+ face_exists=face_exists,
248
+ teachers=teachers)
249
+
250
+ @app.route('/api/update_profile', methods=['POST'])
251
+ def update_profile():
252
+ if 'user_id' not in session or session['role'] != 'student':
253
+ return jsonify({'success': False, 'message': 'Unauthorized'}), 401
254
+ data = request.json
255
+ user = db.session.get(User, session['user_id'])
256
+ user.full_name = data['full_name']
257
+ user.enrollment_number = data['enrollment_number']
258
+ user.email = data['email']
259
+ user.student_number = data['student_number']
260
+ user.parent_number = data['parent_number']
261
+ user.parent_email = data['parent_email']
262
+ db.session.commit()
263
+ return jsonify({'success': True, 'message': 'Profile updated'})
264
+
265
+ @app.route('/api/select_class', methods=['POST'])
266
+ def api_select_class():
267
+ if 'user_id' not in session or session['role'] != 'student':
268
+ return jsonify({'success': False}), 401
269
+ data = request.json
270
+ cls = data.get('class')
271
+ user = db.session.get(User, session['user_id'])
272
+ user.selected_class = cls
273
+ db.session.commit()
274
+ return jsonify({'success': True})
275
+
276
+ @app.route('/api/update_regular_subjects', methods=['POST'])
277
+ def update_regular_subjects():
278
+ if 'user_id' not in session:
279
+ return jsonify(success=False), 401
280
+ subjects = request.json.get('subjects')
281
+ u = db.session.get(User, session['user_id'])
282
+ u.selected_regular_subjects = json.dumps(subjects)
283
+ db.session.commit()
284
+ return jsonify(success=True)
285
+
286
+ @app.route('/api/update_specializations', methods=['POST'])
287
+ def update_specializations():
288
+ if 'user_id' not in session:
289
+ return jsonify(success=False), 401
290
+ subjects = request.json.get('subjects')
291
+ u = db.session.get(User, session['user_id'])
292
+ u.selected_specializations = json.dumps(subjects)
293
+ db.session.commit()
294
+ return jsonify(success=True)
295
+
296
+ @app.route('/api/update_batch', methods=['POST'])
297
+ def update_batch():
298
+ if 'user_id' not in session:
299
+ return jsonify(success=False), 401
300
+ batch = request.json.get('batch')
301
+ u = db.session.get(User, session['user_id'])
302
+ u.selected_batch = batch
303
+ db.session.commit()
304
+ return jsonify(success=True)
305
+
306
+ @app.route('/api/update_practicals', methods=['POST'])
307
+ def update_practicals():
308
+ if 'user_id' not in session:
309
+ return jsonify(success=False), 401
310
+ practs = request.json.get('practicals')
311
+ u = db.session.get(User, session['user_id'])
312
+ u.selected_practicals = json.dumps(practs)
313
+ db.session.commit()
314
+ return jsonify(success=True)
315
+
316
+ def get_excel_filename(class_name):
317
+ if class_name:
318
+ return f"{class_name}_timetable.xlsx"
319
+ return "timetable.xlsx"
320
+
321
+ @app.route('/timetable')
322
+ def timetable_page():
323
+ if 'user_id' not in session or session['role'] != 'teacher':
324
+ return redirect(url_for('index'))
325
+ return render_template('timetable.html')
326
+
327
+ @app.route('/add_config', methods=['POST'])
328
+ def add_config():
329
+ data = request.get_json()
330
+ class_name = data.get("class")
331
+ config_type = data.get("type")
332
+ name = data.get("name")
333
+ if not (class_name and config_type and name):
334
+ return jsonify({"error": "Missing parameters"}), 400
335
+ conn = get_db_connection()
336
+ conn.execute('INSERT INTO teacher_config (class_name, config_type, name) VALUES (?, ?, ?)',
337
+ (class_name, config_type, name))
338
+ conn.commit()
339
+ conn.close()
340
+ return jsonify({"message": "Added successfully"})
341
+
342
+ @app.route('/delete_config', methods=['POST'])
343
+ def delete_config():
344
+ data = request.get_json()
345
+ class_name = data.get("class")
346
+ config_type = data.get("type")
347
+ name = data.get("name")
348
+ if not (class_name and config_type and name):
349
+ return jsonify({"error": "Missing parameters"}), 400
350
+ conn = get_db_connection()
351
+ conn.execute('DELETE FROM teacher_config WHERE class_name=? AND config_type=? AND name=?',
352
+ (class_name, config_type, name))
353
+ conn.commit()
354
+ conn.close()
355
+ return jsonify({"message": "Deleted successfully"})
356
+
357
+ @app.route('/get_config', methods=['GET'])
358
+ def get_config():
359
+ class_name = request.args.get("class")
360
+ config_type = request.args.get("type")
361
+ if not (class_name and config_type):
362
+ return jsonify([])
363
+ conn = get_db_connection()
364
+ configs = conn.execute('SELECT name FROM teacher_config WHERE class_name=? AND config_type=?',
365
+ (class_name, config_type)).fetchall()
366
+ conn.close()
367
+ return jsonify([row["name"] for row in configs])
368
+
369
+ @app.route('/load', methods=['GET'])
370
+ def load_timetable():
371
+ class_name = request.args.get('class')
372
+ filename = get_excel_filename(class_name)
373
+ if not os.path.exists(filename):
374
+ return jsonify({})
375
+ df = pd.read_excel(filename, index_col=0)
376
+ return df.to_json()
377
+
378
+ @app.route('/save', methods=['POST'])
379
+ def save_timetable():
380
+ payload = request.get_json()
381
+ teacher_class = payload.get("class", "")
382
+ data = payload.get("timetable", {})
383
+ filename = get_excel_filename(teacher_class)
384
+ df = pd.DataFrame(data)
385
+ df.to_excel(filename)
386
+ return jsonify({'message': f'Saved successfully to {filename}!'})
387
+
388
+ @app.route('/download_excel', methods=['GET'])
389
+ def download_excel():
390
+ class_name = request.args.get('class')
391
+ filename = get_excel_filename(class_name)
392
+ if not os.path.exists(filename):
393
+ return "File not found", 404
394
+ return send_file(filename, as_attachment=True)
395
+
396
+ def distance(v1, v2):
397
+ return np.sqrt(((v1 - v2) ** 2).sum())
398
+
399
+ def knn(train, test, k=5):
400
+ dist = []
401
+ for i in range(train.shape[0]):
402
+ ix = train[i, :-1]
403
+ iy = train[i, -1]
404
+ d = distance(test, ix)
405
+ dist.append([d, iy])
406
+ dk = sorted(dist, key=lambda x: x[0])[:k]
407
+ labels = np.array(dk)[:, -1]
408
+ return np.unique(labels, return_counts=True)[0][0]
409
+
410
+ class AttendanceSystem:
411
+ def __init__(self, class_name):
412
+ self.class_name = class_name
413
+ self.main_file = os.path.join(attendance_path, f"{class_name}.xlsx")
414
+ self.pending_file = os.path.join(attendance_path, f"{class_name}_pending.xlsx")
415
+ self.columns = ["Enrollment", "Name", "Date", "Day", "CheckIn", "CheckOut"]
416
+
417
+ for f in [self.main_file, self.pending_file]:
418
+ if not os.path.exists(f):
419
+ pd.DataFrame(columns=self.columns).to_excel(f, index=False)
420
+
421
+ def checkin(self, enrollment, name, pin):
422
+ today = datetime.now().strftime("%Y-%m-%d")
423
+ correct_pin = get_daily_pin(self.class_name)
424
+ if pin != correct_pin:
425
+ return False, "Invalid PIN."
426
+
427
+ now = datetime.now()
428
+ checkin_time = now.strftime("%H.%M")
429
+
430
+ df_main = pd.read_excel(self.main_file)
431
+ if ((df_main["Enrollment"] == enrollment) & (df_main["Date"] == today)).any():
432
+ return False, "You have already checked in and checked out today."
433
+
434
+ df_pending = pd.read_excel(self.pending_file)
435
+ if ((df_pending["Enrollment"] == enrollment) & (df_pending["Date"] == today)).any():
436
+ return False, "You have already checked in today. Please check out first."
437
+
438
+ if now.time() < datetime.strptime("08:00", "%H:%M").time():
439
+ return False, "Check-in starts at 8 AM."
440
+
441
+ day_name = now.strftime("%A")
442
+ new_entry = pd.DataFrame([[enrollment, name, today, day_name, checkin_time, ""]],
443
+ columns=self.columns)
444
+ df_pending = pd.concat([df_pending, new_entry], ignore_index=True)
445
+ df_pending.to_excel(self.pending_file, index=False)
446
+
447
+ return True, "Check-in successful. Please remember to check out later."
448
+
449
+ def checkout(self, enrollment):
450
+ now = datetime.now()
451
+ today = now.strftime("%Y-%m-%d")
452
+ checkout_time = now.strftime("%H.%M")
453
+
454
+ df_pending = pd.read_excel(self.pending_file)
455
+ df_main = pd.read_excel(self.main_file)
456
+
457
+ has_main = ((df_main["Enrollment"] == enrollment) & (df_main["Date"] == today)).any()
458
+ has_pending = ((df_pending["Enrollment"] == enrollment) & (df_pending["Date"] == today)).any()
459
+
460
+ if not has_pending and has_main:
461
+ return False, "You have already checked out today."
462
+
463
+ if not has_pending:
464
+ return False, "No pending check-in found. Please check in first."
465
+
466
+ if now.time() > datetime.strptime("18:00", "%H:%M").time():
467
+ return False, "You missed check-out time. Contact your class teacher."
468
+
469
+ idx = df_pending[(df_pending["Enrollment"] == enrollment) & (df_pending["Date"] == today)].index[0]
470
+ pending_entry = df_pending.loc[idx]
471
+
472
+ new_row = {
473
+ "Enrollment": pending_entry["Enrollment"],
474
+ "Name": pending_entry["Name"],
475
+ "Date": today,
476
+ "Day": pending_entry["Day"],
477
+ "CheckIn": pending_entry["CheckIn"],
478
+ "CheckOut": checkout_time
479
+ }
480
+ df_main = pd.concat([df_main, pd.DataFrame([new_row])], ignore_index=True)
481
+ df_main.to_excel(self.main_file, index=False)
482
+
483
+ df_pending = df_pending.drop(idx)
484
+ df_pending.to_excel(self.pending_file, index=False)
485
+
486
+ return True, "Check-out successful. Attendance recorded."
487
+
488
+ import pandas as pd
489
+ from datetime import datetime, time
490
+
491
+ def parse_time(val):
492
+ if pd.isna(val):
493
+ return None
494
+ s = str(val).replace('.', ':')
495
+ if ':' not in s and len(s) >= 3 and s.isdigit():
496
+ s = s[:-2] + ':' + s[-2:]
497
+ try:
498
+ return datetime.strptime(s, '%H:%M').time()
499
+ except ValueError:
500
+ return datetime.strptime(s, '%I:%M').time()
501
+
502
+ def parse_timetable_time(time_str):
503
+ time_str = time_str.replace('.', ':')
504
+ hour, minute = map(int, time_str.split(':'))
505
+ if 1 <= hour <= 5:
506
+ hour += 12
507
+ return time(hour, minute)
508
+
509
+ def get_slot_type(content):
510
+ content = "" if pd.isna(content) else str(content)
511
+ if "Batch" in content:
512
+ return "practical"
513
+ elif "<br>" in content:
514
+ return "specialization"
515
+ elif content in ["Short Break", "Lunch Break"]:
516
+ return "break"
517
+ elif content.strip() == "":
518
+ return "break"
519
+ else:
520
+ return "regular"
521
+
522
+ def parse_practical(content):
523
+ lines = content.split('<br>')
524
+ batch_subjects = {}
525
+ for line in lines:
526
+ if ':' in line:
527
+ batch, subject = line.split(':')
528
+ batch = batch.strip().replace('Batch ', '')
529
+ subject = subject.strip()
530
+ batch_subjects[batch] = subject
531
+ return batch_subjects
532
+
533
+ def parse_specialization(content):
534
+ return [subj.strip() for subj in content.split('<br>')]
535
+
536
+ def get_slots(df, day):
537
+ row = df.loc[day]
538
+ slots = []
539
+ for col in df.columns:
540
+ start_str, end_str = col.split(" - ")
541
+ start_time = parse_timetable_time(start_str)
542
+ end_time = parse_timetable_time(end_str)
543
+ content = row[col]
544
+ if pd.isna(content) or str(content).strip() == "":
545
+ continue
546
+ slots.append({
547
+ "start": start_time,
548
+ "end": end_time,
549
+ "content": str(content)
550
+ })
551
+ return slots
552
+
553
+ def get_subject_attendance(user):
554
+ class_name = user.selected_class
555
+ attendance_file = os.path.join(attendance_path, f"{class_name}.xlsx")
556
+ if not os.path.exists(attendance_file):
557
+ return {'regular': {}, 'specialization': {}, 'practical': {}}
558
+ df_att = pd.read_excel(attendance_file)
559
+ all_dates = df_att['Date'].unique()
560
+ timetable_file = f"{class_name}_timetable.xlsx"
561
+ if not os.path.exists(timetable_file):
562
+ return {'regular': {}, 'specialization': {}, 'practical': {}}
563
+ df_timetable = pd.read_excel(timetable_file, index_col=0)
564
+ regular_counts = {subj: {'attendance': 0, 'total': 0} for subj in json.loads(user.selected_regular_subjects or '[]')}
565
+ spec_counts = {subj: {'attendance': 0, 'total': 0} for subj in json.loads(user.selected_specializations or '[]')}
566
+ prac_counts = {subj: {'attendance': 0, 'total': 0} for subj in json.loads(user.selected_practicals or '[]')}
567
+ for date in all_dates:
568
+ day = datetime.strptime(date, '%Y-%m-%d').strftime('%A')
569
+ if day not in df_timetable.index:
570
+ continue
571
+ slots = get_slots(df_timetable, day)
572
+ student_att = df_att[(df_att['Enrollment'] == user.enrollment_number) & (df_att['Date'] == date)]
573
+ if not student_att.empty:
574
+ check_in_str = student_att['CheckIn'].values[0]
575
+ check_out_str = student_att['CheckOut'].values[0]
576
+ check_in_time = parse_time(check_in_str)
577
+ check_out_time = parse_time(check_out_str)
578
+ else:
579
+ check_in_time = None
580
+ check_out_time = None
581
+ for slot in slots:
582
+ slot_start = slot['start']
583
+ slot_end = slot['end']
584
+ content = slot['content']
585
+ slot_type = get_slot_type(content)
586
+ if slot_type == 'regular':
587
+ subject = content
588
+ if subject in regular_counts:
589
+ regular_counts[subject]['total'] += 1
590
+ if check_in_time and check_in_time <= slot_start and check_out_time >= slot_end:
591
+ regular_counts[subject]['attendance'] += 1
592
+ elif slot_type == 'specialization':
593
+ subjects = parse_specialization(content)
594
+ selected_specs = json.loads(user.selected_specializations or '[]')
595
+ for selected_spec in selected_specs:
596
+ if selected_spec in subjects:
597
+ spec_counts[selected_spec]['total'] += 1
598
+ if check_in_time and check_in_time <= slot_start and check_out_time >= slot_end:
599
+ spec_counts[selected_spec]['attendance'] += 1
600
+ elif slot_type == 'practical':
601
+ batch_subjects = parse_practical(content)
602
+ if user.selected_batch in batch_subjects:
603
+ subject = batch_subjects[user.selected_batch]
604
+ if subject in prac_counts:
605
+ prac_counts[subject]['total'] += 1
606
+ if check_in_time and check_in_time <= slot_start and check_out_time >= slot_end:
607
+ prac_counts[subject]['attendance'] += 1
608
+ return {
609
+ 'regular': regular_counts,
610
+ 'specialization': spec_counts,
611
+ 'practical': prac_counts
612
+ }
613
+
614
+ @app.route('/check_attendance', methods=['GET'])
615
+ def check_attendance():
616
+ students = User.query.filter_by(role='student').all()
617
+ for student in students:
618
+ subject_attendance = get_subject_attendance(student)
619
+ total_attendance = 0
620
+ total_possible = 0
621
+ for subj_type in ['regular', 'specialization', 'practical']:
622
+ for subj, counts in subject_attendance[subj_type].items():
623
+ total_attendance += counts['attendance']
624
+ total_possible += counts['total']
625
+ if total_possible > 0:
626
+ overall_percentage = (total_attendance / total_possible) * 100
627
+ if overall_percentage < 75:
628
+ details = {
629
+ 'regular': [],
630
+ 'specialization': [],
631
+ 'practical': []
632
+ }
633
+ for subj_type in ['regular', 'specialization', 'practical']:
634
+ for subj, counts in subject_attendance[subj_type].items():
635
+ percentage = (counts['attendance'] / counts['total'] * 100) if counts['total'] > 0 else 0
636
+ details[subj_type].append({
637
+ 'subject': subj,
638
+ 'attendance_count': counts['attendance'],
639
+ 'total_count': counts['total'],
640
+ 'percentage': round(percentage, 2)
641
+ })
642
+ send_attendance_email(student, overall_percentage, details)
643
+ return 'Attendance check completed'
644
+
645
+ def send_attendance_email(student, overall_percentage, details):
646
+ subject = "Low Attendance Alert"
647
+ recipients = [student.email]
648
+ if student.parent_email:
649
+ recipients.append(student.parent_email)
650
+ body = f"""
651
+ Dear {student.full_name},
652
+
653
+ Your overall attendance average is {overall_percentage:.2f}%, which is below the required 75%.
654
+
655
+ Here are your attendance details:
656
+
657
+ Regular Subjects:
658
+ """
659
+ for subj in details['regular']:
660
+ body += f"- {subj['subject']}: {subj['attendance_count']}/{subj['total_count']} ({subj['percentage']}%\n"
661
+ body += "\nSpecialization Subjects:\n"
662
+ for subj in details['specialization']:
663
+ body += f"- {subj['subject']}: {subj['attendance_count']}/{subj['total_count']} ({subj['percentage']}%\n"
664
+ body += "\nPractical Subjects:\n"
665
+ for subj in details['practical']:
666
+ body += f"- {subj['subject']}: {subj['attendance_count']}/{subj['total_count']} ({subj['percentage']}%\n"
667
+ body += "\nPlease take necessary actions to improve your attendance.\n\nBest regards,\nAttendance System"
668
+
669
+ msg = Message(subject, recipients=recipients, body=body)
670
+ mail.send(msg)
671
+
672
+ @app.route('/mark_attendance')
673
+ def mark_attendance_page():
674
+ return render_template('mark.html')
675
+
676
+ @app.route('/register_face')
677
+ def register_face_page():
678
+ if 'user_id' not in session or session['role'] != 'student':
679
+ return redirect(url_for('index'))
680
+ return render_template('register.html')
681
+
682
+ @app.route('/api/register_face', methods=['POST'])
683
+ def register_face():
684
+ data = request.json
685
+ class_name = data['class_name']
686
+ name = data['name']
687
+ enrollment = data['enrollment']
688
+ images = data['images']
689
+
690
+ class_folder = os.path.join(dataset_path, class_name)
691
+ os.makedirs(class_folder, exist_ok=True)
692
+
693
+ face_data = []
694
+ for img_data in images:
695
+ img_bytes = base64.b64decode(img_data.split(",")[1])
696
+ np_arr = np.frombuffer(img_bytes, np.uint8)
697
+ img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
698
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
699
+ gray = cv2.equalizeHist(gray)
700
+ faces = face_cascade.detectMultiScale(gray, 1.3, 5)
701
+ for (x, y, w, h) in faces[:1]:
702
+ face = img[y:y+h, x:x+w]
703
+ face = cv2.resize(face, (100, 100))
704
+ face_data.append(face.flatten())
705
+ flipped = cv2.flip(face, 1)
706
+ face_data.append(flipped.flatten())
707
+
708
+ if face_data:
709
+ face_data = np.array(face_data)
710
+ filename = f"{enrollment}_{name}.npy"
711
+ np.save(os.path.join(class_folder, filename), face_data)
712
+ return jsonify({"status": "success", "message": f"{len(face_data)} faces saved"})
713
+ return jsonify({"status": "fail", "message": "No faces detected"})
714
+
715
+ @app.route('/api/identify_face', methods=['POST'])
716
+ def identify_face():
717
+ data = request.json
718
+ class_name = data['class_name']
719
+ img_data = data['image']
720
+ img_bytes = base64.b64decode(img_data.split(",")[1])
721
+ np_arr = np.frombuffer(img_bytes, np.uint8)
722
+ img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
723
+
724
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
725
+ faces = face_cascade.detectMultiScale(gray, 1.3, 5)
726
+
727
+ class_folder = os.path.join(dataset_path, class_name)
728
+ if not os.path.exists(class_folder):
729
+ return jsonify({"status": "fail", "message": "No data for this class"})
730
+
731
+ face_data = []
732
+ labels = []
733
+ names = {}
734
+ class_id = 0
735
+ for file in os.listdir(class_folder):
736
+ if file.endswith('.npy'):
737
+ data_arr = np.load(os.path.join(class_folder, file))
738
+ face_data.append(data_arr)
739
+ parts = file[:-4].split('_', 1)
740
+ labels.extend([class_id] * data_arr.shape[0])
741
+ names[class_id] = {'enrollment': parts[0], 'name': parts[1]}
742
+ class_id += 1
743
+
744
+ if not face_data:
745
+ return jsonify({"status": "fail", "message": "No trained data found"})
746
+
747
+ X_train = np.concatenate(face_data, axis=0)
748
+ y_train = np.array(labels).reshape(-1, 1)
749
+ trainset = np.hstack((X_train, y_train))
750
+
751
+ for (x, y, w, h) in faces[:1]:
752
+ face = img[y:y+h, x:x+w]
753
+ face = cv2.resize(face, (100, 100)).flatten()
754
+ pred_id = knn(trainset, face)
755
+ info = names.get(pred_id)
756
+ if info:
757
+ return jsonify({
758
+ "status": "success",
759
+ "name": info['name'],
760
+ "enrollment": info['enrollment']
761
+ })
762
+ return jsonify({"status": "fail", "message": "Face not recognized"})
763
+
764
+ @app.route('/api/checkin', methods=['POST'])
765
+ def api_checkin():
766
+ data = request.json
767
+ class_name = data['class_name']
768
+ enrollment = data['enrollment']
769
+ name = data['name']
770
+ pin = data.get('pin') # Get PIN from request
771
+ attendance = AttendanceSystem(class_name)
772
+ ok, msg = attendance.checkin(enrollment, name, pin)
773
+ status = "success" if ok else "fail"
774
+ return jsonify({"status": status, "message": msg})
775
+
776
+ @app.route('/api/checkout', methods=['POST'])
777
+ def api_checkout():
778
+ data = request.json
779
+ class_name = data['class_name']
780
+ enrollment = data['enrollment']
781
+ attendance = AttendanceSystem(class_name)
782
+ ok, msg = attendance.checkout(enrollment)
783
+ status = "success" if ok else "fail"
784
+ return jsonify({"status": status, "message": msg})
785
+
786
+ # New endpoint to get the overall attendance average
787
+ @app.route('/api/get_attendance_average', methods=['POST'])
788
+ def get_attendance_average():
789
+ data = request.json
790
+ class_name = data['class_name']
791
+ enrollment = data['enrollment']
792
+ user = User.query.filter_by(enrollment_number=enrollment, selected_class=class_name, role='student').first()
793
+ if not user:
794
+ return jsonify({'error': 'Student not found'}), 404
795
+ subject_attendance = get_subject_attendance(user)
796
+ total_attendance = 0
797
+ total_possible = 0
798
+ for subj_type in ['regular', 'specialization', 'practical']:
799
+ for subj, counts in subject_attendance[subj_type].items():
800
+ total_attendance += counts['attendance']
801
+ total_possible += counts['total']
802
+ if total_possible > 0:
803
+ overall_percentage = (total_attendance / total_possible) * 100
804
+ else:
805
+ overall_percentage = 0
806
+ return jsonify({'overall_percentage': round(overall_percentage, 2)})
807
+
808
+ if __name__ == '__main__':
809
+ app.run(host='0.0.0.0', port=5000, debug=True)
attendance_data/SY-21.xlsx ADDED
Binary file (4.96 kB). View file
 
attendance_data/SY-21_pending.xlsx ADDED
Binary file (4.96 kB). View file
 
attendance_data/SY-22.xlsx ADDED
Binary file (5.2 kB). View file
 
attendance_data/SY-22_pending.xlsx ADDED
Binary file (5.15 kB). View file
 
config.db ADDED
Binary file (12.3 kB). View file
 
face_dataset/SY-21/ADT24SOCBD145_yash.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3d32dd3cbe87bddf2b83fa452863fc84123b058dde74af69df1020ee23943ceb
3
+ size 7140128
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
instance/users.db ADDED
Binary file (28.7 kB). View file
 
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/logo.PNG ADDED
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
+ '8.45 - 9.40', // index 0
12
+ '9.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 - 1.40', // index 5 -> Lunch Break (fixed)
17
+ '1.40 - 2.35', // index 6
18
+ '2.35 - 3.30', // index 7
19
+ '3.30 - 3.40', // index 8 -> Short Break (fixed)
20
+ '3.40 - 4.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: '8.45 - 10.35', indices: [0, 1] },
32
+ { label: '10.50 - 12.40', indices: [3, 4] },
33
+ { label: '1.40 - 3.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
+ }
templates/index.html ADDED
@@ -0,0 +1,647 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <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
+ :root {
11
+ --primary: #1a4a7c;
12
+ --secondary: #3498db;
13
+ --success: #2ecc71;
14
+ --danger: #e74c3c;
15
+ --warning: #f39c12;
16
+ --info: #2980b9;
17
+ --light: #ecf0f1;
18
+ --dark: #2c3e50;
19
+ --white: #ffffff;
20
+ --gray: #bdc3c7;
21
+ --border-radius: 8px;
22
+ --font-main: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
23
+ }
24
+ * {
25
+ box-sizing: border-box;
26
+ margin: 0;
27
+ padding: 0;
28
+ font-family: var(--font-main);
29
+ }
30
+ body {
31
+ background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%);
32
+ min-height: 100vh;
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ padding: 40px 20px;
37
+ }
38
+ header {
39
+ width: 100%;
40
+ background: var(--white);
41
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
42
+ padding: 20px 0;
43
+ margin-bottom: 40px;
44
+ border-bottom: 3px solid var(--primary);
45
+ }
46
+ header .container {
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ flex-wrap: wrap;
51
+ }
52
+ header img {
53
+ height: 70px;
54
+ margin-right: 20px;
55
+ border-radius: 5px;
56
+ }
57
+ header h1 {
58
+ font-size: 1.8rem;
59
+ font-weight: 600;
60
+ color: var(--primary);
61
+ margin: 0;
62
+ text-align: center;
63
+ }
64
+ .login-box {
65
+ background-color: var(--white);
66
+ border-radius: var(--border-radius);
67
+ padding: 40px;
68
+ width: 100%;
69
+ max-width: 500px;
70
+ box-shadow: 0 10px 25px rgba(0,0,0,0.15);
71
+ margin-bottom: 40px;
72
+ border-top: 4px solid var(--primary);
73
+ }
74
+ .login-box h2 {
75
+ color: var(--primary);
76
+ margin-bottom: 30px;
77
+ font-weight: 600;
78
+ text-align: center;
79
+ padding-bottom: 12px;
80
+ position: relative;
81
+ }
82
+ .login-box h2:after {
83
+ content: "";
84
+ position: absolute;
85
+ bottom: 0;
86
+ left: 50%;
87
+ transform: translateX(-50%);
88
+ width: 80px;
89
+ height: 3px;
90
+ background: linear-gradient(90deg, var(--primary), var(--secondary));
91
+ }
92
+ .section-buttons {
93
+ display: flex;
94
+ justify-content: space-between;
95
+ margin-bottom: 30px;
96
+ border-radius: var(--border-radius);
97
+ overflow: hidden;
98
+ box-shadow: 0 3px 10px rgba(0,0,0,0.1);
99
+ }
100
+ .section-buttons button {
101
+ flex: 1;
102
+ padding: 15px 0;
103
+ background-color: var(--light);
104
+ border: none;
105
+ cursor: pointer;
106
+ font-weight: 500;
107
+ color: var(--dark);
108
+ transition: all 0.3s;
109
+ border-right: 1px solid var(--gray);
110
+ }
111
+ .section-buttons button:last-child {
112
+ border-right: none;
113
+ }
114
+ .section-buttons button:hover {
115
+ background-color: #e6f0ff;
116
+ }
117
+ .section-buttons button.active {
118
+ background-color: var(--primary);
119
+ color: var(--white);
120
+ box-shadow: 0 4px 8px rgba(26, 74, 124, 0.3);
121
+ }
122
+ .form-section {
123
+ display: none;
124
+ margin-top: 20px;
125
+ }
126
+ .form-section h3 {
127
+ color: var(--primary);
128
+ margin-bottom: 20px;
129
+ font-weight: 500;
130
+ text-align: center;
131
+ }
132
+ .input-group {
133
+ margin-bottom: 20px;
134
+ position: relative;
135
+ }
136
+ .input-group label {
137
+ display: block;
138
+ margin-bottom: 8px;
139
+ color: var(--dark);
140
+ font-size: 15px;
141
+ font-weight: 500;
142
+ }
143
+ .input-group input {
144
+ width: 100%;
145
+ padding: 14px 15px 14px 40px;
146
+ border: 1px solid var(--gray);
147
+ border-radius: var(--border-radius);
148
+ font-size: 15px;
149
+ transition: all 0.3s;
150
+ background-color: var(--light);
151
+ }
152
+ .input-group input:focus {
153
+ border-color: var(--secondary);
154
+ outline: none;
155
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
156
+ background-color: var(--white);
157
+ }
158
+ .input-group i {
159
+ position: absolute;
160
+ left: 15px;
161
+ top: 38px;
162
+ color: var(--gray);
163
+ transition: color 0.3s;
164
+ }
165
+ .input-group input:focus + i {
166
+ color: var(--secondary);
167
+ }
168
+ /* Fixed Password Input Styling */
169
+ .password-group {
170
+ position: relative;
171
+ }
172
+ .password-group input {
173
+ padding-right: 40px;
174
+ }
175
+ .password-toggle {
176
+ position: absolute;
177
+ right: 53px;
178
+ top: 10px; /* Align with the input field */
179
+ cursor: pointer;
180
+ color: var(--gray);
181
+ font-size: 18px;
182
+ z-index: 10;
183
+ }
184
+ .password-toggle:hover {
185
+ color: var(--dark);
186
+ }
187
+ .form-section button, .modal-container button {
188
+ width: 100%;
189
+ padding: 15px;
190
+ border: none;
191
+ border-radius: var(--border-radius);
192
+ cursor: pointer;
193
+ font-size: 16px;
194
+ font-weight: 600;
195
+ color: var(--white);
196
+ background: linear-gradient(to right, var(--primary), var(--secondary));
197
+ transition: all 0.3s;
198
+ margin-top: 15px;
199
+ box-shadow: 0 4px 10px rgba(26, 74, 124, 0.3);
200
+ }
201
+ .form-section button:hover, .modal-container button:hover {
202
+ transform: translateY(-2px);
203
+ box-shadow: 0 6px 15px rgba(26, 74, 124, 0.4);
204
+ background: linear-gradient(to right, #155785, #2980b9);
205
+ }
206
+ .modal-overlay {
207
+ position: fixed;
208
+ top: 0; left: 0; right: 0; bottom: 0;
209
+ background-color: rgba(0,0,0,0.6);
210
+ display: none;
211
+ justify-content: center;
212
+ align-items: center;
213
+ z-index: 1000;
214
+ backdrop-filter: blur(3px);
215
+ }
216
+ .modal-container {
217
+ background: var(--white);
218
+ border-radius: var(--border-radius);
219
+ padding: 35px;
220
+ width: 100%;
221
+ max-width: 500px;
222
+ box-shadow: 0 10px 25px rgba(0,0,0,0.2);
223
+ border-top: 4px solid var(--primary);
224
+ animation: modalFadeIn 0.3s;
225
+ }
226
+ @keyframes modalFadeIn {
227
+ from { opacity: 0; transform: translateY(-20px); }
228
+ to { opacity: 1; transform: translateY(0); }
229
+ }
230
+ .close-button {
231
+ position: absolute;
232
+ top: 15px; right: 15px;
233
+ background: var(--light);
234
+ border: none;
235
+ font-size: 20px;
236
+ color: var(--dark);
237
+ cursor: pointer;
238
+ width: 30px;
239
+ height: 30px;
240
+ border-radius: 50%;
241
+ display: flex;
242
+ align-items: center;
243
+ justify-content: center;
244
+ transition: all 0.3s;
245
+ }
246
+ .close-button:hover {
247
+ background: var(--gray);
248
+ color: var(--primary);
249
+ transform: rotate(90deg);
250
+ }
251
+ .notes-card {
252
+ width: 100%;
253
+ margin-bottom: 20px;
254
+ border: none;
255
+ border-radius: var(--border-radius);
256
+ box-shadow: 0 8px 20px rgba(0,0,0,0.1);
257
+ overflow: hidden;
258
+ }
259
+ .notes-card .card-header {
260
+ background-color: var(--primary);
261
+ color: var(--white);
262
+ padding: 15px 20px;
263
+ font-weight: 600;
264
+ }
265
+ .notes-card .card-body {
266
+ padding: 25px;
267
+ }
268
+ .notes-card .card-body h5 {
269
+ font-weight: 600;
270
+ margin-top: 15px;
271
+ color: var(--primary);
272
+ display: flex;
273
+ align-items: center;
274
+ }
275
+ .notes-card .card-body h5 i {
276
+ margin-right: 10px;
277
+ color: var(--secondary);
278
+ }
279
+ .notes-card ul {
280
+ margin-top: 15px;
281
+ padding-left: 20px;
282
+ }
283
+ .notes-card ul li {
284
+ margin-bottom: 10px;
285
+ line-height: 1.6;
286
+ }
287
+ .notes-card a {
288
+ color: var(--secondary);
289
+ text-decoration: none;
290
+ transition: color 0.3s;
291
+ }
292
+ .notes-card a:hover {
293
+ color: var(--primary);
294
+ text-decoration: underline;
295
+ }
296
+ @media (max-width: 768px) {
297
+ header h1 { font-size: 1.4rem; margin-top: 10px; }
298
+ .login-box { padding: 25px; }
299
+ .section-buttons button { font-size: 14px; padding: 12px 0; }
300
+ .modal-container { padding: 25px; margin: 0 15px; }
301
+ }
302
+ @media (max-width: 576px) {
303
+ header img { height: 50px; }
304
+ header h1 { font-size: 1.2rem; }
305
+ }
306
+ .toast-container {
307
+ position: fixed;
308
+ bottom: 20px;
309
+ right: 20px;
310
+ z-index: 1001;
311
+ }
312
+ .toast {
313
+ background-color: rgba(26, 74, 124, 0.9);
314
+ color: var(--white);
315
+ padding: 15px 25px;
316
+ border-radius: var(--border-radius);
317
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
318
+ margin-top: 10px;
319
+ display: flex;
320
+ align-items: center;
321
+ animation: toastFadeIn 0.3s;
322
+ backdrop-filter: blur(5px);
323
+ }
324
+ .toast i {
325
+ margin-right: 10px;
326
+ font-size: 20px;
327
+ }
328
+ @keyframes toastFadeIn {
329
+ from { opacity: 0; transform: translateY(20px); }
330
+ to { opacity: 1; transform: translateY(0); }
331
+ }
332
+ </style>
333
+ </head>
334
+ <body>
335
+ <header>
336
+ <div class="container">
337
+ <img src="https://exam.mitapps.in/site/static/images/mit.jpg" alt="MIT-ADT Logo">
338
+ </div>
339
+ <br>
340
+ <h1>MIT Art, Design and Technology University, Pune</h1>
341
+ </header>
342
+ <div class="login-box">
343
+ <h2>Welcome to Attendance Portal</h2>
344
+ <div class="section-buttons">
345
+ <button id="teacherBtn" onclick="showForm('teacher')">
346
+ <i class="fas fa-chalkboard-teacher"></i> Teacher
347
+ </button>
348
+ <button id="studentBtn" onclick="showForm('student')">
349
+ <i class="fas fa-user-graduate"></i> Student
350
+ </button>
351
+ <button id="attendanceBtn" onclick="showForm('attendance')">
352
+ <i class="fas fa-clipboard-check"></i> Attendance
353
+ </button>
354
+ </div>
355
+ <div id="teacher" class="form-section">
356
+ <h3>Teacher Login</h3>
357
+ <div class="input-group">
358
+ <label for="teacherUsername">Username</label>
359
+ <input type="text" id="teacherUsername" placeholder="Enter your username" />
360
+ <i class="fas fa-user"></i>
361
+ </div>
362
+ <div class="input-group password-group">
363
+ <label for="teacherPassword">Password</label>
364
+ <input type="password" id="teacherPassword" placeholder="Enter your password" />
365
+ <i class="fas fa-lock"></i>
366
+ <span class="password-toggle" onclick="togglePassword('teacherPassword')">
367
+ <i class="fas fa-eye"></i>
368
+ </span>
369
+ </div>
370
+ <button onclick="attemptLogin('teacher')">
371
+ <i class="fas fa-sign-in-alt"></i> Login
372
+ </button>
373
+ <div class="link">
374
+ <a href="#" onclick="showSignup('Teacher'); return false;">Don't have an account? Create one</a>
375
+ </div>
376
+ </div>
377
+ <div id="student" class="form-section">
378
+ <h3>Student Login</h3>
379
+ <div class="input-group">
380
+ <label for="studentUsername">Username</label>
381
+ <input type="text" id="studentUsername" placeholder="Enter your username" />
382
+ <i class="fas fa-user"></i>
383
+ </div>
384
+ <div class="input-group password-group">
385
+ <label for="studentPassword">Password</label>
386
+ <input type="password" id="studentPassword" placeholder="Enter your password" />
387
+ <i class="fas fa-lock"></i>
388
+ <span class="password-toggle" onclick="togglePassword('studentPassword')">
389
+ <i class="fas fa-eye"></i>
390
+ </span>
391
+ </div>
392
+ <button onclick="attemptLogin('student')">
393
+ <i class="fas fa-sign-in-alt"></i> Login
394
+ </button>
395
+ <div class="link">
396
+ <a href="#" onclick="showSignup('Student'); return false;">Don't have an account? Create one</a>
397
+ </div>
398
+ </div>
399
+ <div id="attendance" class="form-section">
400
+ <h3>Mark Attendance</h3>
401
+ <button onclick="window.location.href='/mark_attendance'">
402
+ <i class="fas fa-clipboard-check"></i> Mark Attendance
403
+ </button>
404
+ </div>
405
+ </div>
406
+ <div class="modal-overlay" id="signupModal">
407
+ <div class="modal-container">
408
+ <button class="close-button" onclick="closeSignup()">×</button>
409
+ <h3 id="signupTitle">Create Account</h3>
410
+ <div class="input-group">
411
+ <label for="signupFullname">Full Name</label>
412
+ <input type="text" id="signupFullname" placeholder="Enter your full name" />
413
+ <i class="fas fa-user"></i>
414
+ </div>
415
+ <div class="input-group" id="phoneNumberGroup" style="display: none;">
416
+ <label for="signupPhoneNumber">Phone Number</label>
417
+ <input type="text" id="signupPhoneNumber" placeholder="Enter your phone number" />
418
+ <i class="fas fa-phone"></i>
419
+ </div>
420
+ <div class="input-group" id="enrollmentGroup" style="display: none;">
421
+ <label for="signupEnrollment">Enrollment Number</label>
422
+ <input type="text" id="signupEnrollment" placeholder="e.g, ADT24SOCBD053" />
423
+ <i class="fas fa-id-card"></i>
424
+ </div>
425
+ <div class="input-group" id="studentNumberGroup" style="display: none;">
426
+ <label for="signupStudentNumber">Student Number</label>
427
+ <input type="text" id="signupStudentNumber" placeholder="Enter Student Number" />
428
+ <i class="fas fa-id-badge"></i>
429
+ </div>
430
+ <div class="input-group" id="parentNumberGroup" style="display: none;">
431
+ <label for="signupParentNumber">Parent Number</label>
432
+ <input type="text" id="signupParentNumber" placeholder="Enter Parent Mobile Number" />
433
+ <i class="fas fa-phone"></i>
434
+ </div>
435
+ <div class="input-group" id="parentEmailGroup" style="display: none;">
436
+ <label for="signupParentEmail">Parent Email</label>
437
+ <input type="email" id="signupParentEmail" placeholder="Enter Parent Email ID" />
438
+ <i class="fas fa-envelope-open"></i>
439
+ </div>
440
+ <div class="input-group">
441
+ <label for="signupEmail">Email Address</label>
442
+ <input type="email" id="signupEmail" placeholder="Enter your email address" />
443
+ <i class="fas fa-envelope"></i>
444
+ </div>
445
+ <div class="input-group">
446
+ <label for="signupUsername">Username</label>
447
+ <input type="text" id="signupUsername" placeholder="Choose a username" />
448
+ <i class="fas fa-user-circle"></i>
449
+ </div>
450
+ <div class="input-group password-group">
451
+ <label for="signupPassword">Password</label>
452
+ <input type="password" id="signupPassword" placeholder="Choose a password" />
453
+ <i class="fas fa-lock"></i>
454
+ <span class="password-toggle" onclick="togglePassword('signupPassword')">
455
+ <i class="fas fa-eye"></i>
456
+ </span>
457
+ </div>
458
+ <div class="input-group password-group">
459
+ <label for="confirmPassword">Confirm Password</label>
460
+ <input type="password" id="confirmPassword" placeholder="Confirm your password" />
461
+ <i class="fas fa-check-circle"></i>
462
+ <span class="password-toggle" onclick="togglePassword('confirmPassword')">
463
+ <i class="fas fa-eye"></i>
464
+ </span>
465
+ </div>
466
+ <button onclick="createAccount()">
467
+ <i class="fas fa-user-plus"></i> Create Account
468
+ </button>
469
+ <div class="link">
470
+ <a href="#" onclick="closeSignup(); return false;">Already have an account? Login</a>
471
+ </div>
472
+ </div>
473
+ </div>
474
+ <div class="toast-container" id="toastContainer"></div>
475
+ <div class="container">
476
+ <div class="card notes-card">
477
+ <div class="card-header">
478
+ <i class="fas fa-info-circle"></i> Important Information
479
+ </div>
480
+ <div class="card-body">
481
+ <h5><i class="fas fa-exclamation-circle"></i> Important Note:</h5>
482
+ <ul>
483
+ <li>Upload a clear front-facing photo in your student profile to activate Face Recognition Attendance.</li>
484
+ <li>Be present in class on time – attendance is auto-marked using your face.</li>
485
+ <li>Manual attendance is not allowed – ensure your face is visible and unobstructed.</li>
486
+ <li>Minimum 75% attendance (via face recognition) is mandatory for exam eligibility.</li>
487
+ </ul>
488
+ <h5><i class="fas fa-link"></i> Weblinks:</h5>
489
+ <ul>
490
+ <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>
491
+ <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>
492
+ <li><i class="fas fa-external-link-alt"></i> D N: <a href="#" target="_blank">ni</a></li>
493
+ <li><i class="fas fa-external-link-alt"></i> A P: <a href="#" target="_blank">ain</a></li>
494
+ </ul>
495
+ </div>
496
+ </div>
497
+ </div>
498
+ <script>
499
+ document.addEventListener('DOMContentLoaded', () => showForm('teacher'));
500
+ function showForm(section) {
501
+ document.querySelectorAll('.form-section').forEach(el => el.style.display = 'none');
502
+ document.querySelectorAll('.section-buttons button').forEach(btn => btn.classList.remove('active'));
503
+ document.getElementById(section).style.display = 'block';
504
+ document.getElementById(section + 'Btn').classList.add('active');
505
+ }
506
+ function showSignup(role) {
507
+ window.currentSignupRole = role;
508
+ document.getElementById('signupTitle').textContent = role + " Signup";
509
+ const enrollmentGroup = document.getElementById('enrollmentGroup');
510
+ const studentNumberGroup = document.getElementById('studentNumberGroup');
511
+ const parentNumberGroup = document.getElementById('parentNumberGroup');
512
+ const parentEmailGroup = document.getElementById('parentEmailGroup');
513
+ const phoneNumberGroup = document.getElementById('phoneNumberGroup');
514
+ if (role === 'Student') {
515
+ enrollmentGroup.style.display = 'block';
516
+ studentNumberGroup.style.display = 'block';
517
+ parentNumberGroup.style.display = 'block';
518
+ parentEmailGroup.style.display = 'block';
519
+ phoneNumberGroup.style.display = 'none';
520
+ } else if (role === 'Teacher') {
521
+ enrollmentGroup.style.display = 'none';
522
+ studentNumberGroup.style.display = 'none';
523
+ parentNumberGroup.style.display = 'none';
524
+ parentEmailGroup.style.display = 'none';
525
+ phoneNumberGroup.style.display = 'block';
526
+ }
527
+ document.getElementById('signupModal').style.display = 'flex';
528
+ }
529
+ function closeSignup() {
530
+ document.getElementById('signupModal').style.display = 'none';
531
+ }
532
+ function createAccount() {
533
+ const fullname = document.getElementById('signupFullname').value;
534
+ const email = document.getElementById('signupEmail').value;
535
+ const username = document.getElementById('signupUsername').value;
536
+ const password = document.getElementById('signupPassword').value;
537
+ const confirmPassword = document.getElementById('confirmPassword').value;
538
+ const role = window.currentSignupRole.toLowerCase();
539
+ let data = {
540
+ full_name: fullname,
541
+ email: email,
542
+ username: username,
543
+ password: password,
544
+ role: role,
545
+ };
546
+ if (role === 'student') {
547
+ data.enrollment_number = document.getElementById('signupEnrollment').value;
548
+ data.student_number = document.getElementById('signupStudentNumber').value;
549
+ data.parent_number = document.getElementById('signupParentNumber').value;
550
+ data.parent_email = document.getElementById('signupParentEmail').value;
551
+ if (!data.enrollment_number || !data.student_number || !data.parent_number || !data.parent_email) {
552
+ showToast('Please fill all student details!', 'error');
553
+ return;
554
+ }
555
+ } else if (role === 'teacher') {
556
+ data.phone_number = document.getElementById('signupPhoneNumber').value;
557
+ if (!data.phone_number) {
558
+ showToast('Please enter phone number!', 'error');
559
+ return;
560
+ }
561
+ }
562
+ if (!fullname || !email || !username || !password || !confirmPassword) {
563
+ showToast('Please fill all fields!', 'error');
564
+ return;
565
+ }
566
+ if (password !== confirmPassword) {
567
+ showToast('Passwords do not match!', 'error');
568
+ return;
569
+ }
570
+ fetch('/signup', {
571
+ method: 'POST',
572
+ headers: { 'Content-Type': 'application/json' },
573
+ body: JSON.stringify(data),
574
+ })
575
+ .then(response => response.json())
576
+ .then(data => {
577
+ if (data.success) {
578
+ showToast('Account created successfully!', 'success');
579
+ setTimeout(() => closeSignup(), 1500);
580
+ } else {
581
+ showToast(data.message, 'error');
582
+ }
583
+ })
584
+ .catch(() => showToast('An error occurred. Please try again.', 'error'));
585
+ }
586
+ function attemptLogin(type) {
587
+ const username = document.getElementById(type + 'Username').value;
588
+ const password = document.getElementById(type + 'Password').value;
589
+ if (!username || !password) {
590
+ showToast('Please enter both username and password', 'error');
591
+ return;
592
+ }
593
+ const data = { username, password, role: type };
594
+ fetch('/login', {
595
+ method: 'POST',
596
+ headers: { 'Content-Type': 'application/json' },
597
+ body: JSON.stringify(data),
598
+ })
599
+ .then(response => response.json())
600
+ .then(data => {
601
+ if (data.success) {
602
+ showToast('Login successful! Redirecting...', 'success');
603
+ setTimeout(() => window.location.href = data.redirect, 1500);
604
+ } else {
605
+ showToast(data.message, 'error');
606
+ }
607
+ })
608
+ .catch(() => showToast('An error occurred. Please try again.', 'error'));
609
+ }
610
+ function showToast(message, type = 'info') {
611
+ const toastContainer = document.getElementById('toastContainer');
612
+ const toast = document.createElement('div');
613
+ toast.className = 'toast';
614
+ let icon = type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-triangle' : 'info-circle';
615
+ toast.innerHTML = `<i class="fas fa-${icon}"></i> ${message}`;
616
+ toastContainer.appendChild(toast);
617
+ setTimeout(() => {
618
+ toast.style.opacity = '0';
619
+ toast.style.transform = 'translateY(20px)';
620
+ toast.style.transition = 'all 0.3s';
621
+ setTimeout(() => toastContainer.removeChild(toast), 300);
622
+ }, 10000);
623
+ }
624
+ function togglePassword(id) {
625
+ const input = document.getElementById(id);
626
+ const toggleButton = input.parentElement.querySelector('.password-toggle');
627
+ const icon = toggleButton.querySelector('i');
628
+
629
+ if (input.type === 'password') {
630
+ input.type = 'text';
631
+ icon.classList.remove('fa-eye');
632
+ icon.classList.add('fa-eye-slash');
633
+ } else {
634
+ input.type = 'password';
635
+ icon.classList.remove('fa-eye-slash');
636
+ icon.classList.add('fa-eye');
637
+ }
638
+ }
639
+ window.onclick = function(e) {
640
+ if (e.target === document.getElementById('signupModal')) closeSignup();
641
+ }
642
+ </script>
643
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
644
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.1/umd/popper.min.js"></script>
645
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/js/bootstrap.min.js"></script>
646
+ </body>
647
+ </html>
templates/mark.html ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%);
13
+ color: #333;
14
+ line-height: 1.6;
15
+ }
16
+ .container {
17
+ width: 100%;
18
+ max-width: 800px;
19
+ margin: 30px auto;
20
+ padding: 30px;
21
+ background-color: #fff;
22
+ border-radius: 12px;
23
+ box-shadow: 0 10px 25px rgba(0,0,0,0.12);
24
+ text-align: center;
25
+ border-top: 4px solid #1a4a7c;
26
+ }
27
+ h2 {
28
+ color: #1a4a7c;
29
+ margin-bottom: 25px;
30
+ font-size: 28px;
31
+ font-weight: 600;
32
+ position: relative;
33
+ padding-bottom: 10px;
34
+ }
35
+ h2:after {
36
+ content: "";
37
+ position: absolute;
38
+ bottom: 0;
39
+ left: 50%;
40
+ transform: translateX(-50%);
41
+ width: 80px;
42
+ height: 3px;
43
+ background: linear-gradient(90deg, #1a4a7c, #3498db);
44
+ }
45
+ .btn {
46
+ display: inline-block;
47
+ padding: 12px 24px;
48
+ background: linear-gradient(to right, #1a4a7c, #3498db);
49
+ color: white;
50
+ text-decoration: none;
51
+ border-radius: 8px;
52
+ font-weight: 500;
53
+ transition: all 0.3s;
54
+ border: none;
55
+ font-size: 16px;
56
+ cursor: pointer;
57
+ margin: 15px 0;
58
+ box-shadow: 0 4px 8px rgba(26, 74, 124, 0.25);
59
+ }
60
+ .btn:hover {
61
+ transform: translateY(-2px);
62
+ box-shadow: 0 6px 12px rgba(26, 74, 124, 0.35);
63
+ background: linear-gradient(to right, #155785, #2980b9);
64
+ }
65
+ select {
66
+ width: 100%;
67
+ padding: 12px 15px;
68
+ margin: 10px 0;
69
+ border: 1px solid #ddd;
70
+ border-radius: 8px;
71
+ box-sizing: border-box;
72
+ font-size: 16px;
73
+ background-color: #f9fbff;
74
+ transition: all 0.3s;
75
+ }
76
+ select:focus {
77
+ border-color: #3498db;
78
+ outline: none;
79
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15);
80
+ background-color: #fff;
81
+ }
82
+ video {
83
+ margin: 20px auto;
84
+ border-radius: 8px;
85
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
86
+ background-color: #eee;
87
+ max-width: 100%;
88
+ }
89
+ #status {
90
+ margin-top: 20px;
91
+ font-size: 18px;
92
+ padding: 15px;
93
+ border-radius: 5px;
94
+ background-color: #f8f9fa;
95
+ display: inline-block;
96
+ min-width: 60%;
97
+ }
98
+ .success {
99
+ color: #27ae60;
100
+ font-weight: 600;
101
+ }
102
+ .error {
103
+ color: #e74c3c;
104
+ font-weight: 600;
105
+ }
106
+ .header {
107
+ display: flex;
108
+ justify-content: space-between;
109
+ align-items: center;
110
+ margin-bottom: 20px;
111
+ }
112
+ .back-link {
113
+ color: #3498db;
114
+ text-decoration: none;
115
+ display: flex;
116
+ align-items: center;
117
+ font-weight: 500;
118
+ transition: color 0.3s;
119
+ }
120
+ .back-link:hover {
121
+ color: #1a4a7c;
122
+ text-decoration: underline;
123
+ }
124
+ .video-container {
125
+ position: relative;
126
+ margin: 0 auto;
127
+ width: 320px;
128
+ max-width: 100%;
129
+ }
130
+ .camera-overlay {
131
+ position: absolute;
132
+ top: 0;
133
+ left: 0;
134
+ right: 0;
135
+ bottom: 0;
136
+ border: 2px solid rgba(52, 152, 219, 0.5);
137
+ border-radius: 8px;
138
+ pointer-events: none;
139
+ }
140
+ .loading {
141
+ display: none;
142
+ margin: 20px auto;
143
+ border: 5px solid #f3f3f3;
144
+ border-radius: 50%;
145
+ border-top: 5px solid #3498db;
146
+ width: 40px;
147
+ height: 40px;
148
+ animation: spin 1s linear infinite;
149
+ }
150
+ @keyframes spin {
151
+ 0% { transform: rotate(0deg); }
152
+ 100% { transform: rotate(360deg); }
153
+ }
154
+ </style>
155
+ </head>
156
+ <body>
157
+ <div class="container">
158
+ <div class="header">
159
+ <a href="/" class="back-link">
160
+ <i class="fas fa-arrow-left"></i>  Back to Home
161
+ </a>
162
+ <h2>Mark Attendance</h2>
163
+ </div>
164
+ <select id="class-select-mark">
165
+ <option value="">Select Class</option>
166
+ <option value="SY-19">SY-19</option>
167
+ <option value="SY-20">SY-20</option>
168
+ <option value="SY-21">SY-21</option>
169
+ <option value="SY-22">SY-22</option>
170
+ </select>
171
+ <div class="video-container">
172
+ <video id="video" width="300" autoplay></video>
173
+ <div class="camera-overlay"></div>
174
+ </div>
175
+ <button onclick="scanFace()" class="btn">
176
+ <i class="fas fa-user-check"></i> Scan Face
177
+ </button>
178
+ <div id="result"></div>
179
+ <p id="status">Select class and click "Scan Face"</p>
180
+ </div>
181
+
182
+ <script src="/static/js/webcam.js"></script>
183
+ <script>
184
+ let selectedVoice = null;
185
+
186
+ function selectVoice() {
187
+ const voices = speechSynthesis.getVoices();
188
+ console.log('Available voices:', voices);
189
+ selectedVoice = voices.find(voice =>
190
+ voice.lang === 'en-GB' &&
191
+ voice.name.includes('Google UK English Female')
192
+ ) || voices.find(voice => voice.lang === 'en-GB');
193
+
194
+ if (!selectedVoice) {
195
+ console.log('Desired voice not found, using default');
196
+ } else {
197
+ console.log('Selected voice:', selectedVoice.name);
198
+ }
199
+ }
200
+
201
+ // Load voices
202
+ if (speechSynthesis.getVoices().length > 0) {
203
+ selectVoice();
204
+ } else {
205
+ speechSynthesis.onvoiceschanged = selectVoice;
206
+ }
207
+ </script>
208
+ <script>
209
+ function scanFace() {
210
+ const className = document.getElementById('class-select-mark').value;
211
+ if (!className) return alert('Please select a class');
212
+ document.getElementById('status').innerText = 'Scanning...';
213
+ captureFrame([], true).then(image => {
214
+ fetch('/api/identify_face', {
215
+ method: 'POST',
216
+ headers: {'Content-Type': 'application/json'},
217
+ body: JSON.stringify({class_name: className, image: image})
218
+ })
219
+ .then(res => res.json())
220
+ .then(data => {
221
+ const result = document.getElementById('result');
222
+ if (data.status === 'success') {
223
+ // Fetch attendance average
224
+ fetch('/api/get_attendance_average', {
225
+ method: 'POST',
226
+ headers: {'Content-Type': 'application/json'},
227
+ body: JSON.stringify({class_name: className, enrollment: data.enrollment})
228
+ })
229
+ .then(res => res.json())
230
+ .then(avgData => {
231
+ let overallPercentage;
232
+ if (avgData.error) {
233
+ console.error(avgData.error);
234
+ overallPercentage = 'N/A';
235
+ } else {
236
+ overallPercentage = avgData.overall_percentage;
237
+ }
238
+ result.innerHTML = `
239
+ <p>Is this you? <strong>${data.name}</strong> (${data.enrollment})</p>
240
+ <button id="checkin-btn" class="btn">Check In</button>
241
+ <button id="checkout-btn" class="btn">Check Out</button>
242
+ <button id="rescan-btn" class="btn">Rescan</button>
243
+ `;
244
+ document.getElementById('status').innerText = '';
245
+ document.getElementById('checkin-btn').onclick = () => handleAction('checkin', className, data.enrollment, data.name, overallPercentage);
246
+ document.getElementById('checkout-btn').onclick = () => handleAction('checkout', className, data.enrollment, data.name, overallPercentage);
247
+ document.getElementById('rescan-btn').onclick = () => { result.innerHTML = ''; document.getElementById('status').innerText = 'Select class and click "Scan Face"'; };
248
+ });
249
+ } else {
250
+ result.innerHTML = `<span class="error">${data.message}</span><br><button id="rescan-btn" class="btn">Rescan</button>`;
251
+ document.getElementById('rescan-btn').onclick = () => { result.innerHTML = ''; document.getElementById('status').innerText = 'Select class and click "Scan Face"'; };
252
+ }
253
+ });
254
+ });
255
+ }
256
+
257
+ function handleAction(action, className, enrollment, name, overallPercentage) {
258
+ let data = { class_name: className, enrollment: enrollment, name: name };
259
+ if (action === 'checkin') {
260
+ const pin = prompt('Enter PIN and Check-in (4-digit PIN):');
261
+ if (!pin || !/^\d{4}$/.test(pin)) {
262
+ alert('Please enter a valid 4-digit PIN.');
263
+ return;
264
+ }
265
+ data.pin = pin;
266
+ }
267
+ fetch(`/api/${action}`, {
268
+ method: 'POST',
269
+ headers: {'Content-Type': 'application/json'},
270
+ body: JSON.stringify(data)
271
+ })
272
+ .then(res => res.json())
273
+ .then(data => {
274
+ alert(data.message);
275
+ document.getElementById('result').innerHTML = '';
276
+ document.getElementById('status').innerText = 'Select class and click "Scan Face"';
277
+ if (data.status === 'success') {
278
+ const now = new Date();
279
+ const hour = now.getHours();
280
+ let greeting;
281
+ if (action === 'checkin') {
282
+ if (hour < 12) {
283
+ greeting = `Good morning, ${name}. Your overall attendance average is ${overallPercentage} percent.`;
284
+ } else if (hour < 17) {
285
+ greeting = `Good afternoon, ${name}. Your overall attendance average is ${overallPercentage} percent.`;
286
+ } else {
287
+ greeting = `Good evening, ${name}. Your overall attendance average is ${overallPercentage} percent.`;
288
+ }
289
+ } else if (action === 'checkout') {
290
+ greeting = `Get well soon, see you tomorrow, ${name}. Your overall attendance average is ${overallPercentage} percent.`;
291
+ }
292
+ speakMessage(greeting);
293
+ }
294
+ });
295
+ }
296
+
297
+ function speakMessage(message) {
298
+ if ('speechSynthesis' in window) {
299
+ const utterance = new SpeechSynthesisUtterance(message);
300
+ if (selectedVoice) {
301
+ utterance.voice = selectedVoice;
302
+ }
303
+ speechSynthesis.speak(utterance);
304
+ } else {
305
+ console.log('Speech synthesis not supported');
306
+ }
307
+ }
308
+
309
+ initWebcam('video');
310
+ </script>
311
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js"></script>
312
+ </body>
313
+ </html>
templates/register.html ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%);
13
+ color: #333;
14
+ line-height: 1.6;
15
+ }
16
+ .container {
17
+ width: 100%;
18
+ max-width: 800px;
19
+ margin: 30px auto;
20
+ padding: 30px;
21
+ background-color: #fff;
22
+ border-radius: 12px;
23
+ box-shadow: 0 10px 25px rgba(0,0,0,0.12);
24
+ text-align: center;
25
+ border-top: 4px solid #1a4a7c;
26
+ }
27
+ h2 {
28
+ color: #1a4a7c;
29
+ margin-bottom: 25px;
30
+ font-size: 28px;
31
+ font-weight: 600;
32
+ position: relative;
33
+ padding-bottom: 10px;
34
+ }
35
+ h2:after {
36
+ content: "";
37
+ position: absolute;
38
+ bottom: 0;
39
+ left: 50%;
40
+ transform: translateX(-50%);
41
+ width: 80px;
42
+ height: 3px;
43
+ background: linear-gradient(90deg, #1a4a7c, #3498db);
44
+ }
45
+ .btn {
46
+ display: inline-block;
47
+ padding: 12px 24px;
48
+ background: linear-gradient(to right, #1a4a7c, #3498db);
49
+ color: white;
50
+ text-decoration: none;
51
+ border-radius: 8px;
52
+ font-weight: 500;
53
+ transition: all 0.3s;
54
+ border: none;
55
+ font-size: 16px;
56
+ cursor: pointer;
57
+ margin-top: 15px;
58
+ box-shadow: 0 4px 8px rgba(26, 74, 124, 0.25);
59
+ }
60
+ .btn:hover {
61
+ transform: translateY(-2px);
62
+ box-shadow: 0 6px 12px rgba(26, 74, 124, 0.35);
63
+ background: linear-gradient(to right, #155785, #2980b9);
64
+ }
65
+ input[type="text"], select {
66
+ width: 100%;
67
+ padding: 12px 15px;
68
+ margin: 10px 0;
69
+ border: 1px solid #ddd;
70
+ border-radius: 8px;
71
+ box-sizing: border-box;
72
+ font-size: 16px;
73
+ background-color: #f9fbff;
74
+ transition: all 0.3s;
75
+ }
76
+ input[type="text"]:focus, select:focus {
77
+ border-color: #3498db;
78
+ outline: none;
79
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15);
80
+ background-color: #fff;
81
+ }
82
+ video {
83
+ margin: 20px auto;
84
+ border-radius: 8px;
85
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
86
+ background-color: #eee;
87
+ max-width: 100%;
88
+ }
89
+ #status {
90
+ margin-top: 15px;
91
+ font-weight: 500;
92
+ color: #2c3e50;
93
+ font-size: 16px;
94
+ }
95
+ .header {
96
+ display: flex;
97
+ justify-content: space-between;
98
+ align-items: center;
99
+ margin-bottom: 20px;
100
+ }
101
+ .back-link {
102
+ color: #3498db;
103
+ text-decoration: none;
104
+ display: flex;
105
+ align-items: center;
106
+ font-weight: 500;
107
+ transition: color 0.3s;
108
+ }
109
+ .back-link:hover {
110
+ color: #1a4a7c;
111
+ text-decoration: underline;
112
+ }
113
+ .form-container {
114
+ margin-top: 20px;
115
+ }
116
+ .progress {
117
+ margin-top: 10px;
118
+ height: 10px;
119
+ background-color: #eee;
120
+ border-radius: 5px;
121
+ overflow: hidden;
122
+ position: relative;
123
+ margin-bottom: 15px;
124
+ }
125
+ .progress-bar {
126
+ height: 100%;
127
+ background-color: #3498db;
128
+ width: 0%;
129
+ transition: width 0.5s ease;
130
+ }
131
+ </style>
132
+ </head>
133
+ <body>
134
+ <div class="container">
135
+ <div class="header">
136
+ <a href="/" class="back-link">
137
+ <i class="fas fa-arrow-left"></i> &nbsp;Back to Home
138
+ </a>
139
+ <h2>Register Face</h2>
140
+ </div>
141
+ <div class="form-container">
142
+ <select id="class-select" required>
143
+ <option value="">Select Class</option>
144
+ <option value="SY-19">SY-19</option>
145
+ <option value="SY-20">SY-20</option>
146
+ <option value="SY-21">SY-21</option>
147
+ <option value="SY-22">SY-22</option>
148
+ </select>
149
+ <input type="text" id="name" placeholder="Enter your name" required>
150
+ <input type="text" id="enrollment" placeholder="Enter Enrollment Number" required>
151
+ <button onclick="startCapture()" class="btn">
152
+ <i class="fas fa-camera"></i> Start Capture
153
+ </button>
154
+ <div class="video-container">
155
+ <video id="video" width="300" autoplay></video>
156
+ </div>
157
+ <div class="progress">
158
+ <div class="progress-bar" id="progress-bar"></div>
159
+ </div>
160
+ <p id="status">Select class, enter details, and click "Start Capture"</p>
161
+ </div>
162
+ </div>
163
+ <script src="/static/js/webcam.js"></script>
164
+ <script>
165
+ let frames = [];
166
+ let interval;
167
+ const TOTAL_FRAMES = 120;
168
+ function startCapture() {
169
+ const className = document.getElementById('class-select').value;
170
+ const name = document.getElementById('name').value.trim();
171
+ const enrollment = document.getElementById('enrollment').value.trim();
172
+ if (!className) return alert('Please select a class');
173
+ if (!name) return alert('Please enter your name');
174
+ if (!enrollment) return alert('Please enter enrollment number');
175
+ frames = [];
176
+ document.getElementById('status').innerHTML = 'Capturing...<br><small>Slowly move your head</small>';
177
+ document.getElementById('progress-bar').style.width = '0%';
178
+ interval = setInterval(() => {
179
+ captureFrame(frames);
180
+ const progress = (frames.length / TOTAL_FRAMES) * 100;
181
+ document.getElementById('progress-bar').style.width = progress + '%';
182
+ if (frames.length >= TOTAL_FRAMES) {
183
+ clearInterval(interval);
184
+ document.getElementById('status').innerHTML = 'Processing...';
185
+ fetch('/api/register_face', {
186
+ method: 'POST',
187
+ headers: {'Content-Type': 'application/json'},
188
+ body: JSON.stringify({
189
+ class_name: className,
190
+ name: name,
191
+ enrollment: enrollment,
192
+ images: frames
193
+ })
194
+ })
195
+ .then(res => res.json())
196
+ .then(data => {
197
+ let msg = data.message;
198
+ if (data.status === 'success') {
199
+ msg += '. Please maintain consistent lighting.';
200
+ }
201
+ document.getElementById('status').innerHTML = msg;
202
+ });
203
+ }
204
+ }, 1000);
205
+ }
206
+ initWebcam('video');
207
+ </script>
208
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js"></script>
209
+ <script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'93ecda75aef8e14d',t:'MTc0NzA4MzcwNy4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
210
+ </html>
templates/student_dashboard.html ADDED
@@ -0,0 +1,863 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ :root {
11
+ --primary: #1a4a7c;
12
+ --secondary: #3498db;
13
+ --success: #2ecc71;
14
+ --warning: #f39c12;
15
+ --info: #2980b9;
16
+ --light: #ecf0f1;
17
+ --dark: #2c3e50;
18
+ --white: #ffffff;
19
+ --gray: #bdc3c7;
20
+ --border-radius: 8px;
21
+ --font-main: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
22
+ }
23
+ body {
24
+ background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%);
25
+ min-height: 100vh;
26
+ font-family: var(--font-main);
27
+ padding: 40px 20px;
28
+ }
29
+ .container {
30
+ max-width: 900px;
31
+ }
32
+ .dashboard-box {
33
+ background-color: var(--white);
34
+ border-radius: var(--border-radius);
35
+ padding: 35px;
36
+ width: 100%;
37
+ box-shadow: 0 10px 25px rgba(0,0,0,0.15);
38
+ margin: 20px auto;
39
+ border-top: 4px solid var(--primary);
40
+ }
41
+ h1 {
42
+ color: var(--primary);
43
+ margin-bottom: 25px;
44
+ text-align: center;
45
+ font-weight: 600;
46
+ font-size: 2.2rem;
47
+ }
48
+ h4 {
49
+ color: var(--primary);
50
+ margin: 25px 0 15px;
51
+ font-weight: 500;
52
+ font-size: 1.4rem;
53
+ }
54
+ p {
55
+ font-size: 16px;
56
+ color: var(--dark);
57
+ margin-bottom: 15px;
58
+ }
59
+ .editable {
60
+ cursor: pointer;
61
+ color: var(--info);
62
+ font-size: 0.9rem;
63
+ text-decoration: none;
64
+ transition: color 0.3s;
65
+ display: block;
66
+ text-align: center;
67
+ margin-top: 5px;
68
+ }
69
+ .editable:hover {
70
+ color: var(--secondary);
71
+ text-decoration: underline;
72
+ }
73
+ .hidden {
74
+ display: none;
75
+ }
76
+ .btn-primary {
77
+ background: linear-gradient(to right, var(--primary), var(--secondary));
78
+ border: none;
79
+ border-radius: 50px;
80
+ font-weight: 500;
81
+ padding: 12px 25px;
82
+ transition: all 0.3s;
83
+ box-shadow: 0 4px 10px rgba(26, 74, 124, 0.3);
84
+ margin-top: 20px;
85
+ }
86
+ .btn-primary:hover {
87
+ background: linear-gradient(to right, #155785, #2980b9);
88
+ transform: translateY(-2px);
89
+ box-shadow: 0 6px 12px rgba(26, 74, 124, 0.4);
90
+ }
91
+ .btn-secondary {
92
+ background-color: var(--light);
93
+ color: var(--dark);
94
+ border: none;
95
+ border-radius: 50px;
96
+ font-weight: 500;
97
+ padding: 12px 25px;
98
+ transition: all 0.3s;
99
+ margin-top: 10px;
100
+ }
101
+ .btn-secondary:hover {
102
+ background-color: var(--gray);
103
+ transform: translateY(-2px);
104
+ }
105
+ .btn-success {
106
+ background: linear-gradient(to right, var(--success), var(--info));
107
+ border: none;
108
+ border-radius: 50px;
109
+ font-weight: 500;
110
+ padding: 12px 25px;
111
+ transition: all 0.3s;
112
+ box-shadow: 0 4px 10px rgba(46, 204, 113, 0.3);
113
+ margin-right: 10px;
114
+ }
115
+ .btn-success:hover {
116
+ background: linear-gradient(to right, #27ae60, var(--success));
117
+ transform: translateY(-2px);
118
+ box-shadow: 0 6px 12px rgba(46, 204, 113, 0.4);
119
+ }
120
+ .btn-link {
121
+ color: var(--gray);
122
+ text-decoration: none;
123
+ padding: 10px;
124
+ display: block;
125
+ text-align: center;
126
+ transition: color 0.3s;
127
+ font-weight: 500;
128
+ }
129
+ .btn-link:hover {
130
+ color: var(--primary);
131
+ text-decoration: none;
132
+ }
133
+ .profile-info {
134
+ background-color: var(--light);
135
+ border-radius: var(--border-radius);
136
+ padding: 25px;
137
+ margin-bottom: 25px;
138
+ border-left: 4px solid var(--primary);
139
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
140
+ }
141
+ .profile-info p {
142
+ margin-bottom: 15px;
143
+ display: flex;
144
+ align-items: center;
145
+ line-height: 1.6;
146
+ }
147
+ .profile-info p strong {
148
+ min-width: 130px;
149
+ display: inline-block;
150
+ }
151
+ .profile-info p strong i {
152
+ width: 20px;
153
+ color: var(--primary);
154
+ margin-right: 8px;
155
+ }
156
+ .form-group label {
157
+ font-weight: 500;
158
+ color: var(--dark);
159
+ margin-bottom: 8px;
160
+ }
161
+ .form-control {
162
+ border-radius: var(--border-radius);
163
+ border: 1px solid var(--gray);
164
+ padding: 12px;
165
+ transition: border 0.3s, box-shadow 0.3s;
166
+ margin-bottom: 15px;
167
+ }
168
+ .form-control:focus {
169
+ border-color: var(--secondary);
170
+ box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
171
+ }
172
+ select.form-control {
173
+ cursor: pointer;
174
+ height: 46px;
175
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'%3E%3Cpath fill='%231a4a7c' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
176
+ background-repeat: no-repeat;
177
+ background-position: right 10px center;
178
+ padding-right: 30px;
179
+ background-size: 20px;
180
+ -webkit-appearance: none;
181
+ -moz-appearance: none;
182
+ appearance: none;
183
+ }
184
+ .table {
185
+ border-radius: var(--border-radius);
186
+ overflow: hidden;
187
+ box-shadow: 0 4px 10px rgba(0,0,0,0.05);
188
+ margin-top: 20px;
189
+ margin-bottom: 25px;
190
+ border: none;
191
+ }
192
+ .table thead th {
193
+ background-color: rgba(26, 74, 124, 0.1);
194
+ color: var(--primary);
195
+ border-bottom: 2px solid var(--primary);
196
+ font-weight: 600;
197
+ padding: 15px;
198
+ text-transform: uppercase;
199
+ font-size: 0.85rem;
200
+ letter-spacing: 0.5px;
201
+ }
202
+ .table tbody td {
203
+ padding: 15px;
204
+ border-top: 1px solid #e9ecef;
205
+ vertical-align: middle;
206
+ }
207
+ .table-striped tbody tr:nth-of-type(odd) {
208
+ background-color: rgba(248, 249, 250, 0.7);
209
+ }
210
+ hr {
211
+ border-top: 1px solid #e9ecef;
212
+ margin: 30px 0;
213
+ }
214
+ .section-title {
215
+ display: flex;
216
+ align-items: center;
217
+ margin-bottom: 20px;
218
+ padding-bottom: 10px;
219
+ border-bottom: 2px solid rgba(26, 74, 124, 0.2);
220
+ }
221
+ .section-title i {
222
+ margin-right: 10px;
223
+ color: var(--primary);
224
+ font-size: 1.2rem;
225
+ }
226
+ .action-buttons {
227
+ display: flex;
228
+ justify-content: space-between;
229
+ margin-top: 25px;
230
+ }
231
+ .action-buttons button {
232
+ width: 48%;
233
+ }
234
+ .alert-info {
235
+ background-color: rgba(26, 74, 124, 0.1);
236
+ border-color: rgba(26, 74, 124, 0.2);
237
+ color: var(--primary);
238
+ border-radius: var(--border-radius);
239
+ padding: 15px;
240
+ margin-bottom: 25px;
241
+ }
242
+ .alert-info i {
243
+ color: var(--primary);
244
+ margin-right: 8px;
245
+ }
246
+ .modal {
247
+ display: none;
248
+ position: fixed;
249
+ top: 0;
250
+ left: 0;
251
+ width: 100%;
252
+ height: 100%;
253
+ background-color: rgba(0,0,0,0.5);
254
+ z-index: 1000;
255
+ justify-content: center;
256
+ align-items: center;
257
+ }
258
+ .modal-content {
259
+ background-color: var(--white);
260
+ border-radius: var(--border-radius);
261
+ margin:15% auto;
262
+ padding: 30px;
263
+ border: none;
264
+ width: 90%;
265
+ max-width: 500px;
266
+ box-shadow: 0 15px 30px rgba(0,0,0,0.2);
267
+ position: relative;
268
+ }
269
+ .close-modal {
270
+ position: absolute;
271
+ top: 15px;
272
+ right: 20px;
273
+ color: var(--gray);
274
+ font-size: 24px;
275
+ font-weight: bold;
276
+ cursor: pointer;
277
+ transition: color 0.3s;
278
+ }
279
+ .close-modal:hover {
280
+ color: var(--dark);
281
+ }
282
+ .modal h3 {
283
+ color: var(--primary);
284
+ margin-bottom: 20px;
285
+ padding-bottom: 15px;
286
+ border-bottom: 1px solid #e9ecef;
287
+ }
288
+ .modal button {
289
+ background: linear-gradient(to right, var(--primary), var(--secondary));
290
+ color: var(--white);
291
+ border: none;
292
+ border-radius: 50px;
293
+ padding: 12px 25px;
294
+ margin-top: 20px;
295
+ cursor: pointer;
296
+ transition: all 0.3s;
297
+ font-weight: 500;
298
+ box-shadow: 0 4px 10px rgba(26, 74, 124, 0.3);
299
+ display: block;
300
+ width: 100%;
301
+ }
302
+ .modal button:hover {
303
+ background: linear-gradient(to right, #155785, #2980b9);
304
+ transform: translateY(-2px);
305
+ box-shadow: 0 6px 12px rgba(26, 74, 124, 0.4);
306
+ }
307
+ .modal label {
308
+ display: block;
309
+ margin: 10px 0;
310
+ font-weight: 500;
311
+ }
312
+ .modal select {
313
+ width: 100%;
314
+ padding: 10px;
315
+ border-radius: var(--border-radius);
316
+ border: 1px solid var(--gray);
317
+ margin-bottom: 20px;
318
+ }
319
+ #regularSubjectsList,
320
+ #specializationSubjectsList,
321
+ #practicalsList {
322
+ max-height: 300px;
323
+ overflow-y: auto;
324
+ padding: 10px;
325
+ border: 1px solid #eee;
326
+ border-radius: var(--border-radius);
327
+ margin-bottom: 10px;
328
+ }
329
+ #regularSubjectsList label,
330
+ #specializationSubjectsList label,
331
+ #practicalsList label {
332
+ display: block;
333
+ padding: 8px;
334
+ border-bottom: 1px solid #f5f5f5;
335
+ margin: 0;
336
+ }
337
+ #regularSubjectsList label:hover,
338
+ #specializationSubjectsList label:hover,
339
+ #practicalsList label:hover {
340
+ background-color: #f8f9fa;
341
+ }
342
+ #regularSubjectsList input,
343
+ #specializationSubjectsList input,
344
+ #practicalsList input {
345
+ margin-right: 10px;
346
+ }
347
+ .subject-selection-button {
348
+ display: inline-block;
349
+ background-color: var(--light);
350
+ color: var(--primary);
351
+ border: 1px solid var(--primary);
352
+ border-radius: 50px;
353
+ padding: 10px 20px;
354
+ margin: 10px 0;
355
+ font-weight: 500;
356
+ transition: all 0.3s;
357
+ text-decoration: none;
358
+ cursor: pointer;
359
+ }
360
+ .subject-selection-button:hover {
361
+ background-color: rgba(26, 74, 124, 0.1);
362
+ transform: translateY(-2px);
363
+ box-shadow: 0 4px 8px rgba(0,0,0,0.05);
364
+ }
365
+ .subject-selection-button i {
366
+ margin-right: 5px;
367
+ }
368
+ .mt-4 {
369
+ margin-top: 2rem;
370
+ }
371
+ .text-center {
372
+ text-align: center;
373
+ }
374
+ @media (max-width: 768px) {
375
+ .dashboard-box {
376
+ padding: 25px 20px;
377
+ }
378
+ .action-buttons {
379
+ flex-direction: column;
380
+ }
381
+ .action-buttons button {
382
+ width: 100%;
383
+ margin-bottom: 10px;
384
+ }
385
+ .profile-info p strong {
386
+ min-width: 105px;
387
+ }
388
+ }
389
+ </style>
390
+ </head>
391
+ <body>
392
+ <div class="container">
393
+ <div class="dashboard-box">
394
+ <h1>
395
+ Welcome, <span id="view-fullname">{{ user.full_name }}</span>
396
+ <small class="editable" onclick="openEdit()"><i class="fas fa-edit"></i> Edit Profile</small>
397
+ </h1>
398
+
399
+ <!-- PROFILE VIEW -->
400
+ <div id="profile-view" class="profile-info">
401
+ <p><strong><i class="fas fa-id-card"></i> Enrollment No:</strong> <span id="view-enroll">{{ user.enrollment_number }}</span></p>
402
+ <p><strong><i class="fas fa-envelope"></i> Email:</strong> <span id="view-email">{{ user.email }}</span></p>
403
+ <p><strong><i class="fas fa-phone"></i> Student No:</strong> <span id="view-studno">{{ user.student_number or '-' }}</span></p>
404
+ <p><strong><i class="fas fa-phone-alt"></i> Parent No:</strong> <span id="view-parentno">{{ user.parent_number or '-' }}</span></p>
405
+ <p><strong><i class="fas fa-envelope-open"></i> Parent Email:</strong> <span id="view-parentemail">{{ user.parent_email or '-' }}</span></p>
406
+ </div>
407
+
408
+ <!-- PROFILE EDIT -->
409
+ <div id="profile-edit" class="hidden">
410
+ <div class="form-group">
411
+ <label>Full Name</label>
412
+ <input id="edit-fullname" class="form-control" value="{{ user.full_name }}"/>
413
+ </div>
414
+ <div class="form-group">
415
+ <label>Enrollment No</label>
416
+ <input id="edit-enroll" class="form-control" value="{{ user.enrollment_number }}"/>
417
+ </div>
418
+ <div class="form-group">
419
+ <label>Email</label>
420
+ <input id="edit-email" class="form-control" value="{{ user.email }}"/>
421
+ </div>
422
+ <div class="form-group">
423
+ <label>Student No</label>
424
+ <input id="edit-studno" class="form-control" value="{{ user.student_number }}"/>
425
+ </div>
426
+ <div class="form-group">
427
+ <label>Parent No</label>
428
+ <input id="edit-parentno" class="form-control" value="{{ user.parent_number }}"/>
429
+ </div>
430
+ <div class="form-group">
431
+ <label>Parent Email</label>
432
+ <input id="edit-parentemail" class="form-control" value="{{ user.parent_email }}"/>
433
+ </div>
434
+ <div class="action-buttons">
435
+ <button class="btn btn-success" onclick="saveProfile()"><i class="fas fa-save"></i> Save</button>
436
+ <button class="btn btn-secondary" onclick="cancelEdit()"><i class="fas fa-times"></i> Cancel</button>
437
+ </div>
438
+ </div>
439
+
440
+ <hr/>
441
+
442
+ <div class="alert alert-info">
443
+ <i class="fas fa-chart-pie"></i>
444
+ <strong>Overall Attendance Average:</strong> <span id="overall-average">Calculating...</span>
445
+ </div>
446
+
447
+ <!-- CLASS SELECT -->
448
+ <div class="section-title">
449
+ <i class="fas fa-chalkboard-teacher"></i>
450
+ <h4>Class Information</h4>
451
+ </div>
452
+
453
+ <div class="form-group">
454
+ <label>Select Class</label>
455
+ <select id="class-select" class="form-control">
456
+ <option value="">-- Choose Class --</option>
457
+ <option {{ 'selected' if user.selected_class=='SY-19' }}>SY-19</option>
458
+ <option {{ 'selected' if user.selected_class=='SY-20' }}>SY-20</option>
459
+ <option {{ 'selected' if user.selected_class=='SY-21' }}>SY-21</option>
460
+ <option {{ 'selected' if user.selected_class=='SY-22' }}>SY-22</option>
461
+ </select>
462
+ </div>
463
+
464
+ <!-- SUBJECT TABLES -->
465
+ <div id="subjects-section" class="{{ '' if user.selected_class else 'hidden' }}">
466
+ <div class="alert alert-info">
467
+ <i class="fas fa-info-circle"></i>
468
+ <strong>Current Class:</strong> <span id="cls-label">{{ user.selected_class }}</span>
469
+ </div>
470
+
471
+ <!-- Regular Subjects -->
472
+ <div class="section-title">
473
+ <i class="fas fa-book"></i>
474
+ <h4>Regular Subjects</h4>
475
+ </div>
476
+
477
+ <button class="subject-selection-button" onclick="openRegularSubjectsModal()">
478
+ <i class="fas fa-plus-circle"></i> Select Regular Subjects
479
+ </button>
480
+
481
+ <table class="table table-striped">
482
+ <thead>
483
+ <tr>
484
+ <th>Subject</th>
485
+ <th>Attendance Count</th>
486
+ <th>Total Count</th>
487
+ <th>Percentage</th>
488
+ </tr>
489
+ </thead>
490
+ <tbody id="tbl-subjects">
491
+ {% for subject in selected_regular_subjects %}
492
+ <tr>
493
+ <td>{{ subject }}</td>
494
+ <td>{{ subject_attendance['regular'].get(subject, {}).get('attendance', 0) }}</td>
495
+ <td>{{ subject_attendance['regular'].get(subject, {}).get('total', 0) }}</td>
496
+ <td>
497
+ {% set attendance = subject_attendance['regular'].get(subject, {}).get('attendance', 0) %}
498
+ {% set total = subject_attendance['regular'].get(subject, {}).get('total', 0) %}
499
+ {% if total > 0 %}
500
+ {{ (attendance / total * 100) | round(2) }}%
501
+ {% else %}
502
+ N/A
503
+ {% endif %}
504
+ </td>
505
+ </tr>
506
+ {% endfor %}
507
+ </tbody>
508
+ </table>
509
+
510
+ <!-- Specialization -->
511
+ <div class="section-title">
512
+ <i class="fas fa-star"></i>
513
+ <h4>Specialization Subjects</h4>
514
+ </div>
515
+
516
+ <button class="subject-selection-button" onclick="openSpecializationSubjectsModal()">
517
+ <i class="fas fa-plus-circle"></i> Select Specialization Subjects
518
+ </button>
519
+
520
+ <table class="table table-striped">
521
+ <thead>
522
+ <tr>
523
+ <th>Specialization Subject</th>
524
+ <th>Attendance Count</th>
525
+ <th>Total Count</th>
526
+ <th>Percentage</th>
527
+ </tr>
528
+ </thead>
529
+ <tbody id="tbl-spec">
530
+ {% for subject in selected_specializations %}
531
+ <tr>
532
+ <td>{{ subject }}</td>
533
+ <td>{{ subject_attendance['specialization'].get(subject, {}).get('attendance', 0) }}</td>
534
+ <td>{{ subject_attendance['specialization'].get(subject, {}).get('total', 0) }}</td>
535
+ <td>
536
+ {% set attendance = subject_attendance['specialization'].get(subject, {}).get('attendance', 0) %}
537
+ {% set total = subject_attendance['specialization'].get(subject, {}).get('total', 0) %}
538
+ {% if total > 0 %}
539
+ {{ (attendance / total * 100) | round(2) }}%
540
+ {% else %}
541
+ N/A
542
+ {% endif %}
543
+ </td>
544
+ </tr>
545
+ {% endfor %}
546
+ </tbody>
547
+ </table>
548
+
549
+ <!-- Practical Subjects -->
550
+ <div class="section-title">
551
+ <i class="fas fa-flask"></i>
552
+ <h4>Practical Subjects</h4>
553
+ </div>
554
+
555
+ <button class="subject-selection-button" onclick="openBatchPracticalsModal()">
556
+ <i class="fas fa-plus-circle"></i> Select Batch & Practical Subjects
557
+ </button>
558
+
559
+ <table class="table table-striped">
560
+ <thead>
561
+ <tr>
562
+ <th>Practical Subject</th>
563
+ <th>Batch</th>
564
+ <th>Attendance Count</th>
565
+ <th>Total Count</th>
566
+ <th>Percentage</th>
567
+ </tr>
568
+ </thead>
569
+ <tbody id="tbl-prac">
570
+ {% for subject in selected_practicals %}
571
+ <tr>
572
+ <td>{{ subject }}</td>
573
+ <td>{{ user.selected_batch or 'N/A' }}</td>
574
+ <td>{{ subject_attendance['practical'].get(subject, {}).get('attendance', 0) }}</td>
575
+ <td>{{ subject_attendance['practical'].get(subject, {}).get('total', 0) }}</td>
576
+ <td>
577
+ {% set attendance = subject_attendance['practical'].get(subject, {}).get('attendance', 0) %}
578
+ {% set total = subject_attendance['practical'].get(subject, {}).get('total', 0) %}
579
+ {% if total > 0 %}
580
+ {{ (attendance / total * 100) | round(2) }}%
581
+ {% else %}
582
+ N/A
583
+ {% endif %}
584
+ </td>
585
+ </tr>
586
+ {% endfor %}
587
+ </tbody>
588
+ </table>
589
+
590
+ <!-- Teachers -->
591
+ <div class="section-title">
592
+ <i class="fas fa-chalkboard-teacher"></i>
593
+ <h4>Class Teacher Details</h4>
594
+ </div>
595
+ <table class="table table-striped">
596
+ <thead>
597
+ <tr>
598
+ <th>Name</th>
599
+ <th>Email</th>
600
+ <th>Phone</th>
601
+ </tr>
602
+ </thead>
603
+ <tbody>
604
+ {% for teacher in teachers %}
605
+ <tr>
606
+ <td>{{ teacher.full_name }}</td>
607
+ <td>
608
+ {% if teacher.email %}
609
+ <a href="mailto:{{ teacher.email }}">{{ teacher.email }}</a>
610
+ {% else %}
611
+ Not provided
612
+ {% endif %}
613
+ </td>
614
+ <td>
615
+ {% if teacher.phone_number %}
616
+ <a href="tel:{{ teacher.phone_number }}">{{ teacher.phone_number }}</a>
617
+ {% else %}
618
+ Not provided
619
+ {% endif %}
620
+ </td>
621
+ </tr>
622
+ {% endfor %}
623
+ </tbody>
624
+ </table>
625
+ </div>
626
+
627
+ <!-- FACE REGISTRATION -->
628
+ <div class="mt-4 text-center">
629
+ {% if face_exists %}
630
+ <a href="/register_face" class="btn btn-primary">
631
+ <i class="fas fa-camera"></i> Re-register Face
632
+ </a>
633
+ {% else %}
634
+ <a href="/register_face" class="btn btn-secondary">
635
+ <i class="fas fa-camera"></i> Register Face
636
+ </a>
637
+ {% endif %}
638
+ </div>
639
+
640
+ <a href="/logout" class="btn btn-link mt-2">
641
+ <i class="fas fa-sign-out-alt"></i> Logout
642
+ </a>
643
+ </div>
644
+ </div>
645
+
646
+ <!-- Modals -->
647
+ <div class="modal" id="regularSubjectsModal">
648
+ <div class="modal-content">
649
+ <span class="close-modal" onclick="closeModal('regularSubjectsModal')">✕</span>
650
+ <h3><i class="fas fa-book"></i> Select Regular Subjects</h3>
651
+ <div id="regularSubjectsList"></div>
652
+ <button onclick="saveRegularSubjects()"><i class="fas fa-save"></i> Save Selections</button>
653
+ </div>
654
+ </div>
655
+
656
+ <div class="modal" id="specializationSubjectsModal">
657
+ <div class="modal-content">
658
+ <span class="close-modal" onclick="closeModal('specializationSubjectsModal')">✕</span>
659
+ <h3><i class="fas fa-star"></i> Select Specialization Subjects</h3>
660
+ <div id="specializationSubjectsList"></div>
661
+ <button onclick="saveSpecializationSubjects()"><i class="fas fa-save"></i> Save Selections</button>
662
+ </div>
663
+ </div>
664
+
665
+ <div class="modal" id="batchPracticalsModal">
666
+ <div class="modal-content">
667
+ <span class="close-modal" onclick="closeModal('batchPracticalsModal')">✕</span>
668
+ <h3><i class="fas fa-flask"></i> Select Batch & Practical Subjects</h3>
669
+ <label for="batchSelectModal"><i class="fas fa-layer-group"></i> Select Batch:</label>
670
+ <select id="batchSelectModal"></select>
671
+ <div id="practicalsList"></div>
672
+ <button onclick="saveBatchAndPracticals()"><i class="fas fa-save"></i> Save Selections</button>
673
+ </div>
674
+ </div>
675
+
676
+ <script>
677
+ function openEdit() {
678
+ document.getElementById('profile-view').classList.add('hidden');
679
+ document.getElementById('profile-edit').classList.remove('hidden');
680
+ }
681
+
682
+ function cancelEdit() {
683
+ document.getElementById('profile-edit').classList.add('hidden');
684
+ document.getElementById('profile-view').classList.remove('hidden');
685
+ }
686
+
687
+ function saveProfile() {
688
+ fetch('/api/update_profile', {
689
+ method: 'POST',
690
+ headers: { 'Content-Type': 'application/json' },
691
+ body: JSON.stringify({
692
+ full_name: document.getElementById('edit-fullname').value,
693
+ enrollment_number: document.getElementById('edit-enroll').value,
694
+ email: document.getElementById('edit-email').value,
695
+ student_number: document.getElementById('edit-studno').value,
696
+ parent_number: document.getElementById('edit-parentno').value,
697
+ parent_email: document.getElementById('edit-parentemail').value
698
+ })
699
+ }).then(r => r.json()).then(resp => {
700
+ if (resp.success) location.reload();
701
+ else alert(resp.message);
702
+ });
703
+ }
704
+
705
+ let selectedRegularSubjects = {{ selected_regular_subjects | tojson | safe }};
706
+ let selectedSpecializations = {{ selected_specializations | tojson | safe }};
707
+ let selectedBatch = "{{ user.selected_batch or '' }}";
708
+ let selectedPracticals = {{ selected_practicals | tojson | safe }};
709
+
710
+ document.getElementById('class-select').addEventListener('change', e => {
711
+ const cls = e.target.value;
712
+ fetch('/api/select_class', {
713
+ method: 'POST',
714
+ headers: {'Content-Type': 'application/json'},
715
+ body: JSON.stringify({ class: cls })
716
+ }).then(_ => {
717
+ if (!cls) return location.reload();
718
+ document.getElementById('subjects-section').classList.remove('hidden');
719
+ document.getElementById('cls-label').innerText = cls;
720
+ loadStudentSection(cls);
721
+ });
722
+ });
723
+
724
+ function loadStudentSection(cls) {
725
+ // Handled by template rendering initially
726
+ }
727
+
728
+ function openRegularSubjectsModal() {
729
+ const cls = document.getElementById('class-select').value;
730
+ if (!cls) {
731
+ alert('Please select a class first');
732
+ return;
733
+ }
734
+ fetch(`/get_config?class=${cls}&type=subject`)
735
+ .then(r => r.json())
736
+ .then(list => {
737
+ const listDiv = document.getElementById('regularSubjectsList');
738
+ listDiv.innerHTML = '';
739
+ list.forEach(subject => {
740
+ const checked = selectedRegularSubjects.includes(subject) ? 'checked' : '';
741
+ listDiv.innerHTML += `<label><input type="checkbox" value="${subject}" ${checked}> ${subject}</label><br>`;
742
+ });
743
+ document.getElementById('regularSubjectsModal').style.display = 'block';
744
+ });
745
+ }
746
+
747
+ function openSpecializationSubjectsModal() {
748
+ const cls = document.getElementById('class-select').value;
749
+ if (!cls) {
750
+ alert('Please select a class first');
751
+ return;
752
+ }
753
+ fetch(`/get_config?class=${cls}&type=specSubject`)
754
+ .then(r => r.json())
755
+ .then(list => {
756
+ const listDiv = document.getElementById('specializationSubjectsList');
757
+ listDiv.innerHTML = '';
758
+ list.forEach(subject => {
759
+ const checked = selectedSpecializations.includes(subject) ? 'checked' : '';
760
+ listDiv.innerHTML += `<label><input type="checkbox" value="${subject}" ${checked}> ${subject}</label><br>`;
761
+ });
762
+ document.getElementById('specializationSubjectsModal').style.display = 'block';
763
+ });
764
+ }
765
+
766
+ function openBatchPracticalsModal() {
767
+ const cls = document.getElementById('class-select').value;
768
+ if (!cls) {
769
+ alert('Please select a class first');
770
+ return;
771
+ }
772
+ fetch(`/get_config?class=${cls}&type=batch`)
773
+ .then(r => r.json())
774
+ .then(batches => {
775
+ const batchSelect = document.getElementById('batchSelectModal');
776
+ batchSelect.innerHTML = '<option value="">-- Select Batch --</option>';
777
+ batches.forEach(batch => {
778
+ const selected = batch === selectedBatch ? 'selected' : '';
779
+ batchSelect.innerHTML += `<option value="${batch}" ${selected}>${batch}</option>`;
780
+ });
781
+ });
782
+ fetch(`/get_config?class=${cls}&type=practical`)
783
+ .then(r => r.json())
784
+ .then(practicals => {
785
+ const listDiv = document.getElementById('practicalsList');
786
+ listDiv.innerHTML = '';
787
+ practicals.forEach(subject => {
788
+ const checked = selectedPracticals.includes(subject) ? 'checked' : '';
789
+ listDiv.innerHTML += `<label><input type="checkbox" value="${subject}" ${checked}> ${subject}</label><br>`;
790
+ });
791
+ document.getElementById('batchPracticalsModal').style.display = 'block';
792
+ });
793
+ }
794
+
795
+ function saveRegularSubjects() {
796
+ const checkboxes = document.querySelectorAll('#regularSubjectsList input[type="checkbox"]:checked');
797
+ const selected = Array.from(checkboxes).map(cb => cb.value);
798
+ fetch('/api/update_regular_subjects', {
799
+ method: 'POST',
800
+ headers: {'Content-Type': 'application/json'},
801
+ body: JSON.stringify({subjects: selected})
802
+ }).then(() => {
803
+ location.reload();
804
+ });
805
+ }
806
+
807
+ function saveSpecializationSubjects() {
808
+ const checkboxes = document.querySelectorAll('#specializationSubjectsList input[type="checkbox"]:checked');
809
+ const selected = Array.from(checkboxes).map(cb => cb.value);
810
+ fetch('/api/update_specializations', {
811
+ method: 'POST',
812
+ headers: {'Content-Type': 'application/json'},
813
+ body: JSON.stringify({subjects: selected})
814
+ }).then(() => {
815
+ location.reload();
816
+ });
817
+ }
818
+
819
+ function saveBatchAndPracticals() {
820
+ const batch = document.getElementById('batchSelectModal').value;
821
+ const checkboxes = document.querySelectorAll('#practicalsList input[type="checkbox"]:checked');
822
+ const selectedPracticals = Array.from(checkboxes).map(cb => cb.value);
823
+ fetch('/api/update_batch', {
824
+ method: 'POST',
825
+ headers: {'Content-Type': 'application/json'},
826
+ body: JSON.stringify({batch: batch})
827
+ }).then(() => {
828
+ fetch('/api/update_practicals', {
829
+ method: 'POST',
830
+ headers: {'Content-Type': 'application/json'},
831
+ body: JSON.stringify({practicals: selectedPracticals})
832
+ }).then(() => {
833
+ location.reload();
834
+ });
835
+ });
836
+ }
837
+
838
+ function closeModal(modalId) {
839
+ document.getElementById(modalId).style.display = 'none';
840
+ }
841
+
842
+ window.addEventListener('DOMContentLoaded', () => {
843
+ const cs = document.getElementById('class-select');
844
+ if (cs.value) cs.dispatchEvent(new Event('change'));
845
+
846
+ // Calculate overall average
847
+ const percentageCells = document.querySelectorAll('#tbl-subjects td:last-child, #tbl-spec td:last-child, #tbl-prac td:last-child');
848
+ let sum = 0;
849
+ let count = 0;
850
+ percentageCells.forEach(cell => {
851
+ const text = cell.textContent.trim();
852
+ if (text !== 'N/A') {
853
+ const percentage = parseFloat(text.replace('%', ''));
854
+ sum += percentage;
855
+ count++;
856
+ }
857
+ });
858
+ const average = count > 0 ? (sum / count).toFixed(2) : 'N/A';
859
+ document.getElementById('overall-average').textContent = average === 'N/A' ? 'N/A' : average + '%';
860
+ });
861
+ </script>
862
+ </body>
863
+ </html>
templates/teacher_dashboard.html ADDED
@@ -0,0 +1,574 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ :root {
11
+ --primary: #1a4a7c;
12
+ --secondary: #3498db;
13
+ --success: #2ecc71;
14
+ --warning: #f39c12;
15
+ --danger: #e74c3c;
16
+ --info: #2980b9;
17
+ --light: #ecf0f1;
18
+ --dark: #2c3e50;
19
+ --white: #ffffff;
20
+ --gray: #bdc3c7;
21
+ --border-radius: 8px;
22
+ --font-main: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
23
+ }
24
+ body {
25
+ background: linear-gradient(135deg, #f0f4f8 0%, #d7e1ec 100%);
26
+ min-height: 100vh;
27
+ font-family: var(--font-main);
28
+ padding: 40px 20px;
29
+ color: var(--dark);
30
+ }
31
+ .container {
32
+ max-width: 1140px;
33
+ }
34
+ .dashboard-box {
35
+ background-color: var(--white);
36
+ border-radius: var(--border-radius);
37
+ padding: 30px;
38
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
39
+ margin-bottom: 30px;
40
+ border-top: 4px solid var(--primary);
41
+ transition: all 0.3s ease;
42
+ }
43
+ .dashboard-box:hover {
44
+ box-shadow: 0 6px 16px rgba(0,0,0,0.12);
45
+ }
46
+ h1 {
47
+ color: var(--primary);
48
+ margin-bottom: 25px;
49
+ text-align: center;
50
+ font-weight: 600;
51
+ font-size: 2.2rem;
52
+ }
53
+ h2 {
54
+ color: var(--primary);
55
+ margin-bottom: 20px;
56
+ font-weight: 500;
57
+ font-size: 1.6rem;
58
+ border-bottom: 2px solid rgba(26, 74, 124, 0.2);
59
+ padding-bottom: 10px;
60
+ }
61
+ h3 {
62
+ color: var(--primary);
63
+ font-weight: 500;
64
+ }
65
+ p {
66
+ font-size: 16px;
67
+ color: var(--dark);
68
+ margin-bottom: 15px;
69
+ }
70
+ .btn {
71
+ border-radius: var(--border-radius);
72
+ font-weight: 500;
73
+ letter-spacing: 0.5px;
74
+ transition: all 0.3s ease;
75
+ }
76
+ .btn-primary {
77
+ background: linear-gradient(to right, var(--primary), var(--secondary));
78
+ border: none;
79
+ padding: 12px 20px;
80
+ margin-top: 20px;
81
+ box-shadow: 0 3px 6px rgba(0,0,0,0.16);
82
+ }
83
+ .btn-primary:hover {
84
+ background: linear-gradient(to right, #155785, #2980b9);
85
+ box-shadow: 0 4px 8px rgba(0,0,0,0.24);
86
+ transform: translateY(-2px);
87
+ }
88
+ .btn-secondary {
89
+ background: var(--light);
90
+ color: var(--dark);
91
+ border: 1px solid var(--gray);
92
+ padding: 12px 20px;
93
+ margin-top: 10px;
94
+ }
95
+ .btn-secondary:hover {
96
+ background: var(--gray);
97
+ color: var(--white);
98
+ }
99
+ button {
100
+ background-color: var(--primary);
101
+ color: var(--white);
102
+ border: none;
103
+ padding: 8px 16px;
104
+ border-radius: var(--border-radius);
105
+ cursor: pointer;
106
+ font-weight: 500;
107
+ transition: all 0.2s ease;
108
+ }
109
+ button:hover {
110
+ background-color: var(--secondary);
111
+ }
112
+ .profile-section {
113
+ background-color: var(--light);
114
+ padding: 20px;
115
+ border-radius: var(--border-radius);
116
+ margin-bottom: 25px;
117
+ border-left: 4px solid var(--primary);
118
+ }
119
+ .profile-section p {
120
+ margin-bottom: 8px;
121
+ }
122
+ .profile-section h2 {
123
+ margin-bottom: 16px;
124
+ }
125
+ #editForm {
126
+ background-color: var(--light);
127
+ padding: 20px;
128
+ border-radius: var(--border-radius);
129
+ margin-bottom: 20px;
130
+ border-left: 4px solid var(--secondary);
131
+ }
132
+ #editForm input {
133
+ width: 100%;
134
+ padding: 10px 12px;
135
+ margin-bottom: 15px;
136
+ border: 1px solid var(--gray);
137
+ border-radius: var(--border-radius);
138
+ font-size: 16px;
139
+ transition: border 0.3s ease;
140
+ }
141
+ #editForm input:focus {
142
+ border-color: var(--secondary);
143
+ outline: none;
144
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.25);
145
+ }
146
+ #editForm button {
147
+ padding: 10px 20px;
148
+ margin-right: 10px;
149
+ }
150
+ select {
151
+ width: 100%;
152
+ padding: 12px;
153
+ border: 1px solid var(--gray);
154
+ border-radius: var(--border-radius);
155
+ margin-bottom: 20px;
156
+ font-size: 16px;
157
+ background-color: var(--white);
158
+ color: var(--dark);
159
+ }
160
+ select:focus {
161
+ border-color: var(--secondary);
162
+ outline: none;
163
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.25);
164
+ }
165
+ .table {
166
+ width: 100%;
167
+ border-collapse: separate;
168
+ border-spacing: 0;
169
+ border-radius: var(--border-radius);
170
+ overflow: hidden;
171
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
172
+ margin-top: 20px;
173
+ }
174
+ .table th {
175
+ background-color: var(--primary);
176
+ color: var(--white);
177
+ padding: 12px 16px;
178
+ text-align: left;
179
+ font-weight: 500;
180
+ border: none;
181
+ }
182
+ .table td {
183
+ padding: 12px 16px;
184
+ border-bottom: 1px solid var(--gray);
185
+ color: var(--dark);
186
+ }
187
+ .table tr:last-child td {
188
+ border-bottom: none;
189
+ }
190
+ .table tr:nth-child(even) {
191
+ background-color: #f9f9f9;
192
+ }
193
+ .table tr:hover {
194
+ background-color: #f1f1f1;
195
+ }
196
+ .table a {
197
+ color: var(--secondary);
198
+ text-decoration: none;
199
+ font-weight: 500;
200
+ }
201
+ .table a:hover {
202
+ text-decoration: underline;
203
+ color: var(--primary);
204
+ }
205
+ .modal {
206
+ display: none;
207
+ position: fixed;
208
+ top: 0;
209
+ left: 0;
210
+ width: 100%;
211
+ height: 100%;
212
+ background-color: rgba(0,0,0,0.5);
213
+ z-index: 1000;
214
+ overflow-y: auto;
215
+ padding: 40px 0;
216
+ }
217
+ .modal-content {
218
+ background-color: var(--white);
219
+ margin: 0% auto;
220
+ padding: 25px;
221
+ border-radius: var(--border-radius);
222
+ width: 90%;
223
+ max-width: 700px;
224
+ box-shadow: 0 10px 25px rgba(0,0,0,0.2);
225
+ position: relative;
226
+ }
227
+ .close-modal {
228
+ position: absolute;
229
+ top: 15px;
230
+ right: 15px;
231
+ color: var(--gray);
232
+ font-size: 24px;
233
+ font-weight: bold;
234
+ cursor: pointer;
235
+ transition: all 0.2s ease;
236
+ width: 30px;
237
+ height: 30px;
238
+ text-align: center;
239
+ line-height: 30px;
240
+ border-radius: 50%;
241
+ }
242
+ .close-modal:hover {
243
+ color: var(--dark);
244
+ background-color: #f1f1f1;
245
+ }
246
+ .modal-content h3 {
247
+ color: var(--primary);
248
+ margin-bottom: 20px;
249
+ padding-bottom: 10px;
250
+ border-bottom: 2px solid rgba(26, 74, 124, 0.2);
251
+ font-size: 1.5rem;
252
+ }
253
+ .modal-content h4 {
254
+ margin-top: 25px;
255
+ margin-bottom: 15px;
256
+ color: var(--primary);
257
+ font-size: 1.2rem;
258
+ font-weight: 500;
259
+ }
260
+ .modal-content table {
261
+ width: 100%;
262
+ border-collapse: collapse;
263
+ margin-top: 10px;
264
+ margin-bottom: 20px;
265
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
266
+ border-radius: var(--border-radius);
267
+ overflow: hidden;
268
+ }
269
+ .modal-content th {
270
+ background-color: var(--light);
271
+ color: var(--primary);
272
+ padding: 12px;
273
+ text-align: left;
274
+ font-weight: 500;
275
+ }
276
+ .modal-content td {
277
+ padding: 10px 12px;
278
+ border-bottom: 1px solid #ddd;
279
+ }
280
+ .modal-content tr:last-child td {
281
+ border-bottom: none;
282
+ }
283
+ .modal-content tr:nth-child(even) {
284
+ background-color: #f9f9f9;
285
+ }
286
+ .status {
287
+ display: inline-block;
288
+ padding: 5px 10px;
289
+ border-radius: 20px;
290
+ font-size: 14px;
291
+ font-weight: 500;
292
+ }
293
+ .status-registered {
294
+ background-color: #e8f5e9;
295
+ color: #388e3c;
296
+ }
297
+ .status-not-registered {
298
+ background-color: #ffebee;
299
+ color: #d32f2f;
300
+ }
301
+ .mt-4 {
302
+ margin-top: 1.5rem;
303
+ }
304
+ .mb-4 {
305
+ margin-bottom: 1.5rem;
306
+ }
307
+ .text-center {
308
+ text-align: center;
309
+ }
310
+ </style>
311
+ </head>
312
+ <body>
313
+ <div class="container">
314
+ <div class="dashboard-box">
315
+ <h1><i class="fas fa-chalkboard-teacher mr-2"></i>Welcome, {{ user.full_name }}</h1>
316
+ <div class="profile-section">
317
+ <h2><i class="fas fa-user mr-2"></i>Profile</h2>
318
+ <p><strong>Name:</strong> {{ user.full_name }}</p>
319
+ <p><strong>Email:</strong> {{ user.email }}</p>
320
+ <p><strong>Phone:</strong> {{ user.phone_number or 'Not provided' }}</p>
321
+ <button onclick="openEdit()"><i class="fas fa-edit mr-1"></i> Edit Profile</button>
322
+ </div>
323
+ <div id="editForm" style="display:none;">
324
+ <h2><i class="fas fa-pen mr-2"></i>Edit Profile</h2>
325
+ <div class="form-group">
326
+ <label for="editFullname">Full Name</label>
327
+ <input type="text" id="editFullname" class="form-control" value="{{ user.full_name }}">
328
+ </div>
329
+ <div class="form-group">
330
+ <label for="editEmail">Email</label>
331
+ <input type="email" id="editEmail" class="form-control" value="{{ user.email }}">
332
+ </div>
333
+ <div class="form-group">
334
+ <label for="editPhone">Phone Number</label>
335
+ <input type="text" id="editPhone" class="form-control" value="{{ user.phone_number or '' }}">
336
+ </div>
337
+ <div>
338
+ <button onclick="saveProfile()"><i class="fas fa-save mr-1"></i> Save Changes</button>
339
+ <button onclick="cancelEdit()" style="background-color: #757575;"><i class="fas fa-times mr-1"></i> Cancel</button>
340
+ </div>
341
+ </div>
342
+ <div class="button-group mt-4">
343
+ <button class="btn btn-primary" onclick="window.location.href='/timetable'"><i class="fas fa-calendar-alt mr-2"></i> View Timetable</button>
344
+ <a href="/logout" class="btn btn-secondary"><i class="fas fa-sign-out-alt mr-2"></i> Logout</a>
345
+ {% if daily_pin %}
346
+ <div class="daily-pin" style="margin-top: 20px;">
347
+ <h3>Today's PIN for {{ selected_class }}</h3>
348
+ <p><strong>PIN:</strong> {{ daily_pin }}</p>
349
+ </div>
350
+ {% endif %}
351
+ </div>
352
+ </div>
353
+ <div class="dashboard-box">
354
+ <h2><i class="fas fa-users mr-2"></i>Class Management</h2>
355
+ <div class="form-group">
356
+ <label for="classSelect"><strong>Select Class</strong></label>
357
+ <select id="classSelect" class="form-control" onchange="loadStudents()">
358
+ <option value="">--Select Class--</option>
359
+ <option value="SY-19" {% if selected_class == 'SY-19' %}selected{% endif %}>SY-19</option>
360
+ <option value="SY-20" {% if selected_class == 'SY-20' %}selected{% endif %}>SY-20</option>
361
+ <option value="SY-21" {% if selected_class == 'SY-21' %}selected{% endif %}>SY-21</option>
362
+ <option value="SY-22" {% if selected_class == 'SY-22' %}selected{% endif %}>SY-22</option>
363
+ </select>
364
+ </div>
365
+ <table id="studentsTable" style="display:none;" class="table">
366
+ <thead>
367
+ <tr>
368
+ <th><i class="fas fa-user-graduate mr-1"></i> Student Name</th>
369
+ <th><i class="fas fa-id-card mr-1"></i> Enrollment No</th>
370
+ </tr>
371
+ </thead>
372
+ <tbody id="studentsBody">
373
+ </tbody>
374
+ </table>
375
+ </div>
376
+ </div>
377
+ <div class="modal" id="studentModal">
378
+ <div class="modal-content">
379
+ <span class="close-modal" onclick="closeStudentModal()">✕</span>
380
+ <h3 id="studentName"><i class="fas fa-user-graduate mr-2"></i>Student Name</h3>
381
+ <div id="studentDetails"></div>
382
+ </div>
383
+ </div>
384
+ <script>
385
+ function openEdit() {
386
+ document.getElementById('editForm').style.display = 'block';
387
+ }
388
+ function cancelEdit() {
389
+ document.getElementById('editForm').style.display = 'none';
390
+ }
391
+ function saveProfile() {
392
+ const full_name = document.getElementById('editFullname').value;
393
+ const email = document.getElementById('editEmail').value;
394
+ const phone_number = document.getElementById('editPhone').value;
395
+ fetch('/api/update_teacher_profile', {
396
+ method: 'POST',
397
+ headers: {'Content-Type': 'application/json'},
398
+ body: JSON.stringify({full_name, email, phone_number})
399
+ })
400
+ .then(response => response.json())
401
+ .then(data => {
402
+ if (data.success) {
403
+ alert('Profile updated');
404
+ location.reload();
405
+ } else {
406
+ alert(data.message);
407
+ }
408
+ });
409
+ }
410
+ function loadStudents() {
411
+ const classSelected = document.getElementById('classSelect').value;
412
+ if (!classSelected) {
413
+ document.getElementById('studentsTable').style.display = 'none';
414
+ return;
415
+ }
416
+ fetch(`/get_students?class=${classSelected}`)
417
+ .then(response => response.json())
418
+ .then(data => {
419
+ const tbody = document.getElementById('studentsBody');
420
+ tbody.innerHTML = '';
421
+ data.students.forEach(student => {
422
+ const tr = document.createElement('tr');
423
+ tr.innerHTML = `<td><a href="#" onclick="showStudentDetails('${student.id}', '${student.full_name}')">${student.full_name}</a></td><td>${student.enrollment_number}</td>`;
424
+ tbody.appendChild(tr);
425
+ });
426
+ document.getElementById('studentsTable').style.display = 'table';
427
+ });
428
+ fetch('/api/select_teacher_class', {
429
+ method: 'POST',
430
+ headers: {'Content-Type': 'application/json'},
431
+ body: JSON.stringify({class: classSelected})
432
+ });
433
+ }
434
+ function showStudentDetails(studentId, fullName) {
435
+ fetch(`/get_student_details?id=${studentId}`)
436
+ .then(response => response.json())
437
+ .then(data => {
438
+ document.getElementById('studentName').innerHTML = `<i class="fas fa-user-graduate mr-2"></i>${fullName}`;
439
+ let all_percentages = [];
440
+
441
+ const regular_rows = data.regular_subjects.map(sub => {
442
+ const percent = sub.total_count > 0 ? (sub.attendance_count / sub.total_count * 100).toFixed(2) : 'N/A';
443
+ if (percent !== 'N/A') all_percentages.push(parseFloat(percent));
444
+ const percentClass = percent !== 'N/A' ?
445
+ (parseFloat(percent) >= 75 ? 'text-success' : 'text-danger') : '';
446
+ return `
447
+ <tr>
448
+ <td>${sub.name}</td>
449
+ <td>${sub.attendance_count}</td>
450
+ <td>${sub.total_count}</td>
451
+ <td class="${percentClass}">${percent === 'N/A' ? 'N/A' : percent + '%'}</td>
452
+ </tr>
453
+ `;
454
+ }).join('');
455
+
456
+ const specialization_rows = data.specialization_subjects.map(sub => {
457
+ const percent = sub.total_count > 0 ? (sub.attendance_count / sub.total_count * 100).toFixed(2) : 'N/A';
458
+ if (percent !== 'N/A') all_percentages.push(parseFloat(percent));
459
+ const percentClass = percent !== 'N/A' ?
460
+ (parseFloat(percent) >= 75 ? 'text-success' : 'text-danger') : '';
461
+ return `
462
+ <tr>
463
+ <td>${sub.name}</td>
464
+ <td>${sub.attendance_count}</td>
465
+ <td>${sub.total_count}</td>
466
+ <td class="${percentClass}">${percent === 'N/A' ? 'N/A' : percent + '%'}</td>
467
+ </tr>
468
+ `;
469
+ }).join('');
470
+
471
+ const practical_rows = data.practical_subjects.map(sub => {
472
+ const percent = sub.total_count > 0 ? (sub.attendance_count / sub.total_count * 100).toFixed(2) : 'N/A';
473
+ if (percent !== 'N/A') all_percentages.push(parseFloat(percent));
474
+ const percentClass = percent !== 'N/A' ?
475
+ (parseFloat(percent) >= 75 ? 'text-success' : 'text-danger') : '';
476
+ return `
477
+ <tr>
478
+ <td>${sub.name}</td>
479
+ <td>${sub.batch}</td>
480
+ <td>${sub.attendance_count}</td>
481
+ <td>${sub.total_count}</td>
482
+ <td class="${percentClass}">${percent === 'N/A' ? 'N/A' : percent + '%'}</td>
483
+ </tr>
484
+ `;
485
+ }).join('');
486
+
487
+ const average = all_percentages.length > 0 ? (all_percentages.reduce((a, b) => a + b, 0) / all_percentages.length).toFixed(2) + '%' : 'N/A';
488
+ const averageClass = average !== 'N/A' ?
489
+ (parseFloat(average) >= 75 ? 'text-success' : 'text-danger') : '';
490
+
491
+ const detailsDiv = document.getElementById('studentDetails');
492
+ detailsDiv.innerHTML = `
493
+ <div class="row">
494
+ <div class="col-md-6">
495
+ <p><strong><i class="fas fa-id-card mr-1"></i> Enrollment No:</strong> ${data.enrollment_number}</p>
496
+ <p><strong><i class="fas fa-envelope mr-1"></i> Email:</strong> ${data.email}</p>
497
+ <p><strong><i class="fas fa-phone mr-1"></i> Student No:</strong> ${data.student_number || '-'}</p>
498
+ </div>
499
+ <div class="col-md-6">
500
+ <p><strong><i class="fas fa-phone-alt mr-1"></i> Parent No:</strong> ${data.parent_number || '-'}</p>
501
+ <p><strong><i class="fas fa-envelope mr-1"></i> Parent Email:</strong> ${data.parent_email || '-'}</p>
502
+ <p><strong><i class="fas fa-percentage mr-1"></i> Overall Attendance Average:</strong> <span class="${averageClass}">${average}</span></p>
503
+ </div>
504
+ </div>
505
+
506
+ <div class="mb-2 mt-4">
507
+ <p><strong><i class="fas fa-camera mr-1"></i> Face Registration:</strong>
508
+ <span class="status ${data.face_registered ? 'status-registered' : 'status-not-registered'}">
509
+ ${data.face_registered ? 'Registered' : 'Not Registered'}
510
+ </span>
511
+ </p>
512
+ </div>
513
+
514
+ <h4><i class="fas fa-book mr-2"></i>Regular Subjects</h4>
515
+ <table>
516
+ <thead>
517
+ <tr>
518
+ <th>Subject</th>
519
+ <th>Attendance Count</th>
520
+ <th>Total Count</th>
521
+ <th>Percentage</th>
522
+ </tr>
523
+ </thead>
524
+ <tbody>
525
+ ${regular_rows}
526
+ </tbody>
527
+ </table>
528
+
529
+ <h4><i class="fas fa-graduation-cap mr-2"></i>Specialization Subjects</h4>
530
+ <table>
531
+ <thead>
532
+ <tr>
533
+ <th>Specialization Subject</th>
534
+ <th>Attendance Count</th>
535
+ <th>Total Count</th>
536
+ <th>Percentage</th>
537
+ </tr>
538
+ </thead>
539
+ <tbody>
540
+ ${specialization_rows}
541
+ </tbody>
542
+ </table>
543
+
544
+ <h4><i class="fas fa-flask mr-2"></i>Practical Subjects</h4>
545
+ <table>
546
+ <thead>
547
+ <tr>
548
+ <th>Practical Subject</th>
549
+ <th>Batch</th>
550
+ <th>Attendance Count</th>
551
+ <th>Total Count</th>
552
+ <th>Percentage</th>
553
+ </tr>
554
+ </thead>
555
+ <tbody>
556
+ ${practical_rows}
557
+ </tbody>
558
+ </table>
559
+ `;
560
+ document.getElementById('studentModal').style.display = 'block';
561
+ });
562
+ }
563
+ function closeStudentModal() {
564
+ document.getElementById('studentModal').style.display = 'none';
565
+ }
566
+ window.onload = function() {
567
+ const classSelect = document.getElementById('classSelect');
568
+ if (classSelect.value) {
569
+ loadStudents();
570
+ }
571
+ }
572
+ </script>
573
+ </body>
574
+ </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
+ <script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'93ecda760d73bf9d',t:'MTc0NzA4MzcwNy4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
692
+ </html>