ministerchief commited on
Commit
1c9424e
Β·
verified Β·
1 Parent(s): e7f1623

Upload 12 files

Browse files
__pycache__/app.cpython-312.pyc ADDED
Binary file (17.3 kB). View file
 
app.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, redirect, url_for, session, flash, g
2
+ import sqlite3
3
+ import os
4
+ import re
5
+ from datetime import datetime
6
+
7
+ app = Flask(__name__)
8
+ app.secret_key = 'student_system_secret_key_2024'
9
+
10
+ # Database path
11
+ DATABASE = os.path.join(os.path.dirname(__file__), 'database.db')
12
+
13
+ # ─────────────────────────────────────────
14
+ # Database Helpers
15
+ # ─────────────────────────────────────────
16
+
17
+ def get_db():
18
+ """Open a new database connection if not already open for this request."""
19
+ if 'db' not in g:
20
+ g.db = sqlite3.connect(DATABASE)
21
+ g.db.row_factory = sqlite3.Row # rows behave like dicts
22
+ return g.db
23
+
24
+ @app.teardown_appcontext
25
+ def close_db(error):
26
+ """Close database connection at end of request."""
27
+ db = g.pop('db', None)
28
+ if db is not None:
29
+ db.close()
30
+
31
+ def init_db():
32
+ """Create tables if they don't exist."""
33
+ db = sqlite3.connect(DATABASE)
34
+ cursor = db.cursor()
35
+
36
+ # Users table (admin login)
37
+ cursor.execute('''
38
+ CREATE TABLE IF NOT EXISTS users (
39
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
40
+ username TEXT NOT NULL UNIQUE,
41
+ password TEXT NOT NULL
42
+ )
43
+ ''')
44
+
45
+ # Students table
46
+ cursor.execute('''
47
+ CREATE TABLE IF NOT EXISTS students (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ student_id TEXT NOT NULL UNIQUE,
50
+ full_name TEXT NOT NULL,
51
+ father_name TEXT NOT NULL,
52
+ mother_name TEXT NOT NULL,
53
+ gender TEXT NOT NULL,
54
+ dob TEXT NOT NULL,
55
+ email TEXT NOT NULL UNIQUE,
56
+ phone TEXT NOT NULL,
57
+ course TEXT NOT NULL,
58
+ branch TEXT NOT NULL,
59
+ year_sem TEXT NOT NULL,
60
+ address TEXT NOT NULL,
61
+ city TEXT NOT NULL,
62
+ state TEXT NOT NULL,
63
+ pin_code TEXT NOT NULL,
64
+ created_at TEXT NOT NULL
65
+ )
66
+ ''')
67
+
68
+ # Insert default admin user (password: admin123)
69
+ cursor.execute('''
70
+ INSERT OR IGNORE INTO users (username, password)
71
+ VALUES (?, ?)
72
+ ''', ('admin', 'admin123'))
73
+
74
+ db.commit()
75
+ db.close()
76
+ print("Database initialised successfully.")
77
+
78
+ # ─────────────────────────────────────────
79
+ # Auth helpers
80
+ # ─────────────────────────────────────────
81
+
82
+ def login_required(f):
83
+ """Decorator: redirect to login if session missing."""
84
+ from functools import wraps
85
+ @wraps(f)
86
+ def decorated(*args, **kwargs):
87
+ if 'user' not in session:
88
+ flash('Please log in to access this page.', 'warning')
89
+ return redirect(url_for('login'))
90
+ return f(*args, **kwargs)
91
+ return decorated
92
+
93
+ # ─────────────────────────────────────────
94
+ # Validation helpers
95
+ # ─────────────────────────────────────────
96
+
97
+ def validate_student(data):
98
+ """Server-side validation; returns list of error strings."""
99
+ errors = []
100
+
101
+ if not data.get('student_id', '').strip():
102
+ errors.append('Student ID is required.')
103
+ if not data.get('full_name', '').strip():
104
+ errors.append('Full Name is required.')
105
+ if not data.get('father_name', '').strip():
106
+ errors.append('Father Name is required.')
107
+ if not data.get('mother_name', '').strip():
108
+ errors.append('Mother Name is required.')
109
+ if data.get('gender') not in ('Male', 'Female', 'Other'):
110
+ errors.append('Please select a valid Gender.')
111
+
112
+ # Date of birth
113
+ dob = data.get('dob', '').strip()
114
+ if not dob:
115
+ errors.append('Date of Birth is required.')
116
+ else:
117
+ try:
118
+ datetime.strptime(dob, '%Y-%m-%d')
119
+ except ValueError:
120
+ errors.append('Date of Birth format is invalid.')
121
+
122
+ # Email
123
+ email = data.get('email', '').strip()
124
+ if not email:
125
+ errors.append('Email is required.')
126
+ elif not re.match(r'^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$', email):
127
+ errors.append('Email address is invalid.')
128
+
129
+ # Phone (10 digits)
130
+ phone = data.get('phone', '').strip()
131
+ if not phone:
132
+ errors.append('Phone Number is required.')
133
+ elif not re.match(r'^\d{10}$', phone):
134
+ errors.append('Phone Number must be exactly 10 digits.')
135
+
136
+ if not data.get('course', '').strip():
137
+ errors.append('Course is required.')
138
+ if not data.get('branch', '').strip():
139
+ errors.append('Branch is required.')
140
+ if not data.get('year_sem', '').strip():
141
+ errors.append('Year / Semester is required.')
142
+ if not data.get('address', '').strip():
143
+ errors.append('Address is required.')
144
+ if not data.get('city', '').strip():
145
+ errors.append('City is required.')
146
+ if not data.get('state', '').strip():
147
+ errors.append('State is required.')
148
+
149
+ pin = data.get('pin_code', '').strip()
150
+ if not pin:
151
+ errors.append('Pin Code is required.')
152
+ elif not re.match(r'^\d{6}$', pin):
153
+ errors.append('Pin Code must be exactly 6 digits.')
154
+
155
+ return errors
156
+
157
+ # ─────────────────────────────────────────
158
+ # Routes
159
+ # ─────────────────────────────────────────
160
+
161
+ @app.route('/')
162
+ def index():
163
+ return redirect(url_for('login'))
164
+
165
+
166
+ # ── LOGIN ──────────────────────────────────
167
+ @app.route('/login', methods=['GET', 'POST'])
168
+ def login():
169
+ if 'user' in session:
170
+ return redirect(url_for('dashboard'))
171
+
172
+ if request.method == 'POST':
173
+ username = request.form.get('username', '').strip()
174
+ password = request.form.get('password', '').strip()
175
+
176
+ db = get_db()
177
+ user = db.execute(
178
+ 'SELECT * FROM users WHERE username=? AND password=?',
179
+ (username, password)
180
+ ).fetchone()
181
+
182
+ if user:
183
+ session['user'] = username
184
+ flash(f'Welcome back, {username}! πŸ‘‹', 'success')
185
+ return redirect(url_for('dashboard'))
186
+ else:
187
+ flash('Invalid username or password.', 'danger')
188
+
189
+ return render_template('login.html')
190
+
191
+
192
+ # ── LOGOUT ─────────────────────────────────
193
+ @app.route('/logout')
194
+ @login_required
195
+ def logout():
196
+ session.pop('user', None)
197
+ flash('You have been logged out.', 'info')
198
+ return redirect(url_for('login'))
199
+
200
+
201
+ # ── DASHBOARD ──────────────────────────────
202
+ @app.route('/dashboard')
203
+ @login_required
204
+ def dashboard():
205
+ db = get_db()
206
+
207
+ total = db.execute('SELECT COUNT(*) FROM students').fetchone()[0]
208
+ male = db.execute("SELECT COUNT(*) FROM students WHERE gender='Male'").fetchone()[0]
209
+ female = db.execute("SELECT COUNT(*) FROM students WHERE gender='Female'").fetchone()[0]
210
+ courses = db.execute('SELECT COUNT(DISTINCT course) FROM students').fetchone()[0]
211
+
212
+ # Recent 5 students
213
+ recent = db.execute(
214
+ 'SELECT student_id, full_name, course, branch FROM students ORDER BY id DESC LIMIT 5'
215
+ ).fetchall()
216
+
217
+ stats = {
218
+ 'total': total,
219
+ 'male': male,
220
+ 'female': female,
221
+ 'courses': courses,
222
+ }
223
+ return render_template('dashboard.html', stats=stats, recent=recent)
224
+
225
+
226
+ # ── ADD STUDENT ────────────────────────────
227
+ @app.route('/add_student', methods=['GET', 'POST'])
228
+ @login_required
229
+ def add_student():
230
+ if request.method == 'POST':
231
+ data = {k: v.strip() for k, v in request.form.items()}
232
+ errors = validate_student(data)
233
+
234
+ if errors:
235
+ for e in errors:
236
+ flash(e, 'danger')
237
+ return render_template('add_student.html', form=data)
238
+
239
+ db = get_db()
240
+
241
+ # Check duplicate student_id
242
+ dup_id = db.execute('SELECT id FROM students WHERE student_id=?', (data['student_id'],)).fetchone()
243
+ if dup_id:
244
+ flash('Student ID already exists. Use a unique ID.', 'danger')
245
+ return render_template('add_student.html', form=data)
246
+
247
+ # Check duplicate email
248
+ dup_email = db.execute('SELECT id FROM students WHERE email=?', (data['email'],)).fetchone()
249
+ if dup_email:
250
+ flash('Email address already registered.', 'danger')
251
+ return render_template('add_student.html', form=data)
252
+
253
+ db.execute('''
254
+ INSERT INTO students
255
+ (student_id, full_name, father_name, mother_name, gender, dob,
256
+ email, phone, course, branch, year_sem, address, city, state,
257
+ pin_code, created_at)
258
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
259
+ ''', (
260
+ data['student_id'], data['full_name'], data['father_name'],
261
+ data['mother_name'], data['gender'], data['dob'],
262
+ data['email'], data['phone'], data['course'], data['branch'],
263
+ data['year_sem'], data['address'], data['city'], data['state'],
264
+ data['pin_code'], datetime.now().strftime('%Y-%m-%d %H:%M:%S')
265
+ ))
266
+ db.commit()
267
+ flash(f'Student "{data["full_name"]}" added successfully! βœ…', 'success')
268
+ return redirect(url_for('view_students'))
269
+
270
+ return render_template('add_student.html', form={})
271
+
272
+
273
+ # ── VIEW STUDENTS ──────────────────────────
274
+ @app.route('/view_students')
275
+ @login_required
276
+ def view_students():
277
+ db = get_db()
278
+ q = request.args.get('q', '').strip()
279
+ col = request.args.get('col', 'full_name')
280
+
281
+ allowed_cols = {'full_name', 'student_id', 'course', 'phone'}
282
+ if col not in allowed_cols:
283
+ col = 'full_name'
284
+
285
+ if q:
286
+ students = db.execute(
287
+ f'SELECT * FROM students WHERE {col} LIKE ? ORDER BY id DESC',
288
+ (f'%{q}%',)
289
+ ).fetchall()
290
+ else:
291
+ students = db.execute('SELECT * FROM students ORDER BY id DESC').fetchall()
292
+
293
+ return render_template('view_students.html', students=students, q=q, col=col)
294
+
295
+
296
+ # ── EDIT STUDENT ───────────────────────────
297
+ @app.route('/edit_student/<int:sid>', methods=['GET', 'POST'])
298
+ @login_required
299
+ def edit_student(sid):
300
+ db = get_db()
301
+ student = db.execute('SELECT * FROM students WHERE id=?', (sid,)).fetchone()
302
+
303
+ if not student:
304
+ flash('Student not found.', 'danger')
305
+ return redirect(url_for('view_students'))
306
+
307
+ if request.method == 'POST':
308
+ data = {k: v.strip() for k, v in request.form.items()}
309
+ errors = validate_student(data)
310
+
311
+ if errors:
312
+ for e in errors:
313
+ flash(e, 'danger')
314
+ return render_template('edit_student.html', student=data, sid=sid)
315
+
316
+ # Duplicate checks (exclude current record)
317
+ dup_id = db.execute(
318
+ 'SELECT id FROM students WHERE student_id=? AND id!=?',
319
+ (data['student_id'], sid)
320
+ ).fetchone()
321
+ if dup_id:
322
+ flash('Student ID already used by another record.', 'danger')
323
+ return render_template('edit_student.html', student=data, sid=sid)
324
+
325
+ dup_email = db.execute(
326
+ 'SELECT id FROM students WHERE email=? AND id!=?',
327
+ (data['email'], sid)
328
+ ).fetchone()
329
+ if dup_email:
330
+ flash('Email already used by another record.', 'danger')
331
+ return render_template('edit_student.html', student=data, sid=sid)
332
+
333
+ db.execute('''
334
+ UPDATE students SET
335
+ student_id=?, full_name=?, father_name=?, mother_name=?,
336
+ gender=?, dob=?, email=?, phone=?, course=?, branch=?,
337
+ year_sem=?, address=?, city=?, state=?, pin_code=?
338
+ WHERE id=?
339
+ ''', (
340
+ data['student_id'], data['full_name'], data['father_name'],
341
+ data['mother_name'], data['gender'], data['dob'],
342
+ data['email'], data['phone'], data['course'], data['branch'],
343
+ data['year_sem'], data['address'], data['city'], data['state'],
344
+ data['pin_code'], sid
345
+ ))
346
+ db.commit()
347
+ flash(f'Student "{data["full_name"]}" updated successfully! βœ…', 'success')
348
+ return redirect(url_for('view_students'))
349
+
350
+ return render_template('edit_student.html', student=student, sid=sid)
351
+
352
+
353
+ # ── DELETE STUDENT ─────────────────────────
354
+ @app.route('/delete_student/<int:sid>', methods=['POST'])
355
+ @login_required
356
+ def delete_student(sid):
357
+ db = get_db()
358
+ student = db.execute('SELECT full_name FROM students WHERE id=?', (sid,)).fetchone()
359
+
360
+ if student:
361
+ db.execute('DELETE FROM students WHERE id=?', (sid,))
362
+ db.commit()
363
+ flash(f'Student "{student["full_name"]}" deleted successfully.', 'success')
364
+ else:
365
+ flash('Student not found.', 'danger')
366
+
367
+ return redirect(url_for('view_students'))
368
+
369
+
370
+ # ─────────────────────────────────────────
371
+ # Entry point
372
+ # ─────────────────────────────────────────
373
+ if __name__ == '__main__':
374
+ init_db()
375
+ app.run(debug=True)
database.db ADDED
Binary file (28.7 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Flask
2
+ Werkzeug
3
+ gunicorn
4
+ python-dotenv
static/css/style.css ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ═══════════════════════════════════════════
2
+ StudentMS β€” style.css
3
+ Font: Plus Jakarta Sans + JetBrains Mono
4
+ Theme: Deep navy with amber accent
5
+ ═══════════════════════════════════════════ */
6
+
7
+ /* ── VARIABLES ───────────────────────────── */
8
+ :root {
9
+ --navy: #0f172a;
10
+ --navy-2: #1e293b;
11
+ --navy-3: #334155;
12
+ --slate: #64748b;
13
+ --border: #e2e8f0;
14
+ --surface: #f8fafc;
15
+ --white: #ffffff;
16
+
17
+ --amber: #f59e0b;
18
+ --amber-light: #fef3c7;
19
+ --amber-dark: #d97706;
20
+
21
+ --blue: #3b82f6;
22
+ --blue-light: #dbeafe;
23
+ --green: #10b981;
24
+ --green-light: #d1fae5;
25
+ --pink: #ec4899;
26
+ --pink-light: #fce7f3;
27
+ --orange: #f97316;
28
+ --orange-light:#ffedd5;
29
+ --red: #ef4444;
30
+ --red-light: #fee2e2;
31
+
32
+ --radius: 12px;
33
+ --radius-sm: 8px;
34
+ --shadow-sm: 0 1px 3px rgba(0,0,0,.08);
35
+ --shadow: 0 4px 16px rgba(0,0,0,.10);
36
+ --shadow-md: 0 8px 32px rgba(0,0,0,.14);
37
+
38
+ --sidebar-w: 240px;
39
+ --navbar-h: 64px;
40
+ --font: 'Plus Jakarta Sans', sans-serif;
41
+ --mono: 'JetBrains Mono', monospace;
42
+ }
43
+
44
+ /* ── RESET ───────────────────────────────── */
45
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
46
+
47
+ html { scroll-behavior: smooth; }
48
+
49
+ body {
50
+ font-family: var(--font);
51
+ background: var(--surface);
52
+ color: var(--navy);
53
+ min-height: 100vh;
54
+ line-height: 1.6;
55
+ font-size: 15px;
56
+ }
57
+
58
+ a { color: var(--blue); text-decoration: none; }
59
+ a:hover { text-decoration: underline; }
60
+
61
+ /* ── NAVBAR ──────────────────────────────── */
62
+ .navbar {
63
+ position: sticky; top: 0; z-index: 100;
64
+ height: var(--navbar-h);
65
+ background: var(--navy);
66
+ display: flex; align-items: center;
67
+ padding: 0 24px;
68
+ gap: 24px;
69
+ box-shadow: 0 2px 12px rgba(0,0,0,.25);
70
+ }
71
+
72
+ .nav-brand {
73
+ display: flex; align-items: center; gap: 10px;
74
+ color: var(--white);
75
+ font-size: 1.2rem;
76
+ font-weight: 800;
77
+ letter-spacing: -0.5px;
78
+ min-width: 160px;
79
+ }
80
+ .nav-logo {
81
+ width: 36px; height: 36px;
82
+ background: var(--amber);
83
+ border-radius: 8px;
84
+ display: grid; place-items: center;
85
+ color: var(--navy);
86
+ font-size: 1rem;
87
+ }
88
+
89
+ .nav-links {
90
+ display: flex; align-items: center;
91
+ list-style: none;
92
+ gap: 4px;
93
+ flex: 1;
94
+ }
95
+ .nav-links a {
96
+ display: flex; align-items: center; gap: 7px;
97
+ padding: 8px 14px;
98
+ border-radius: 8px;
99
+ color: rgba(255,255,255,.70);
100
+ font-size: .875rem;
101
+ font-weight: 500;
102
+ transition: all .2s;
103
+ }
104
+ .nav-links a:hover { color: var(--white); background: rgba(255,255,255,.08); text-decoration: none; }
105
+ .nav-links a.active { color: var(--amber); background: rgba(245,158,11,.12); }
106
+
107
+ .nav-divider { flex: 1; }
108
+
109
+ .btn-logout {
110
+ display: flex; align-items: center; gap: 7px;
111
+ padding: 8px 14px;
112
+ border-radius: 8px;
113
+ color: rgba(255,255,255,.7) !important;
114
+ font-size: .875rem;
115
+ font-weight: 500;
116
+ transition: all .2s;
117
+ }
118
+ .btn-logout:hover {
119
+ color: var(--white) !important;
120
+ background: rgba(239,68,68,.18) !important;
121
+ text-decoration: none;
122
+ }
123
+
124
+ .nav-user {
125
+ display: flex; align-items: center; gap: 8px;
126
+ color: rgba(255,255,255,.65);
127
+ font-size: .85rem;
128
+ padding-left: 16px;
129
+ border-left: 1px solid rgba(255,255,255,.12);
130
+ }
131
+
132
+ .nav-toggle { display: none; background: none; border: none; color: var(--white); font-size: 1.2rem; cursor: pointer; }
133
+
134
+ /* ── MAIN CONTENT ────────────────────────── */
135
+ .main-content {
136
+ max-width: 1280px;
137
+ margin: 0 auto;
138
+ padding: 32px 24px;
139
+ }
140
+
141
+ /* ── PAGE HEADER ─────────────────────────── */
142
+ .page-header {
143
+ display: flex; align-items: flex-start;
144
+ justify-content: space-between;
145
+ margin-bottom: 28px;
146
+ gap: 16px;
147
+ flex-wrap: wrap;
148
+ }
149
+ .page-title {
150
+ font-size: 1.75rem;
151
+ font-weight: 800;
152
+ color: var(--navy);
153
+ line-height: 1.2;
154
+ }
155
+ .page-sub {
156
+ font-size: .9rem;
157
+ color: var(--slate);
158
+ margin-top: 4px;
159
+ }
160
+
161
+ /* ── CARDS ───────────────────────────────── */
162
+ .card {
163
+ background: var(--white);
164
+ border-radius: var(--radius);
165
+ border: 1px solid var(--border);
166
+ box-shadow: var(--shadow-sm);
167
+ overflow: hidden;
168
+ }
169
+ .card-header {
170
+ display: flex; align-items: center; justify-content: space-between;
171
+ padding: 20px 24px;
172
+ border-bottom: 1px solid var(--border);
173
+ gap: 12px;
174
+ }
175
+ .card-header h3 {
176
+ font-size: 1rem;
177
+ font-weight: 700;
178
+ color: var(--navy);
179
+ display: flex; align-items: center; gap: 8px;
180
+ }
181
+
182
+ .mt-3 { margin-top: 16px; }
183
+ .mt-4 { margin-top: 24px; }
184
+
185
+ /* ── STAT CARDS ──────────────────────────── */
186
+ .stats-grid {
187
+ display: grid;
188
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
189
+ gap: 16px;
190
+ }
191
+ .stat-card {
192
+ border-radius: var(--radius);
193
+ padding: 24px;
194
+ display: flex; align-items: center; gap: 18px;
195
+ box-shadow: var(--shadow-sm);
196
+ border: 1px solid transparent;
197
+ transition: transform .2s, box-shadow .2s;
198
+ }
199
+ .stat-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
200
+
201
+ .stat-icon {
202
+ width: 54px; height: 54px;
203
+ border-radius: 12px;
204
+ display: grid; place-items: center;
205
+ font-size: 1.4rem;
206
+ flex-shrink: 0;
207
+ }
208
+ .stat-number { display: block; font-size: 2rem; font-weight: 800; line-height: 1; }
209
+ .stat-label { display: block; font-size: .8rem; font-weight: 500; opacity: .75; margin-top: 4px; }
210
+
211
+ .stat-blue { background: var(--blue-light); color: #1d4ed8; } .stat-blue .stat-icon { background: #3b82f6; color: #fff; }
212
+ .stat-green { background: var(--green-light); color: #065f46; } .stat-green .stat-icon { background: #10b981; color: #fff; }
213
+ .stat-pink { background: var(--pink-light); color: #9d174d; } .stat-pink .stat-icon { background: #ec4899; color: #fff; }
214
+ .stat-orange { background: var(--orange-light); color: #c2410c; } .stat-orange .stat-icon { background: #f97316; color: #fff; }
215
+
216
+ /* ── QUICK LINKS ─────────────────────────── */
217
+ .quick-links {
218
+ display: flex; flex-wrap: wrap; gap: 12px;
219
+ }
220
+ .qlink {
221
+ display: flex; align-items: center; gap: 10px;
222
+ padding: 14px 20px;
223
+ border-radius: var(--radius);
224
+ background: var(--white);
225
+ border: 1px solid var(--border);
226
+ color: var(--navy);
227
+ font-weight: 600;
228
+ font-size: .9rem;
229
+ transition: all .2s;
230
+ box-shadow: var(--shadow-sm);
231
+ }
232
+ .qlink i { font-size: 1.1rem; color: var(--blue); }
233
+ .qlink:hover { background: var(--navy); color: var(--white); border-color: var(--navy); transform: translateY(-1px); box-shadow: var(--shadow); text-decoration: none; }
234
+ .qlink:hover i { color: var(--amber); }
235
+ .qlink-danger:hover { background: var(--red); border-color: var(--red); }
236
+ .qlink-danger:hover i { color: var(--white); }
237
+
238
+ /* ── TABLE ───────────────────────────────── */
239
+ .table-wrap { overflow-x: auto; }
240
+
241
+ .data-table {
242
+ width: 100%;
243
+ border-collapse: collapse;
244
+ font-size: .875rem;
245
+ }
246
+ .data-table thead tr {
247
+ background: var(--navy);
248
+ color: var(--white);
249
+ }
250
+ .data-table th {
251
+ padding: 13px 16px;
252
+ text-align: left;
253
+ font-weight: 600;
254
+ font-size: .8rem;
255
+ letter-spacing: .04em;
256
+ text-transform: uppercase;
257
+ white-space: nowrap;
258
+ }
259
+ .data-table td {
260
+ padding: 13px 16px;
261
+ border-bottom: 1px solid var(--border);
262
+ color: var(--navy);
263
+ white-space: nowrap;
264
+ }
265
+ .data-table tbody tr:last-child td { border-bottom: none; }
266
+ .data-table tbody tr:hover { background: #f1f5f9; }
267
+
268
+ .fw-600 { font-weight: 600; }
269
+ .text-small { font-size: .8rem; color: var(--slate); }
270
+
271
+ /* ── BADGES ──────────────────────────────── */
272
+ .badge {
273
+ display: inline-flex; align-items: center;
274
+ padding: 3px 10px;
275
+ border-radius: 99px;
276
+ font-size: .78rem;
277
+ font-weight: 600;
278
+ }
279
+ .badge-id { background: var(--navy); color: var(--amber); font-family: var(--mono); }
280
+ .badge-blue { background: var(--blue-light); color: #1d4ed8; }
281
+ .badge-pink { background: var(--pink-light); color: #9d174d; }
282
+ .badge-gray { background: #f1f5f9; color: var(--slate); }
283
+
284
+ /* ── ACTION BUTTONS ──────────────────────── */
285
+ .actions { display: flex; gap: 6px; }
286
+
287
+ .btn-icon {
288
+ width: 32px; height: 32px;
289
+ border-radius: 8px;
290
+ border: none;
291
+ cursor: pointer;
292
+ display: grid; place-items: center;
293
+ font-size: .85rem;
294
+ transition: all .2s;
295
+ text-decoration: none;
296
+ }
297
+ .btn-view { background: var(--blue-light); color: var(--blue); }
298
+ .btn-edit { background: var(--amber-light); color: var(--amber-dark); }
299
+ .btn-delete { background: var(--red-light); color: var(--red); }
300
+ .btn-icon:hover { filter: brightness(.9); transform: scale(1.05); }
301
+
302
+ /* ── BUTTONS ─────────────────────────────── */
303
+ .btn-primary, .btn-outline, .btn-ghost, .btn-danger {
304
+ display: inline-flex; align-items: center; gap: 8px;
305
+ padding: 10px 20px;
306
+ border-radius: var(--radius-sm);
307
+ font-size: .9rem;
308
+ font-weight: 600;
309
+ font-family: var(--font);
310
+ cursor: pointer;
311
+ transition: all .2s;
312
+ border: 2px solid transparent;
313
+ text-decoration: none;
314
+ }
315
+ .btn-primary { background: var(--amber); color: var(--navy); border-color: var(--amber); }
316
+ .btn-primary:hover { background: var(--amber-dark); border-color: var(--amber-dark); color: var(--white); text-decoration: none; transform: translateY(-1px); }
317
+
318
+ .btn-outline { background: transparent; color: var(--navy); border-color: var(--border); }
319
+ .btn-outline:hover { background: var(--navy); color: var(--white); border-color: var(--navy); text-decoration: none; }
320
+
321
+ .btn-ghost { background: var(--surface); color: var(--slate); border-color: var(--border); }
322
+ .btn-ghost:hover { background: var(--border); text-decoration: none; }
323
+
324
+ .btn-danger { background: var(--red); color: var(--white); border-color: var(--red); }
325
+ .btn-danger:hover { background: #dc2626; text-decoration: none; }
326
+
327
+ .btn-full { width: 100%; justify-content: center; padding: 13px; font-size: 1rem; }
328
+
329
+ /* ── EMPTY STATE ─────────────────────────── */
330
+ .empty-state {
331
+ text-align: center;
332
+ padding: 56px 24px;
333
+ color: var(--slate);
334
+ }
335
+ .empty-state i { font-size: 3rem; opacity: .3; display: block; margin-bottom: 16px; }
336
+ .empty-state p { font-size: 1rem; }
337
+
338
+ /* ── SEARCH BAR ──────────────────────────── */
339
+ .search-bar { padding: 16px 20px; }
340
+ .search-row {
341
+ display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
342
+ }
343
+ .search-input-wrap {
344
+ position: relative; flex: 1; min-width: 200px;
345
+ }
346
+ .search-icon {
347
+ position: absolute; left: 14px; top: 50%;
348
+ transform: translateY(-50%);
349
+ color: var(--slate); pointer-events: none;
350
+ }
351
+ .search-input-wrap input {
352
+ width: 100%;
353
+ padding: 10px 14px 10px 40px;
354
+ border: 1px solid var(--border);
355
+ border-radius: var(--radius-sm);
356
+ font-size: .9rem;
357
+ font-family: var(--font);
358
+ outline: none;
359
+ transition: border-color .2s;
360
+ }
361
+ .search-input-wrap input:focus { border-color: var(--amber); box-shadow: 0 0 0 3px rgba(245,158,11,.15); }
362
+ .clear-search {
363
+ position: absolute; right: 12px; top: 50%;
364
+ transform: translateY(-50%);
365
+ background: none; border: none; color: var(--slate);
366
+ cursor: pointer; font-size: .9rem;
367
+ }
368
+ .search-col {
369
+ padding: 10px 14px;
370
+ border: 1px solid var(--border);
371
+ border-radius: var(--radius-sm);
372
+ font-family: var(--font);
373
+ font-size: .9rem;
374
+ background: var(--white);
375
+ color: var(--navy);
376
+ outline: none;
377
+ cursor: pointer;
378
+ }
379
+
380
+ /* ── FLASH MESSAGES ──────────────────────── */
381
+ .flash-container {
382
+ position: fixed; top: 72px; right: 20px;
383
+ z-index: 200;
384
+ display: flex; flex-direction: column; gap: 8px;
385
+ max-width: 380px;
386
+ }
387
+ .alert {
388
+ display: flex; align-items: flex-start; gap: 10px;
389
+ padding: 14px 16px;
390
+ border-radius: var(--radius-sm);
391
+ font-size: .9rem;
392
+ font-weight: 500;
393
+ box-shadow: var(--shadow-md);
394
+ animation: slideIn .3s ease;
395
+ }
396
+ @keyframes slideIn {
397
+ from { opacity: 0; transform: translateX(40px); }
398
+ to { opacity: 1; transform: translateX(0); }
399
+ }
400
+ .alert-success { background: var(--green-light); color: #065f46; border: 1px solid #a7f3d0; }
401
+ .alert-danger { background: var(--red-light); color: #991b1b; border: 1px solid #fca5a5; }
402
+ .alert-warning { background: var(--amber-light); color: #92400e; border: 1px solid #fcd34d; }
403
+ .alert-info { background: var(--blue-light); color: #1e40af; border: 1px solid #93c5fd; }
404
+
405
+ .alert i { margin-top: 2px; flex-shrink: 0; }
406
+ .alert-close {
407
+ margin-left: auto; background: none; border: none;
408
+ font-size: 1.1rem; cursor: pointer; opacity: .6; padding: 0 0 0 8px;
409
+ }
410
+ .alert-close:hover { opacity: 1; }
411
+
412
+ /* ── FORMS ───────────────────────────────── */
413
+ .form-section {
414
+ padding: 24px;
415
+ border-bottom: 1px solid var(--border);
416
+ }
417
+ .form-section:last-of-type { border-bottom: none; }
418
+ .form-section-title {
419
+ font-size: .85rem;
420
+ font-weight: 700;
421
+ text-transform: uppercase;
422
+ letter-spacing: .06em;
423
+ color: var(--slate);
424
+ margin-bottom: 20px;
425
+ display: flex; align-items: center; gap: 8px;
426
+ }
427
+ .form-section-title i { color: var(--amber); }
428
+
429
+ .form-grid {
430
+ display: grid;
431
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
432
+ gap: 18px;
433
+ }
434
+ .form-group { display: flex; flex-direction: column; gap: 6px; }
435
+ .form-group-full { grid-column: 1 / -1; }
436
+
437
+ .form-group label {
438
+ font-size: .85rem;
439
+ font-weight: 600;
440
+ color: var(--navy-3);
441
+ }
442
+ .req { color: var(--red); margin-left: 2px; }
443
+
444
+ .form-group input,
445
+ .form-group select,
446
+ .form-group textarea {
447
+ padding: 10px 14px;
448
+ border: 1.5px solid var(--border);
449
+ border-radius: var(--radius-sm);
450
+ font-size: .9rem;
451
+ font-family: var(--font);
452
+ color: var(--navy);
453
+ background: var(--white);
454
+ outline: none;
455
+ transition: all .2s;
456
+ }
457
+ .form-group input:focus,
458
+ .form-group select:focus,
459
+ .form-group textarea:focus {
460
+ border-color: var(--amber);
461
+ box-shadow: 0 0 0 3px rgba(245,158,11,.15);
462
+ }
463
+ .form-group input.is-invalid,
464
+ .form-group select.is-invalid,
465
+ .form-group textarea.is-invalid {
466
+ border-color: var(--red);
467
+ box-shadow: 0 0 0 3px rgba(239,68,68,.12);
468
+ }
469
+ .form-group textarea { resize: vertical; min-height: 80px; }
470
+ .field-error { font-size: .8rem; color: var(--red); min-height: 18px; }
471
+
472
+ .form-actions {
473
+ display: flex; gap: 12px; align-items: center;
474
+ padding: 20px 24px;
475
+ background: var(--surface);
476
+ border-top: 1px solid var(--border);
477
+ flex-wrap: wrap;
478
+ }
479
+
480
+ /* ── LOGIN PAGE ──────────────────────────── */
481
+ .login-wrapper {
482
+ min-height: 100vh;
483
+ display: flex;
484
+ }
485
+ .login-panel-left {
486
+ flex: 1;
487
+ background: var(--navy);
488
+ display: flex; flex-direction: column;
489
+ justify-content: center; align-items: flex-start;
490
+ padding: 60px;
491
+ gap: 40px;
492
+ }
493
+ .login-brand { color: var(--white); }
494
+ .login-icon {
495
+ width: 64px; height: 64px;
496
+ background: var(--amber);
497
+ border-radius: 16px;
498
+ display: grid; place-items: center;
499
+ font-size: 1.8rem; color: var(--navy);
500
+ margin-bottom: 20px;
501
+ }
502
+ .login-brand h1 {
503
+ font-size: 2.5rem; font-weight: 800;
504
+ letter-spacing: -1px;
505
+ background: linear-gradient(135deg, #fff 0%, var(--amber) 100%);
506
+ -webkit-background-clip: text;
507
+ -webkit-text-fill-color: transparent;
508
+ }
509
+ .login-brand p { color: rgba(255,255,255,.6); margin-top: 6px; }
510
+ .login-features { list-style: none; display: flex; flex-direction: column; gap: 14px; }
511
+ .login-features li {
512
+ display: flex; align-items: center; gap: 12px;
513
+ color: rgba(255,255,255,.8); font-size: .95rem;
514
+ }
515
+ .login-features i { color: var(--amber); }
516
+
517
+ .login-panel-right {
518
+ width: 480px;
519
+ display: flex; align-items: center; justify-content: center;
520
+ background: var(--surface);
521
+ padding: 40px;
522
+ }
523
+ .login-card {
524
+ width: 100%;
525
+ max-width: 400px;
526
+ background: var(--white);
527
+ border-radius: 20px;
528
+ padding: 40px;
529
+ box-shadow: var(--shadow-md);
530
+ border: 1px solid var(--border);
531
+ }
532
+ .login-header { margin-bottom: 32px; text-align: center; }
533
+ .login-header h2 { font-size: 1.75rem; font-weight: 800; color: var(--navy); }
534
+ .login-header p { color: var(--slate); font-size: .9rem; margin-top: 6px; }
535
+
536
+ .login-card .form-group { margin-bottom: 20px; }
537
+ .login-card .form-group label {
538
+ display: flex; align-items: center; gap: 7px;
539
+ font-size: .875rem; font-weight: 600; color: var(--navy-3);
540
+ margin-bottom: 8px;
541
+ }
542
+ .input-icon-wrap { position: relative; }
543
+ .input-icon-wrap input { width: 100%; padding-right: 44px; }
544
+ .toggle-pw {
545
+ position: absolute; right: 12px; top: 50%;
546
+ transform: translateY(-50%);
547
+ background: none; border: none;
548
+ color: var(--slate); cursor: pointer;
549
+ }
550
+
551
+ .login-hint {
552
+ text-align: center;
553
+ font-size: .82rem;
554
+ color: var(--slate);
555
+ margin-top: 20px;
556
+ }
557
+ .login-hint code {
558
+ font-family: var(--mono);
559
+ background: var(--surface);
560
+ padding: 2px 6px; border-radius: 4px;
561
+ font-size: .82rem;
562
+ color: var(--amber-dark);
563
+ }
564
+
565
+ /* ── MODAL ───────────────────────────────── */
566
+ .modal-backdrop {
567
+ position: fixed; inset: 0;
568
+ background: rgba(0,0,0,.45);
569
+ display: flex; align-items: center; justify-content: center;
570
+ z-index: 500;
571
+ animation: fadeBg .2s ease;
572
+ }
573
+ @keyframes fadeBg { from { opacity: 0; } to { opacity: 1; } }
574
+
575
+ .modal {
576
+ background: var(--white);
577
+ border-radius: 20px;
578
+ padding: 36px;
579
+ width: 90%; max-width: 400px;
580
+ text-align: center;
581
+ box-shadow: var(--shadow-md);
582
+ animation: popIn .25s ease;
583
+ }
584
+ @keyframes popIn {
585
+ from { opacity: 0; transform: scale(.9); }
586
+ to { opacity: 1; transform: scale(1); }
587
+ }
588
+ .modal-icon {
589
+ width: 60px; height: 60px;
590
+ border-radius: 50%;
591
+ display: grid; place-items: center;
592
+ font-size: 1.5rem;
593
+ margin: 0 auto 16px;
594
+ }
595
+ .modal-icon.danger { background: var(--red-light); color: var(--red); }
596
+ .modal h3 { font-size: 1.3rem; font-weight: 800; margin-bottom: 10px; }
597
+ .modal p { color: var(--slate); font-size: .9rem; line-height: 1.6; }
598
+ .modal-actions { display: flex; gap: 10px; justify-content: center; margin-top: 24px; }
599
+
600
+ /* ── RESPONSIVE ──────────────────────────── */
601
+ @media (max-width: 900px) {
602
+ .login-panel-left { display: none; }
603
+ .login-panel-right { width: 100%; }
604
+ }
605
+ @media (max-width: 768px) {
606
+ .nav-links { display: none; flex-direction: column; position: absolute; top: var(--navbar-h); left: 0; right: 0; background: var(--navy); padding: 12px; gap: 4px; }
607
+ .nav-links.open { display: flex; }
608
+ .nav-toggle { display: block; }
609
+ .navbar { position: relative; flex-wrap: wrap; }
610
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
611
+ .form-grid { grid-template-columns: 1fr; }
612
+ .main-content { padding: 20px 16px; }
613
+ .page-header { flex-direction: column; align-items: flex-start; }
614
+ .flash-container { right: 10px; left: 10px; max-width: none; }
615
+ .quick-links { flex-direction: column; }
616
+ }
617
+ @media (max-width: 480px) {
618
+ .stats-grid { grid-template-columns: 1fr; }
619
+ .search-row { flex-direction: column; align-items: stretch; }
620
+ .login-card { padding: 28px 20px; }
621
+ }
static/js/script.js ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * StudentMS β€” script.js
3
+ * Client-side validation, modals, and UI helpers.
4
+ */
5
+
6
+ /* ═══════════════════════════════════════════
7
+ 1. AUTO-DISMISS FLASH MESSAGES
8
+ ═══════════════════════════════════════════ */
9
+ document.addEventListener('DOMContentLoaded', () => {
10
+ const alerts = document.querySelectorAll('.alert');
11
+ alerts.forEach(alert => {
12
+ setTimeout(() => {
13
+ alert.style.transition = 'opacity .4s, transform .4s';
14
+ alert.style.opacity = '0';
15
+ alert.style.transform = 'translateX(40px)';
16
+ setTimeout(() => alert.remove(), 420);
17
+ }, 4500);
18
+ });
19
+ });
20
+
21
+ /* ═══════════════════════════════════════════
22
+ 2. MOBILE NAVBAR TOGGLE
23
+ ═══════════════════════════════════════════ */
24
+ const navToggle = document.getElementById('navToggle');
25
+ const navLinks = document.getElementById('navLinks');
26
+ if (navToggle && navLinks) {
27
+ navToggle.addEventListener('click', () => {
28
+ navLinks.classList.toggle('open');
29
+ });
30
+ // Close when clicking outside
31
+ document.addEventListener('click', (e) => {
32
+ if (!navToggle.contains(e.target) && !navLinks.contains(e.target)) {
33
+ navLinks.classList.remove('open');
34
+ }
35
+ });
36
+ }
37
+
38
+ /* ═══════════════════════════════════════════
39
+ 3. PASSWORD TOGGLE
40
+ ═══════════════════════════════════════════ */
41
+ function togglePassword(inputId, btn) {
42
+ const input = document.getElementById(inputId);
43
+ const icon = btn.querySelector('i');
44
+ if (input.type === 'password') {
45
+ input.type = 'text';
46
+ icon.classList.replace('fa-eye', 'fa-eye-slash');
47
+ } else {
48
+ input.type = 'password';
49
+ icon.classList.replace('fa-eye-slash', 'fa-eye');
50
+ }
51
+ }
52
+
53
+ /* ═══════════════════════════════════════════
54
+ 4. LOGIN FORM VALIDATION
55
+ ═══════════════════════════════════════════ */
56
+ const loginForm = document.getElementById('loginForm');
57
+ if (loginForm) {
58
+ loginForm.addEventListener('submit', (e) => {
59
+ let valid = true;
60
+
61
+ const username = document.getElementById('username');
62
+ const password = document.getElementById('password');
63
+ const errUser = document.getElementById('err-username');
64
+ const errPass = document.getElementById('err-password');
65
+
66
+ // Reset
67
+ [errUser, errPass].forEach(el => { if (el) el.textContent = ''; });
68
+ [username, password].forEach(el => el.classList.remove('is-invalid'));
69
+
70
+ if (!username.value.trim()) {
71
+ errUser.textContent = 'Username is required.';
72
+ username.classList.add('is-invalid');
73
+ valid = false;
74
+ }
75
+ if (!password.value.trim()) {
76
+ errPass.textContent = 'Password is required.';
77
+ password.classList.add('is-invalid');
78
+ valid = false;
79
+ }
80
+
81
+ if (!valid) e.preventDefault();
82
+ });
83
+ }
84
+
85
+ /* ═══════════════════════════════════════════
86
+ 5. STUDENT FORM VALIDATION (add & edit)
87
+ ═══════════════════════════════════════════ */
88
+ const studentForm = document.getElementById('studentForm');
89
+ if (studentForm) {
90
+ studentForm.addEventListener('submit', (e) => {
91
+ const errors = validateStudentForm();
92
+ if (errors > 0) e.preventDefault();
93
+ });
94
+ }
95
+
96
+ function validateStudentForm() {
97
+ let errorCount = 0;
98
+
99
+ // Helper: show error
100
+ function showErr(fieldName, msg) {
101
+ const errEl = document.getElementById(`err-${fieldName}`);
102
+ const input = studentForm.querySelector(`[name="${fieldName}"]`);
103
+ if (errEl) errEl.textContent = msg;
104
+ if (input) input.classList.add('is-invalid');
105
+ errorCount++;
106
+ }
107
+
108
+ // Helper: clear error
109
+ function clearErr(fieldName) {
110
+ const errEl = document.getElementById(`err-${fieldName}`);
111
+ const input = studentForm.querySelector(`[name="${fieldName}"]`);
112
+ if (errEl) errEl.textContent = '';
113
+ if (input) input.classList.remove('is-invalid');
114
+ }
115
+
116
+ // Fields to validate
117
+ const required = [
118
+ 'student_id', 'full_name', 'father_name', 'mother_name',
119
+ 'gender', 'dob', 'email', 'phone',
120
+ 'course', 'branch', 'year_sem',
121
+ 'address', 'city', 'state', 'pin_code'
122
+ ];
123
+
124
+ required.forEach(name => clearErr(name));
125
+
126
+ // Required check
127
+ required.forEach(name => {
128
+ const el = studentForm.querySelector(`[name="${name}"]`);
129
+ if (!el) return;
130
+ if (!el.value.trim()) {
131
+ const label = name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
132
+ showErr(name, `${label} is required.`);
133
+ }
134
+ });
135
+
136
+ // Email format
137
+ const emailEl = studentForm.querySelector('[name="email"]');
138
+ if (emailEl && emailEl.value.trim()) {
139
+ const re = /^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$/;
140
+ if (!re.test(emailEl.value.trim())) {
141
+ showErr('email', 'Enter a valid email address.');
142
+ }
143
+ }
144
+
145
+ // Phone β€” 10 digits only
146
+ const phoneEl = studentForm.querySelector('[name="phone"]');
147
+ if (phoneEl && phoneEl.value.trim()) {
148
+ if (!/^\d{10}$/.test(phoneEl.value.trim())) {
149
+ showErr('phone', 'Phone must be exactly 10 digits.');
150
+ }
151
+ }
152
+
153
+ // Pin code β€” 6 digits only
154
+ const pinEl = studentForm.querySelector('[name="pin_code"]');
155
+ if (pinEl && pinEl.value.trim()) {
156
+ if (!/^\d{6}$/.test(pinEl.value.trim())) {
157
+ showErr('pin_code', 'Pin Code must be exactly 6 digits.');
158
+ }
159
+ }
160
+
161
+ // Date of birth β€” must not be in the future
162
+ const dobEl = studentForm.querySelector('[name="dob"]');
163
+ if (dobEl && dobEl.value) {
164
+ const dob = new Date(dobEl.value);
165
+ if (dob >= new Date()) {
166
+ showErr('dob', 'Date of Birth cannot be in the future.');
167
+ }
168
+ }
169
+
170
+ return errorCount;
171
+ }
172
+
173
+ // Live phone & pin – allow only digits
174
+ document.addEventListener('DOMContentLoaded', () => {
175
+ const phoneInput = document.querySelector('[name="phone"]');
176
+ const pinInput = document.querySelector('[name="pin_code"]');
177
+
178
+ if (phoneInput) {
179
+ phoneInput.addEventListener('input', () => {
180
+ phoneInput.value = phoneInput.value.replace(/\D/g, '').slice(0, 10);
181
+ });
182
+ }
183
+ if (pinInput) {
184
+ pinInput.addEventListener('input', () => {
185
+ pinInput.value = pinInput.value.replace(/\D/g, '').slice(0, 6);
186
+ });
187
+ }
188
+ });
189
+
190
+ /* ═══════════════════════════════════════════
191
+ 6. DELETE MODAL
192
+ ═══════════════════════════════════════════ */
193
+ let pendingDeleteId = null;
194
+
195
+ function confirmDelete(studentId, studentName) {
196
+ pendingDeleteId = studentId;
197
+ const modal = document.getElementById('deleteModal');
198
+ const nameEl = document.getElementById('deleteStudentName');
199
+ if (nameEl) nameEl.textContent = studentName;
200
+ if (modal) modal.style.display = 'flex';
201
+ }
202
+
203
+ function closeModal() {
204
+ const modal = document.getElementById('deleteModal');
205
+ if (modal) modal.style.display = 'none';
206
+ pendingDeleteId = null;
207
+ }
208
+
209
+ function submitDelete() {
210
+ if (!pendingDeleteId) return;
211
+ const form = document.getElementById('deleteForm');
212
+ if (form) {
213
+ form.action = `/delete_student/${pendingDeleteId}`;
214
+ form.submit();
215
+ }
216
+ }
217
+
218
+ // Close modal when clicking backdrop
219
+ document.addEventListener('click', (e) => {
220
+ const modal = document.getElementById('deleteModal');
221
+ if (modal && e.target === modal) closeModal();
222
+ });
223
+
224
+ // Close modal on Escape key
225
+ document.addEventListener('keydown', (e) => {
226
+ if (e.key === 'Escape') closeModal();
227
+ });
228
+
229
+ /* ═══════════════════════════════════════════
230
+ 7. SEARCH – CLEAR
231
+ ═══════════════════════════════════════════ */
232
+ function clearSearch() {
233
+ const input = document.getElementById('searchInput');
234
+ if (input) {
235
+ input.value = '';
236
+ document.getElementById('searchForm').submit();
237
+ }
238
+ }
templates/add_student.html ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'base.html' %}
2
+ {% block title %}Add Student β€” StudentMS{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="page-header">
6
+ <div>
7
+ <h2 class="page-title">Add New Student</h2>
8
+ <p class="page-sub">Fill in the form below to register a student</p>
9
+ </div>
10
+ <a href="{{ url_for('view_students') }}" class="btn-outline">
11
+ <i class="fas fa-arrow-left"></i> Back
12
+ </a>
13
+ </div>
14
+
15
+ <div class="card">
16
+ <form method="POST" action="{{ url_for('add_student') }}" id="studentForm" novalidate>
17
+
18
+ <!-- ── SECTION 1: Identity ── -->
19
+ <div class="form-section">
20
+ <div class="form-section-title"><i class="fas fa-id-card"></i> Student Identity</div>
21
+ <div class="form-grid">
22
+
23
+ <div class="form-group">
24
+ <label>Student ID <span class="req">*</span></label>
25
+ <input type="text" name="student_id" value="{{ form.get('student_id','') }}"
26
+ placeholder="e.g. STU2024001" />
27
+ <span class="field-error" id="err-student_id"></span>
28
+ </div>
29
+
30
+ <div class="form-group">
31
+ <label>Full Name <span class="req">*</span></label>
32
+ <input type="text" name="full_name" value="{{ form.get('full_name','') }}"
33
+ placeholder="Full legal name" />
34
+ <span class="field-error" id="err-full_name"></span>
35
+ </div>
36
+
37
+ <div class="form-group">
38
+ <label>Father Name <span class="req">*</span></label>
39
+ <input type="text" name="father_name" value="{{ form.get('father_name','') }}"
40
+ placeholder="Father's full name" />
41
+ <span class="field-error" id="err-father_name"></span>
42
+ </div>
43
+
44
+ <div class="form-group">
45
+ <label>Mother Name <span class="req">*</span></label>
46
+ <input type="text" name="mother_name" value="{{ form.get('mother_name','') }}"
47
+ placeholder="Mother's full name" />
48
+ <span class="field-error" id="err-mother_name"></span>
49
+ </div>
50
+
51
+ <div class="form-group">
52
+ <label>Gender <span class="req">*</span></label>
53
+ <select name="gender">
54
+ <option value="" disabled {% if not form.get('gender') %}selected{% endif %}>Select gender</option>
55
+ {% for g in ['Male','Female','Other'] %}
56
+ <option value="{{ g }}" {% if form.get('gender')==g %}selected{% endif %}>{{ g }}</option>
57
+ {% endfor %}
58
+ </select>
59
+ <span class="field-error" id="err-gender"></span>
60
+ </div>
61
+
62
+ <div class="form-group">
63
+ <label>Date of Birth <span class="req">*</span></label>
64
+ <input type="date" name="dob" value="{{ form.get('dob','') }}" />
65
+ <span class="field-error" id="err-dob"></span>
66
+ </div>
67
+
68
+ </div>
69
+ </div>
70
+
71
+ <!-- ── SECTION 2: Contact ── -->
72
+ <div class="form-section">
73
+ <div class="form-section-title"><i class="fas fa-envelope"></i> Contact Details</div>
74
+ <div class="form-grid">
75
+
76
+ <div class="form-group">
77
+ <label>Email <span class="req">*</span></label>
78
+ <input type="email" name="email" value="{{ form.get('email','') }}"
79
+ placeholder="student@example.com" />
80
+ <span class="field-error" id="err-email"></span>
81
+ </div>
82
+
83
+ <div class="form-group">
84
+ <label>Phone Number <span class="req">*</span></label>
85
+ <input type="tel" name="phone" value="{{ form.get('phone','') }}"
86
+ placeholder="10-digit number" maxlength="10" />
87
+ <span class="field-error" id="err-phone"></span>
88
+ </div>
89
+
90
+ </div>
91
+ </div>
92
+
93
+ <!-- ── SECTION 3: Academic ── -->
94
+ <div class="form-section">
95
+ <div class="form-section-title"><i class="fas fa-book-open"></i> Academic Details</div>
96
+ <div class="form-grid">
97
+
98
+ <div class="form-group">
99
+ <label>Course <span class="req">*</span></label>
100
+ <select name="course">
101
+ <option value="" disabled {% if not form.get('course') %}selected{% endif %}>Select course</option>
102
+ {% for c in ['B.Tech','M.Tech','BCA','MCA','B.Sc','M.Sc','B.Com','M.Com','BBA','MBA','BA','MA','B.Pharm','Diploma'] %}
103
+ <option value="{{ c }}" {% if form.get('course')==c %}selected{% endif %}>{{ c }}</option>
104
+ {% endfor %}
105
+ </select>
106
+ <span class="field-error" id="err-course"></span>
107
+ </div>
108
+
109
+ <div class="form-group">
110
+ <label>Branch <span class="req">*</span></label>
111
+ <input type="text" name="branch" value="{{ form.get('branch','') }}"
112
+ placeholder="e.g. Computer Science" />
113
+ <span class="field-error" id="err-branch"></span>
114
+ </div>
115
+
116
+ <div class="form-group">
117
+ <label>Year / Semester <span class="req">*</span></label>
118
+ <select name="year_sem">
119
+ <option value="" disabled {% if not form.get('year_sem') %}selected{% endif %}>Select year/semester</option>
120
+ {% for y in ['1st Year / 1st Sem','1st Year / 2nd Sem','2nd Year / 3rd Sem','2nd Year / 4th Sem','3rd Year / 5th Sem','3rd Year / 6th Sem','4th Year / 7th Sem','4th Year / 8th Sem'] %}
121
+ <option value="{{ y }}" {% if form.get('year_sem')==y %}selected{% endif %}>{{ y }}</option>
122
+ {% endfor %}
123
+ </select>
124
+ <span class="field-error" id="err-year_sem"></span>
125
+ </div>
126
+
127
+ </div>
128
+ </div>
129
+
130
+ <!-- ── SECTION 4: Address ── -->
131
+ <div class="form-section">
132
+ <div class="form-section-title"><i class="fas fa-location-dot"></i> Address</div>
133
+ <div class="form-grid">
134
+
135
+ <div class="form-group form-group-full">
136
+ <label>Address <span class="req">*</span></label>
137
+ <textarea name="address" rows="2" placeholder="Street / Locality">{{ form.get('address','') }}</textarea>
138
+ <span class="field-error" id="err-address"></span>
139
+ </div>
140
+
141
+ <div class="form-group">
142
+ <label>City <span class="req">*</span></label>
143
+ <input type="text" name="city" value="{{ form.get('city','') }}"
144
+ placeholder="City name" />
145
+ <span class="field-error" id="err-city"></span>
146
+ </div>
147
+
148
+ <div class="form-group">
149
+ <label>State <span class="req">*</span></label>
150
+ <select name="state">
151
+ <option value="" disabled {% if not form.get('state') %}selected{% endif %}>Select state</option>
152
+ {% for st in ['Andhra Pradesh','Arunachal Pradesh','Assam','Bihar','Chhattisgarh','Goa','Gujarat','Haryana','Himachal Pradesh','Jharkhand','Karnataka','Kerala','Madhya Pradesh','Maharashtra','Manipur','Meghalaya','Mizoram','Nagaland','Odisha','Punjab','Rajasthan','Sikkim','Tamil Nadu','Telangana','Tripura','Uttar Pradesh','Uttarakhand','West Bengal','Delhi','Jammu & Kashmir','Ladakh','Chandigarh','Puducherry'] %}
153
+ <option value="{{ st }}" {% if form.get('state')==st %}selected{% endif %}>{{ st }}</option>
154
+ {% endfor %}
155
+ </select>
156
+ <span class="field-error" id="err-state"></span>
157
+ </div>
158
+
159
+ <div class="form-group">
160
+ <label>Pin Code <span class="req">*</span></label>
161
+ <input type="text" name="pin_code" value="{{ form.get('pin_code','') }}"
162
+ placeholder="6-digit pin" maxlength="6" />
163
+ <span class="field-error" id="err-pin_code"></span>
164
+ </div>
165
+
166
+ </div>
167
+ </div>
168
+
169
+ <!-- ── BUTTONS ── -->
170
+ <div class="form-actions">
171
+ <button type="submit" class="btn-primary">
172
+ <i class="fas fa-save"></i> Save Student
173
+ </button>
174
+ <button type="reset" class="btn-ghost">
175
+ <i class="fas fa-rotate-left"></i> Reset
176
+ </button>
177
+ <a href="{{ url_for('view_students') }}" class="btn-outline">Cancel</a>
178
+ </div>
179
+
180
+ </form>
181
+ </div>
182
+ {% endblock %}
templates/base.html ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>{% block title %}Student Data Entry System{% endblock %}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
11
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" />
12
+ </head>
13
+ <body>
14
+
15
+ {% if session.user %}
16
+ <!-- ── NAVBAR ── -->
17
+ <nav class="navbar">
18
+ <div class="nav-brand">
19
+ <span class="nav-logo"><i class="fas fa-graduation-cap"></i></span>
20
+ <span class="nav-title">StudentMS</span>
21
+ </div>
22
+
23
+ <button class="nav-toggle" id="navToggle" aria-label="Menu">
24
+ <i class="fas fa-bars"></i>
25
+ </button>
26
+
27
+ <ul class="nav-links" id="navLinks">
28
+ <li><a href="{{ url_for('dashboard') }}" class="{% if request.endpoint=='dashboard' %}active{% endif %}"><i class="fas fa-chart-pie"></i> Dashboard</a></li>
29
+ <li><a href="{{ url_for('add_student') }}" class="{% if request.endpoint=='add_student' %}active{% endif %}"><i class="fas fa-user-plus"></i> Add Student</a></li>
30
+ <li><a href="{{ url_for('view_students') }}" class="{% if request.endpoint=='view_students' %}active{% endif %}"><i class="fas fa-table"></i> All Students</a></li>
31
+ <li class="nav-divider"></li>
32
+ <li>
33
+ <a href="{{ url_for('logout') }}" class="btn-logout">
34
+ <i class="fas fa-sign-out-alt"></i> Logout
35
+ </a>
36
+ </li>
37
+ </ul>
38
+
39
+ <div class="nav-user">
40
+ <i class="fas fa-user-shield"></i>
41
+ <span>{{ session.user }}</span>
42
+ </div>
43
+ </nav>
44
+ {% endif %}
45
+
46
+ <!-- ── FLASH MESSAGES ── -->
47
+ <div class="flash-container">
48
+ {% with messages = get_flashed_messages(with_categories=true) %}
49
+ {% if messages %}
50
+ {% for category, message in messages %}
51
+ <div class="alert alert-{{ category }}">
52
+ <i class="fas {% if category=='success' %}fa-circle-check{% elif category=='danger' %}fa-circle-exclamation{% elif category=='warning' %}fa-triangle-exclamation{% else %}fa-circle-info{% endif %}"></i>
53
+ {{ message }}
54
+ <button class="alert-close" onclick="this.parentElement.remove()">Γ—</button>
55
+ </div>
56
+ {% endfor %}
57
+ {% endif %}
58
+ {% endwith %}
59
+ </div>
60
+
61
+ <!-- ── PAGE CONTENT ── -->
62
+ <main class="{% if session.user %}main-content{% endif %}">
63
+ {% block content %}{% endblock %}
64
+ </main>
65
+
66
+ <script src="{{ url_for('static', filename='js/script.js') }}"></script>
67
+ </body>
68
+ </html>
templates/dashboard.html ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'base.html' %}
2
+ {% block title %}Dashboard β€” StudentMS{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="page-header">
6
+ <div>
7
+ <h2 class="page-title">Dashboard</h2>
8
+ <p class="page-sub">Overview of all student records</p>
9
+ </div>
10
+ <a href="{{ url_for('add_student') }}" class="btn-primary">
11
+ <i class="fas fa-plus"></i> Add New Student
12
+ </a>
13
+ </div>
14
+
15
+ <!-- ── STAT CARDS ── -->
16
+ <div class="stats-grid">
17
+ <div class="stat-card stat-blue">
18
+ <div class="stat-icon"><i class="fas fa-users"></i></div>
19
+ <div class="stat-body">
20
+ <span class="stat-number">{{ stats.total }}</span>
21
+ <span class="stat-label">Total Students</span>
22
+ </div>
23
+ </div>
24
+
25
+ <div class="stat-card stat-green">
26
+ <div class="stat-icon"><i class="fas fa-mars"></i></div>
27
+ <div class="stat-body">
28
+ <span class="stat-number">{{ stats.male }}</span>
29
+ <span class="stat-label">Male Students</span>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="stat-card stat-pink">
34
+ <div class="stat-icon"><i class="fas fa-venus"></i></div>
35
+ <div class="stat-body">
36
+ <span class="stat-number">{{ stats.female }}</span>
37
+ <span class="stat-label">Female Students</span>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="stat-card stat-orange">
42
+ <div class="stat-icon"><i class="fas fa-book-open"></i></div>
43
+ <div class="stat-body">
44
+ <span class="stat-number">{{ stats.courses }}</span>
45
+ <span class="stat-label">Courses Enrolled</span>
46
+ </div>
47
+ </div>
48
+ </div>
49
+
50
+ <!-- ── RECENT STUDENTS ── -->
51
+ <div class="card mt-4">
52
+ <div class="card-header">
53
+ <h3><i class="fas fa-clock-rotate-left"></i> Recently Added Students</h3>
54
+ <a href="{{ url_for('view_students') }}" class="btn-outline">View All</a>
55
+ </div>
56
+
57
+ {% if recent %}
58
+ <div class="table-wrap">
59
+ <table class="data-table">
60
+ <thead>
61
+ <tr>
62
+ <th>Student ID</th>
63
+ <th>Full Name</th>
64
+ <th>Course</th>
65
+ <th>Branch</th>
66
+ <th>Actions</th>
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ {% for s in recent %}
71
+ <tr>
72
+ <td><span class="badge badge-id">{{ s.student_id }}</span></td>
73
+ <td>{{ s.full_name }}</td>
74
+ <td>{{ s.course }}</td>
75
+ <td>{{ s.branch }}</td>
76
+ <td class="actions">
77
+ <a href="{{ url_for('view_students') }}" class="btn-icon btn-view" title="View">
78
+ <i class="fas fa-eye"></i>
79
+ </a>
80
+ </td>
81
+ </tr>
82
+ {% endfor %}
83
+ </tbody>
84
+ </table>
85
+ </div>
86
+ {% else %}
87
+ <div class="empty-state">
88
+ <i class="fas fa-user-graduate"></i>
89
+ <p>No students yet. <a href="{{ url_for('add_student') }}">Add the first one β†’</a></p>
90
+ </div>
91
+ {% endif %}
92
+ </div>
93
+
94
+ <!-- ── QUICK LINKS ── -->
95
+ <div class="quick-links mt-4">
96
+ <a href="{{ url_for('add_student') }}" class="qlink">
97
+ <i class="fas fa-user-plus"></i>
98
+ <span>Add Student</span>
99
+ </a>
100
+ <a href="{{ url_for('view_students') }}" class="qlink">
101
+ <i class="fas fa-table-list"></i>
102
+ <span>View Records</span>
103
+ </a>
104
+ <a href="{{ url_for('view_students') }}?q=" class="qlink">
105
+ <i class="fas fa-magnifying-glass"></i>
106
+ <span>Search</span>
107
+ </a>
108
+ <a href="{{ url_for('logout') }}" class="qlink qlink-danger">
109
+ <i class="fas fa-power-off"></i>
110
+ <span>Logout</span>
111
+ </a>
112
+ </div>
113
+ {% endblock %}
templates/edit_student.html ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'base.html' %}
2
+ {% block title %}Edit Student β€” StudentMS{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="page-header">
6
+ <div>
7
+ <h2 class="page-title">Edit Student</h2>
8
+ <p class="page-sub">Update the student record below</p>
9
+ </div>
10
+ <a href="{{ url_for('view_students') }}" class="btn-outline">
11
+ <i class="fas fa-arrow-left"></i> Back
12
+ </a>
13
+ </div>
14
+
15
+ <div class="card">
16
+ <form method="POST" action="{{ url_for('edit_student', sid=sid) }}" id="studentForm" novalidate>
17
+
18
+ <!-- ── SECTION 1: Identity ── -->
19
+ <div class="form-section">
20
+ <div class="form-section-title"><i class="fas fa-id-card"></i> Student Identity</div>
21
+ <div class="form-grid">
22
+
23
+ <div class="form-group">
24
+ <label>Student ID <span class="req">*</span></label>
25
+ <input type="text" name="student_id" value="{{ student.student_id if student.student_id is defined else student['student_id'] }}" placeholder="e.g. STU2024001" />
26
+ <span class="field-error" id="err-student_id"></span>
27
+ </div>
28
+
29
+ <div class="form-group">
30
+ <label>Full Name <span class="req">*</span></label>
31
+ <input type="text" name="full_name" value="{{ student['full_name'] }}" placeholder="Full legal name" />
32
+ <span class="field-error" id="err-full_name"></span>
33
+ </div>
34
+
35
+ <div class="form-group">
36
+ <label>Father Name <span class="req">*</span></label>
37
+ <input type="text" name="father_name" value="{{ student['father_name'] }}" placeholder="Father's full name" />
38
+ <span class="field-error" id="err-father_name"></span>
39
+ </div>
40
+
41
+ <div class="form-group">
42
+ <label>Mother Name <span class="req">*</span></label>
43
+ <input type="text" name="mother_name" value="{{ student['mother_name'] }}" placeholder="Mother's full name" />
44
+ <span class="field-error" id="err-mother_name"></span>
45
+ </div>
46
+
47
+ <div class="form-group">
48
+ <label>Gender <span class="req">*</span></label>
49
+ <select name="gender">
50
+ {% for g in ['Male','Female','Other'] %}
51
+ <option value="{{ g }}" {% if student['gender']==g %}selected{% endif %}>{{ g }}</option>
52
+ {% endfor %}
53
+ </select>
54
+ <span class="field-error" id="err-gender"></span>
55
+ </div>
56
+
57
+ <div class="form-group">
58
+ <label>Date of Birth <span class="req">*</span></label>
59
+ <input type="date" name="dob" value="{{ student['dob'] }}" />
60
+ <span class="field-error" id="err-dob"></span>
61
+ </div>
62
+
63
+ </div>
64
+ </div>
65
+
66
+ <!-- ── SECTION 2: Contact ── -->
67
+ <div class="form-section">
68
+ <div class="form-section-title"><i class="fas fa-envelope"></i> Contact Details</div>
69
+ <div class="form-grid">
70
+
71
+ <div class="form-group">
72
+ <label>Email <span class="req">*</span></label>
73
+ <input type="email" name="email" value="{{ student['email'] }}" placeholder="student@example.com" />
74
+ <span class="field-error" id="err-email"></span>
75
+ </div>
76
+
77
+ <div class="form-group">
78
+ <label>Phone Number <span class="req">*</span></label>
79
+ <input type="tel" name="phone" value="{{ student['phone'] }}" placeholder="10-digit number" maxlength="10" />
80
+ <span class="field-error" id="err-phone"></span>
81
+ </div>
82
+
83
+ </div>
84
+ </div>
85
+
86
+ <!-- ── SECTION 3: Academic ── -->
87
+ <div class="form-section">
88
+ <div class="form-section-title"><i class="fas fa-book-open"></i> Academic Details</div>
89
+ <div class="form-grid">
90
+
91
+ <div class="form-group">
92
+ <label>Course <span class="req">*</span></label>
93
+ <select name="course">
94
+ {% for c in ['B.Tech','M.Tech','BCA','MCA','B.Sc','M.Sc','B.Com','M.Com','BBA','MBA','BA','MA','B.Pharm','Diploma'] %}
95
+ <option value="{{ c }}" {% if student['course']==c %}selected{% endif %}>{{ c }}</option>
96
+ {% endfor %}
97
+ </select>
98
+ <span class="field-error" id="err-course"></span>
99
+ </div>
100
+
101
+ <div class="form-group">
102
+ <label>Branch <span class="req">*</span></label>
103
+ <input type="text" name="branch" value="{{ student['branch'] }}" placeholder="e.g. Computer Science" />
104
+ <span class="field-error" id="err-branch"></span>
105
+ </div>
106
+
107
+ <div class="form-group">
108
+ <label>Year / Semester <span class="req">*</span></label>
109
+ <select name="year_sem">
110
+ {% for y in ['1st Year / 1st Sem','1st Year / 2nd Sem','2nd Year / 3rd Sem','2nd Year / 4th Sem','3rd Year / 5th Sem','3rd Year / 6th Sem','4th Year / 7th Sem','4th Year / 8th Sem'] %}
111
+ <option value="{{ y }}" {% if student['year_sem']==y %}selected{% endif %}>{{ y }}</option>
112
+ {% endfor %}
113
+ </select>
114
+ <span class="field-error" id="err-year_sem"></span>
115
+ </div>
116
+
117
+ </div>
118
+ </div>
119
+
120
+ <!-- ── SECTION 4: Address ── -->
121
+ <div class="form-section">
122
+ <div class="form-section-title"><i class="fas fa-location-dot"></i> Address</div>
123
+ <div class="form-grid">
124
+
125
+ <div class="form-group form-group-full">
126
+ <label>Address <span class="req">*</span></label>
127
+ <textarea name="address" rows="2" placeholder="Street / Locality">{{ student['address'] }}</textarea>
128
+ <span class="field-error" id="err-address"></span>
129
+ </div>
130
+
131
+ <div class="form-group">
132
+ <label>City <span class="req">*</span></label>
133
+ <input type="text" name="city" value="{{ student['city'] }}" placeholder="City name" />
134
+ <span class="field-error" id="err-city"></span>
135
+ </div>
136
+
137
+ <div class="form-group">
138
+ <label>State <span class="req">*</span></label>
139
+ <select name="state">
140
+ {% for st in ['Andhra Pradesh','Arunachal Pradesh','Assam','Bihar','Chhattisgarh','Goa','Gujarat','Haryana','Himachal Pradesh','Jharkhand','Karnataka','Kerala','Madhya Pradesh','Maharashtra','Manipur','Meghalaya','Mizoram','Nagaland','Odisha','Punjab','Rajasthan','Sikkim','Tamil Nadu','Telangana','Tripura','Uttar Pradesh','Uttarakhand','West Bengal','Delhi','Jammu & Kashmir','Ladakh','Chandigarh','Puducherry'] %}
141
+ <option value="{{ st }}" {% if student['state']==st %}selected{% endif %}>{{ st }}</option>
142
+ {% endfor %}
143
+ </select>
144
+ <span class="field-error" id="err-state"></span>
145
+ </div>
146
+
147
+ <div class="form-group">
148
+ <label>Pin Code <span class="req">*</span></label>
149
+ <input type="text" name="pin_code" value="{{ student['pin_code'] }}" placeholder="6-digit pin" maxlength="6" />
150
+ <span class="field-error" id="err-pin_code"></span>
151
+ </div>
152
+
153
+ </div>
154
+ </div>
155
+
156
+ <!-- ── BUTTONS ── -->
157
+ <div class="form-actions">
158
+ <button type="submit" class="btn-primary">
159
+ <i class="fas fa-floppy-disk"></i> Update Student
160
+ </button>
161
+ <a href="{{ url_for('view_students') }}" class="btn-outline">Cancel</a>
162
+ </div>
163
+
164
+ </form>
165
+ </div>
166
+ {% endblock %}
templates/login.html ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'base.html' %}
2
+ {% block title %}Login β€” StudentMS{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="login-wrapper">
6
+
7
+ <!-- Left panel – branding -->
8
+ <div class="login-panel-left">
9
+ <div class="login-brand">
10
+ <div class="login-icon"><i class="fas fa-graduation-cap"></i></div>
11
+ <h1>StudentMS</h1>
12
+ <p>Student Management System</p>
13
+ </div>
14
+ <ul class="login-features">
15
+ <li><i class="fas fa-check-circle"></i> Manage student records easily</li>
16
+ <li><i class="fas fa-check-circle"></i> Search, edit &amp; delete entries</li>
17
+ <li><i class="fas fa-check-circle"></i> Real-time dashboard stats</li>
18
+ <li><i class="fas fa-check-circle"></i> Secure admin access</li>
19
+ </ul>
20
+ </div>
21
+
22
+ <!-- Right panel – login form -->
23
+ <div class="login-panel-right">
24
+ <div class="login-card">
25
+ <div class="login-header">
26
+ <h2>Welcome Back</h2>
27
+ <p>Sign in to your admin account</p>
28
+ </div>
29
+
30
+ <form method="POST" action="{{ url_for('login') }}" novalidate id="loginForm">
31
+ <div class="form-group">
32
+ <label for="username"><i class="fas fa-user"></i> Username</label>
33
+ <input type="text" id="username" name="username" placeholder="Enter your username"
34
+ autocomplete="username" required />
35
+ <span class="field-error" id="err-username"></span>
36
+ </div>
37
+
38
+ <div class="form-group">
39
+ <label for="password"><i class="fas fa-lock"></i> Password</label>
40
+ <div class="input-icon-wrap">
41
+ <input type="password" id="password" name="password" placeholder="Enter your password"
42
+ autocomplete="current-password" required />
43
+ <button type="button" class="toggle-pw" onclick="togglePassword('password', this)">
44
+ <i class="fas fa-eye"></i>
45
+ </button>
46
+ </div>
47
+ <span class="field-error" id="err-password"></span>
48
+ </div>
49
+
50
+ <button type="submit" class="btn-primary btn-full">
51
+ <i class="fas fa-sign-in-alt"></i> Sign In
52
+ </button>
53
+
54
+ <p class="login-hint">Default credentials: <code>admin / admin123</code></p>
55
+ </form>
56
+ </div>
57
+ </div>
58
+
59
+ </div>
60
+ {% endblock %}
templates/view_students.html ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'base.html' %}
2
+ {% block title %}View Students β€” StudentMS{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="page-header">
6
+ <div>
7
+ <h2 class="page-title">All Students</h2>
8
+ <p class="page-sub">
9
+ {% if students %}{{ students|length }} record(s) found{% else %}No records{% endif %}
10
+ </p>
11
+ </div>
12
+ <a href="{{ url_for('add_student') }}" class="btn-primary">
13
+ <i class="fas fa-plus"></i> Add Student
14
+ </a>
15
+ </div>
16
+
17
+ <!-- ── SEARCH BAR ── -->
18
+ <div class="search-bar card">
19
+ <form method="GET" action="{{ url_for('view_students') }}" id="searchForm">
20
+ <div class="search-row">
21
+ <div class="search-input-wrap">
22
+ <i class="fas fa-magnifying-glass search-icon"></i>
23
+ <input type="text" name="q" value="{{ q }}" placeholder="Search students…" autocomplete="off" id="searchInput" />
24
+ {% if q %}<button type="button" class="clear-search" onclick="clearSearch()"><i class="fas fa-xmark"></i></button>{% endif %}
25
+ </div>
26
+
27
+ <select name="col" class="search-col">
28
+ <option value="full_name" {% if col=='full_name' %}selected{% endif %}>by Name</option>
29
+ <option value="student_id" {% if col=='student_id' %}selected{% endif %}>by ID</option>
30
+ <option value="course" {% if col=='course' %}selected{% endif %}>by Course</option>
31
+ <option value="phone" {% if col=='phone' %}selected{% endif %}>by Phone</option>
32
+ </select>
33
+
34
+ <button type="submit" class="btn-primary">
35
+ <i class="fas fa-search"></i> Search
36
+ </button>
37
+ {% if q %}
38
+ <a href="{{ url_for('view_students') }}" class="btn-outline">Clear</a>
39
+ {% endif %}
40
+ </div>
41
+ </form>
42
+ </div>
43
+
44
+ <!-- ── TABLE ── -->
45
+ <div class="card mt-3">
46
+ {% if students %}
47
+ <div class="table-wrap">
48
+ <table class="data-table" id="studentsTable">
49
+ <thead>
50
+ <tr>
51
+ <th>#</th>
52
+ <th>Student ID</th>
53
+ <th>Full Name</th>
54
+ <th>Gender</th>
55
+ <th>Course</th>
56
+ <th>Branch</th>
57
+ <th>Year/Sem</th>
58
+ <th>Phone</th>
59
+ <th>Email</th>
60
+ <th>City</th>
61
+ <th>Actions</th>
62
+ </tr>
63
+ </thead>
64
+ <tbody>
65
+ {% for s in students %}
66
+ <tr>
67
+ <td>{{ loop.index }}</td>
68
+ <td><span class="badge badge-id">{{ s.student_id }}</span></td>
69
+ <td class="fw-600">{{ s.full_name }}</td>
70
+ <td>
71
+ <span class="badge {% if s.gender=='Male' %}badge-blue{% elif s.gender=='Female' %}badge-pink{% else %}badge-gray{% endif %}">
72
+ {{ s.gender }}
73
+ </span>
74
+ </td>
75
+ <td>{{ s.course }}</td>
76
+ <td>{{ s.branch }}</td>
77
+ <td>{{ s.year_sem }}</td>
78
+ <td>{{ s.phone }}</td>
79
+ <td class="text-small">{{ s.email }}</td>
80
+ <td>{{ s.city }}</td>
81
+ <td class="actions">
82
+ <a href="{{ url_for('edit_student', sid=s.id) }}" class="btn-icon btn-edit" title="Edit">
83
+ <i class="fas fa-pen"></i>
84
+ </a>
85
+ <!-- Delete triggers a modal -->
86
+ <button type="button"
87
+ class="btn-icon btn-delete"
88
+ title="Delete"
89
+ onclick="confirmDelete({{ s.id }}, '{{ s.full_name }}')">
90
+ <i class="fas fa-trash"></i>
91
+ </button>
92
+ </td>
93
+ </tr>
94
+ {% endfor %}
95
+ </tbody>
96
+ </table>
97
+ </div>
98
+
99
+ <!-- hidden delete form -->
100
+ <form id="deleteForm" method="POST" action="" style="display:none;">
101
+ </form>
102
+
103
+ {% else %}
104
+ <div class="empty-state">
105
+ <i class="fas fa-users-slash"></i>
106
+ <p>
107
+ {% if q %}No students matching "<strong>{{ q }}</strong>".
108
+ {% else %}No students added yet. <a href="{{ url_for('add_student') }}">Add one β†’</a>{% endif %}
109
+ </p>
110
+ </div>
111
+ {% endif %}
112
+ </div>
113
+
114
+ <!-- ── DELETE CONFIRM MODAL ── -->
115
+ <div class="modal-backdrop" id="deleteModal" style="display:none;">
116
+ <div class="modal">
117
+ <div class="modal-icon danger"><i class="fas fa-triangle-exclamation"></i></div>
118
+ <h3>Confirm Delete</h3>
119
+ <p>Are you sure you want to delete<br /><strong id="deleteStudentName"></strong>?<br />This action cannot be undone.</p>
120
+ <div class="modal-actions">
121
+ <button type="button" class="btn-ghost" onclick="closeModal()">Cancel</button>
122
+ <button type="button" class="btn-danger" onclick="submitDelete()">
123
+ <i class="fas fa-trash"></i> Delete
124
+ </button>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ {% endblock %}