chualinwei3 commited on
Commit
2a44ecb
Β·
verified Β·
1 Parent(s): 3f635ef

Upload 11 files

Browse files
__pycache__/app.cpython-311.pyc ADDED
Binary file (41.4 kB). View file
 
__pycache__/attendance_code_module.cpython-311.pyc ADDED
Binary file (10.8 kB). View file
 
__pycache__/auth.cpython-311.pyc ADDED
Binary file (2.72 kB). View file
 
__pycache__/config.cpython-311.pyc ADDED
Binary file (2.04 kB). View file
 
__pycache__/face_recognition_module.cpython-311.pyc ADDED
Binary file (8.64 kB). View file
 
__pycache__/geolocation_module.cpython-311.pyc ADDED
Binary file (9.71 kB). View file
 
__pycache__/models.cpython-311.pyc ADDED
Binary file (12 kB). View file
 
__pycache__/utils.cpython-311.pyc ADDED
Binary file (3.57 kB). View file
 
static/css/style.css ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * Attendr - UTM Smart Attendance System
3
+ * Design Theme: UTM Universiti Teknologi Malaysia
4
+ * Primary Colors: Maroon (#8B1538) and Gold (#F4A900)
5
+ */
6
+
7
+ /* ==================== IMPORTS ==================== */
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap');
9
+
10
+ /* ==================== CSS VARIABLES ==================== */
11
+ :root {
12
+ /* UTM Brand Colors */
13
+ --color-utm-maroon: #8B1538;
14
+ --color-utm-maroon-dark: #6B0F2A;
15
+ --color-utm-maroon-light: #A01D45;
16
+ --color-utm-gold: #F4A900;
17
+ --color-utm-gold-dark: #D49000;
18
+ --color-utm-gold-light: #FFB820;
19
+
20
+ /* Semantic Colors */
21
+ --color-primary: var(--color-utm-maroon);
22
+ --color-primary-dark: var(--color-utm-maroon-dark);
23
+ --color-primary-light: var(--color-utm-maroon-light);
24
+ --color-accent: var(--color-utm-gold);
25
+ --color-accent-dark: var(--color-utm-gold-dark);
26
+ --color-accent-light: var(--color-utm-gold-light);
27
+
28
+ /* UI Colors */
29
+ --color-success: #10B981;
30
+ --color-error: #EF4444;
31
+ --color-warning: #F59E0B;
32
+ --color-info: #3B82F6;
33
+
34
+ /* Neutral Colors */
35
+ --color-bg: #0F0A15;
36
+ --color-bg-secondary: #1A1425;
37
+ --color-surface: rgba(139, 21, 56, 0.1);
38
+ --color-surface-hover: rgba(139, 21, 56, 0.2);
39
+ --color-text: #F9FAFB;
40
+ --color-text-muted: #D1D5DB;
41
+ --color-border: rgba(244, 169, 0, 0.2);
42
+
43
+ /* Spacing */
44
+ --spacing-xs: 0.25rem;
45
+ --spacing-sm: 0.5rem;
46
+ --spacing-md: 1rem;
47
+ --spacing-lg: 1.5rem;
48
+ --spacing-xl: 2rem;
49
+ --spacing-2xl: 3rem;
50
+
51
+ /* Border Radius */
52
+ --radius-sm: 0.375rem;
53
+ --radius-md: 0.5rem;
54
+ --radius-lg: 0.75rem;
55
+ --radius-xl: 1rem;
56
+
57
+ /* Shadows */
58
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
59
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
60
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
61
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
62
+
63
+ /* Transitions */
64
+ --transition-fast: 150ms ease-in-out;
65
+ --transition-base: 300ms ease-in-out;
66
+ --transition-slow: 500ms ease-in-out;
67
+ }
68
+
69
+ /* ==================== RESET & BASE ==================== */
70
+ * {
71
+ margin: 0;
72
+ padding: 0;
73
+ box-sizing: border-box;
74
+ }
75
+
76
+ body {
77
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
78
+ background: linear-gradient(135deg, #0F0A15 0%, #1A1425 50%, #0F0A15 100%);
79
+ background-attachment: fixed;
80
+ color: var(--color-text);
81
+ line-height: 1.6;
82
+ min-height: 100vh;
83
+ overflow-x: hidden;
84
+ }
85
+
86
+ /* UTM Pattern Background */
87
+ body::before {
88
+ content: '';
89
+ position: fixed;
90
+ top: 0;
91
+ left: 0;
92
+ width: 100%;
93
+ height: 100%;
94
+ background-image:
95
+ radial-gradient(circle at 20% 50%, rgba(139, 21, 56, 0.1) 0%, transparent 50%),
96
+ radial-gradient(circle at 80% 80%, rgba(244, 169, 0, 0.1) 0%, transparent 50%);
97
+ pointer-events: none;
98
+ z-index: 0;
99
+ }
100
+
101
+ /* ==================== TYPOGRAPHY ==================== */
102
+ h1,
103
+ h2,
104
+ h3,
105
+ h4,
106
+ h5,
107
+ h6 {
108
+ font-family: 'Outfit', 'Inter', sans-serif;
109
+ font-weight: 700;
110
+ line-height: 1.2;
111
+ color: var(--color-text);
112
+ }
113
+
114
+ h1 {
115
+ font-size: 2.5rem;
116
+ background: linear-gradient(135deg, var(--color-utm-maroon) 0%, var(--color-utm-gold) 100%);
117
+ -webkit-background-clip: text;
118
+ -webkit-text-fill-color: transparent;
119
+ background-clip: text;
120
+ }
121
+
122
+ h2 {
123
+ font-size: 2rem;
124
+ }
125
+
126
+ h3 {
127
+ font-size: 1.5rem;
128
+ }
129
+
130
+ /* ==================== LAYOUT ==================== */
131
+ .container {
132
+ max-width: 1200px;
133
+ margin: 0 auto;
134
+ padding: 0 var(--spacing-lg);
135
+ position: relative;
136
+ z-index: 1;
137
+ }
138
+
139
+ .section {
140
+ padding: var(--spacing-2xl) 0;
141
+ position: relative;
142
+ }
143
+
144
+ /* ==================== NAVIGATION ==================== */
145
+ .navbar {
146
+ background: rgba(15, 10, 21, 0.95);
147
+ backdrop-filter: blur(10px);
148
+ border-bottom: 2px solid var(--color-utm-gold);
149
+ padding: var(--spacing-md) 0;
150
+ position: sticky;
151
+ top: 0;
152
+ z-index: 1000;
153
+ box-shadow: 0 4px 20px rgba(139, 21, 56, 0.3);
154
+ }
155
+
156
+ .navbar-content {
157
+ display: flex;
158
+ justify-content: space-between;
159
+ align-items: center;
160
+ }
161
+
162
+ .navbar-brand {
163
+ font-family: 'Outfit', sans-serif;
164
+ font-size: 1.75rem;
165
+ font-weight: 800;
166
+ background: linear-gradient(135deg, var(--color-utm-maroon) 0%, var(--color-utm-gold) 100%);
167
+ -webkit-background-clip: text;
168
+ -webkit-text-fill-color: transparent;
169
+ background-clip: text;
170
+ text-decoration: none;
171
+ transition: transform var(--transition-fast);
172
+ }
173
+
174
+ .navbar-brand:hover {
175
+ transform: scale(1.05);
176
+ }
177
+
178
+ .navbar-nav {
179
+ display: flex;
180
+ list-style: none;
181
+ gap: var(--spacing-lg);
182
+ align-items: center;
183
+ }
184
+
185
+ .navbar-link {
186
+ color: var(--color-text);
187
+ text-decoration: none;
188
+ font-weight: 500;
189
+ padding: var(--spacing-sm) var(--spacing-md);
190
+ border-radius: var(--radius-md);
191
+ transition: all var(--transition-fast);
192
+ position: relative;
193
+ }
194
+
195
+ .navbar-link::after {
196
+ content: '';
197
+ position: absolute;
198
+ bottom: 0;
199
+ left: 50%;
200
+ transform: translateX(-50%);
201
+ width: 0;
202
+ height: 2px;
203
+ background: var(--color-utm-gold);
204
+ transition: width var(--transition-fast);
205
+ }
206
+
207
+ .navbar-link:hover {
208
+ color: var(--color-utm-gold);
209
+ }
210
+
211
+ .navbar-link:hover::after {
212
+ width: 80%;
213
+ }
214
+
215
+ /* ==================== GLASS CARD ==================== */
216
+ .glass-card {
217
+ background: rgba(26, 20, 37, 0.7);
218
+ backdrop-filter: blur(10px);
219
+ border: 1px solid var(--color-border);
220
+ border-radius: var(--radius-xl);
221
+ padding: var(--spacing-xl);
222
+ box-shadow:
223
+ 0 8px 32px rgba(139, 21, 56, 0.2),
224
+ inset 0 1px 0 rgba(244, 169, 0, 0.1);
225
+ transition: all var(--transition-base);
226
+ }
227
+
228
+ .glass-card:hover {
229
+ transform: translateY(-2px);
230
+ box-shadow:
231
+ 0 12px 40px rgba(139, 21, 56, 0.3),
232
+ inset 0 1px 0 rgba(244, 169, 0, 0.2);
233
+ border-color: rgba(244, 169, 0, 0.4);
234
+ }
235
+
236
+ /* ==================== BUTTONS ==================== */
237
+ .btn {
238
+ display: inline-block;
239
+ padding: 0.75rem 1.5rem;
240
+ font-size: 1rem;
241
+ font-weight: 600;
242
+ text-align: center;
243
+ text-decoration: none;
244
+ border: none;
245
+ border-radius: var(--radius-md);
246
+ cursor: pointer;
247
+ transition: all var(--transition-fast);
248
+ position: relative;
249
+ overflow: hidden;
250
+ }
251
+
252
+ .btn::before {
253
+ content: '';
254
+ position: absolute;
255
+ top: 50%;
256
+ left: 50%;
257
+ width: 0;
258
+ height: 0;
259
+ border-radius: 50%;
260
+ background: rgba(255, 255, 255, 0.1);
261
+ transform: translate(-50%, -50%);
262
+ transition: width 0.6s, height 0.6s;
263
+ }
264
+
265
+ .btn:hover::before {
266
+ width: 300px;
267
+ height: 300px;
268
+ }
269
+
270
+ .btn-primary {
271
+ background: linear-gradient(135deg, var(--color-utm-maroon) 0%, var(--color-utm-maroon-dark) 100%);
272
+ color: white;
273
+ box-shadow: 0 4px 15px rgba(139, 21, 56, 0.4);
274
+ }
275
+
276
+ .btn-primary:hover {
277
+ background: linear-gradient(135deg, var(--color-utm-maroon-light) 0%, var(--color-utm-maroon) 100%);
278
+ box-shadow: 0 6px 20px rgba(139, 21, 56, 0.6);
279
+ transform: translateY(-2px);
280
+ }
281
+
282
+ .btn-secondary {
283
+ background: linear-gradient(135deg, var(--color-utm-gold) 0%, var(--color-utm-gold-dark) 100%);
284
+ color: var(--color-utm-maroon-dark);
285
+ box-shadow: 0 4px 15px rgba(244, 169, 0, 0.4);
286
+ }
287
+
288
+ .btn-secondary:hover {
289
+ background: linear-gradient(135deg, var(--color-utm-gold-light) 0%, var(--color-utm-gold) 100%);
290
+ box-shadow: 0 6px 20px rgba(244, 169, 0, 0.6);
291
+ transform: translateY(-2px);
292
+ }
293
+
294
+ .btn-success {
295
+ background: linear-gradient(135deg, var(--color-success) 0%, #059669 100%);
296
+ color: white;
297
+ box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
298
+ }
299
+
300
+ .btn-success:hover {
301
+ background: linear-gradient(135deg, #10B981 0%, var(--color-success) 100%);
302
+ transform: translateY(-2px);
303
+ }
304
+
305
+ .btn-outline {
306
+ background: transparent;
307
+ color: var(--color-utm-gold);
308
+ border: 2px solid var(--color-utm-gold);
309
+ }
310
+
311
+ .btn-outline:hover {
312
+ background: var(--color-utm-gold);
313
+ color: var(--color-utm-maroon-dark);
314
+ }
315
+
316
+ .btn:disabled {
317
+ opacity: 0.5;
318
+ cursor: not-allowed;
319
+ }
320
+
321
+ /* ==================== FORMS ==================== */
322
+ .form-group {
323
+ margin-bottom: var(--spacing-lg);
324
+ }
325
+
326
+ .form-label {
327
+ display: block;
328
+ margin-bottom: var(--spacing-sm);
329
+ font-weight: 600;
330
+ color: var(--color-utm-gold);
331
+ }
332
+
333
+ .form-input,
334
+ .form-select {
335
+ width: 100%;
336
+ padding: 0.75rem 1rem;
337
+ background: rgba(26, 20, 37, 0.6);
338
+ border: 1px solid var(--color-border);
339
+ border-radius: var(--radius-md);
340
+ color: var(--color-text);
341
+ font-size: 1rem;
342
+ transition: all var(--transition-fast);
343
+ }
344
+
345
+ .form-input:focus,
346
+ .form-select:focus {
347
+ outline: none;
348
+ border-color: var(--color-utm-gold);
349
+ box-shadow: 0 0 0 3px rgba(244, 169, 0, 0.1);
350
+ background: rgba(26, 20, 37, 0.8);
351
+ }
352
+
353
+ .form-select option {
354
+ background: #1A1425;
355
+ color: var(--color-text);
356
+ }
357
+
358
+ /* ==================== BADGES ==================== */
359
+ .badge {
360
+ display: inline-block;
361
+ padding: 0.25rem 0.75rem;
362
+ font-size: 0.875rem;
363
+ font-weight: 600;
364
+ border-radius: var(--radius-sm);
365
+ background: rgba(139, 21, 56, 0.2);
366
+ color: var(--color-utm-gold);
367
+ border: 1px solid var(--color-utm-gold);
368
+ }
369
+
370
+ .badge-success {
371
+ background: rgba(16, 185, 129, 0.2);
372
+ color: var(--color-success);
373
+ border-color: var(--color-success);
374
+ }
375
+
376
+ .badge-error {
377
+ background: rgba(239, 68, 68, 0.2);
378
+ color: var(--color-error);
379
+ border-color: var(--color-error);
380
+ }
381
+
382
+ /* ==================== CODE DISPLAY ==================== */
383
+ .code-display {
384
+ font-family: 'Courier New', monospace;
385
+ font-size: 4rem;
386
+ font-weight: 800;
387
+ text-align: center;
388
+ padding: var(--spacing-xl);
389
+ background: linear-gradient(135deg, var(--color-utm-maroon) 0%, var(--color-utm-gold) 100%);
390
+ -webkit-background-clip: text;
391
+ -webkit-text-fill-color: transparent;
392
+ background-clip: text;
393
+ letter-spacing: 1rem;
394
+ text-shadow: 0 0 30px rgba(244, 169, 0, 0.5);
395
+ }
396
+
397
+ /* ==================== UTILITIES ==================== */
398
+ .text-center {
399
+ text-align: center;
400
+ }
401
+
402
+ .text-right {
403
+ text-align: right;
404
+ }
405
+
406
+ .text-muted {
407
+ color: var(--color-text-muted);
408
+ }
409
+
410
+ .mt-1 {
411
+ margin-top: var(--spacing-sm);
412
+ }
413
+
414
+ .mt-2 {
415
+ margin-top: var(--spacing-md);
416
+ }
417
+
418
+ .mt-3 {
419
+ margin-top: var(--spacing-lg);
420
+ }
421
+
422
+ .mb-1 {
423
+ margin-bottom: var(--spacing-sm);
424
+ }
425
+
426
+ .mb-2 {
427
+ margin-bottom: var(--spacing-md);
428
+ }
429
+
430
+ .mb-3 {
431
+ margin-bottom: var(--spacing-lg);
432
+ }
433
+
434
+ .hidden {
435
+ display: none !important;
436
+ }
437
+
438
+ .grid {
439
+ display: grid;
440
+ gap: var(--spacing-lg);
441
+ }
442
+
443
+ .grid-1 {
444
+ grid-template-columns: 1fr;
445
+ }
446
+
447
+ .grid-2 {
448
+ grid-template-columns: repeat(2, 1fr);
449
+ }
450
+
451
+ .grid-3 {
452
+ grid-template-columns: repeat(3, 1fr);
453
+ }
454
+
455
+ .gap-1 {
456
+ gap: var(--spacing-sm);
457
+ }
458
+
459
+ .gap-2 {
460
+ gap: var(--spacing-md);
461
+ }
462
+
463
+ .flex-between {
464
+ display: flex;
465
+ justify-content: space-between;
466
+ align-items: center;
467
+ }
468
+
469
+ .flex-center {
470
+ display: flex;
471
+ justify-content: center;
472
+ align-items: center;
473
+ }
474
+
475
+ /* ==================== LOADING & OVERLAYS ==================== */
476
+ .loading-overlay {
477
+ position: fixed;
478
+ top: 0;
479
+ left: 0;
480
+ width: 100%;
481
+ height: 100%;
482
+ background: rgba(15, 10, 21, 0.9);
483
+ display: flex;
484
+ justify-content: center;
485
+ align-items: center;
486
+ z-index: 9999;
487
+ }
488
+
489
+ .spinner {
490
+ width: 50px;
491
+ height: 50px;
492
+ border: 4px solid rgba(244, 169, 0, 0.2);
493
+ border-top-color: var(--color-utm-gold);
494
+ border-radius: 50%;
495
+ animation: spin 1s linear infinite;
496
+ }
497
+
498
+ @keyframes spin {
499
+ to {
500
+ transform: rotate(360deg);
501
+ }
502
+ }
503
+
504
+ /* ==================== ALERTS ==================== */
505
+ .alert {
506
+ padding: var(--spacing-md);
507
+ border-radius: var(--radius-md);
508
+ margin-bottom: var(--spacing-md);
509
+ border-left: 4px solid;
510
+ }
511
+
512
+ .alert-error {
513
+ background: rgba(239, 68, 68, 0.1);
514
+ border-color: var(--color-error);
515
+ color: #FCA5A5;
516
+ }
517
+
518
+ .alert-success {
519
+ background: rgba(16, 185, 129, 0.1);
520
+ border-color: var(--color-success);
521
+ color: #6EE7B7;
522
+ }
523
+
524
+ /* ==================== VIDEO & MEDIA ==================== */
525
+ .video-container {
526
+ position: relative;
527
+ border-radius: var(--radius-lg);
528
+ overflow: hidden;
529
+ border: 2px solid var(--color-utm-gold);
530
+ }
531
+
532
+ .video-preview {
533
+ width: 100%;
534
+ height: auto;
535
+ display: block;
536
+ }
537
+
538
+ /* ==================== STATUS BADGES ==================== */
539
+ .status-badge {
540
+ padding: var(--spacing-md);
541
+ border-radius: var(--radius-md);
542
+ border: 1px solid var(--color-border);
543
+ background: rgba(26, 20, 37, 0.6);
544
+ }
545
+
546
+ .status-info {
547
+ border-color: var(--color-utm-gold);
548
+ background: rgba(244, 169, 0, 0.1);
549
+ }
550
+
551
+ /* ==================== RESPONSIVE ==================== */
552
+ @media (max-width: 768px) {
553
+
554
+ .grid-2,
555
+ .grid-3 {
556
+ grid-template-columns: 1fr;
557
+ }
558
+
559
+ .navbar-nav {
560
+ gap: var(--spacing-sm);
561
+ font-size: 0.875rem;
562
+ }
563
+
564
+ h1 {
565
+ font-size: 2rem;
566
+ }
567
+
568
+ h2 {
569
+ font-size: 1.5rem;
570
+ }
571
+
572
+ .code-display {
573
+ font-size: 2.5rem;
574
+ letter-spacing: 0.5rem;
575
+ }
576
+ }
577
+
578
+ /* ==================== ANIMATIONS ==================== */
579
+ @keyframes fadeIn {
580
+ from {
581
+ opacity: 0;
582
+ transform: translateY(20px);
583
+ }
584
+
585
+ to {
586
+ opacity: 1;
587
+ transform: translateY(0);
588
+ }
589
+ }
590
+
591
+ .glass-card {
592
+ animation: fadeIn 0.5s ease-out;
593
+ }
static/js/lecturer.js ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Lecturer Dashboard JavaScript
2
+ // Handles session creation, code display, and real-time attendance monitoring
3
+
4
+ let currentSessionId = null;
5
+ let refreshInterval = null;
6
+ let sessionStartTime = null;
7
+ let durationInterval = null;
8
+
9
+ // ==================== INITIALIZATION ====================
10
+
11
+ document.addEventListener('DOMContentLoaded', () => {
12
+ loadClassrooms();
13
+ loadActiveSessions();
14
+ setupEventListeners();
15
+ prefillLecturerName();
16
+ });
17
+
18
+ async function prefillLecturerName() {
19
+ try {
20
+ const response = await fetch('/api/auth/current_user');
21
+ const data = await response.json();
22
+ if (data.success && data.data.role === 'lecturer') {
23
+ document.getElementById('lecturerName').value = data.data.name;
24
+ }
25
+ } catch (e) { console.error(e); }
26
+ }
27
+
28
+ function setupEventListeners() {
29
+ document.getElementById('createSessionBtn').addEventListener('click', createSession);
30
+ document.getElementById('endSessionBtn').addEventListener('click', endSession);
31
+ document.getElementById('exportBtn').addEventListener('click', exportAttendance);
32
+ }
33
+
34
+ // ==================== LOAD CLASSROOMS ====================
35
+
36
+ async function loadClassrooms() {
37
+ try {
38
+ const response = await fetch('/api/classrooms');
39
+ const data = await response.json();
40
+
41
+ const select = document.getElementById('classroomSelect');
42
+ select.innerHTML = '<option value="">Select classroom...</option>';
43
+
44
+ if (data.success && data.data.length > 0) {
45
+ data.data.forEach(classroom => {
46
+ const option = document.createElement('option');
47
+ option.value = classroom.id;
48
+ option.textContent = `${classroom.name} - ${classroom.building}`;
49
+ select.appendChild(option);
50
+ });
51
+ }
52
+ } catch (error) {
53
+ console.error('Error loading classrooms:', error);
54
+ showMessage('Failed to load classrooms', 'error');
55
+ }
56
+ }
57
+
58
+ // ==================== LOAD ACTIVE SESSIONS ====================
59
+
60
+ async function loadActiveSessions() {
61
+ try {
62
+ const response = await fetch('/api/get_active_sessions');
63
+ const data = await response.json();
64
+
65
+ const listDiv = document.getElementById('sessionsList');
66
+
67
+ if (data.success && data.data.length > 0) {
68
+ listDiv.innerHTML = data.data.map(session => `
69
+ <div class="glass-card mb-1" style="padding: 1rem; cursor: pointer;" onclick="selectSession(${session.id})">
70
+ <strong>${session.course_name}</strong><br>
71
+ <small class="text-muted">by ${session.lecturer_name}</small>
72
+ </div>
73
+ `).join('');
74
+ } else {
75
+ listDiv.innerHTML = '<p class="text-muted">No active sessions</p>';
76
+ }
77
+ } catch (error) {
78
+ console.error('Error loading sessions:', error);
79
+ }
80
+ }
81
+
82
+ // ==================== CREATE SESSION ====================
83
+
84
+ async function createSession() {
85
+ const courseName = document.getElementById('courseName').value;
86
+ const lecturerName = document.getElementById('lecturerName').value;
87
+ const classroomId = document.getElementById('classroomSelect').value;
88
+
89
+ if (!courseName || !lecturerName || !classroomId) {
90
+ showMessage('Please fill in all fields', 'error');
91
+ return;
92
+ }
93
+
94
+ showLoading('Creating session...');
95
+
96
+ try {
97
+ const response = await fetch('/api/create_session', {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: JSON.stringify({
101
+ course_name: courseName,
102
+ lecturer_name: lecturerName,
103
+ classroom_id: parseInt(classroomId)
104
+ })
105
+ });
106
+
107
+ const data = await response.json();
108
+ hideLoading();
109
+
110
+ if (data.success) {
111
+ showMessage('Session created successfully!', 'success');
112
+ currentSessionId = data.data.id;
113
+ sessionStartTime = new Date();
114
+
115
+ // Clear form
116
+ document.getElementById('courseName').value = '';
117
+ document.getElementById('lecturerName').value = '';
118
+ document.getElementById('classroomSelect').value = '';
119
+
120
+ // Show session display
121
+ showSessionDisplay();
122
+ loadActiveSessions();
123
+ startCodeRefresh();
124
+ startDurationCounter();
125
+ } else {
126
+ showMessage(data.error, 'error');
127
+ }
128
+ } catch (error) {
129
+ hideLoading();
130
+ console.error('Create session error:', error);
131
+ showMessage('Failed to create session', 'error');
132
+ }
133
+ }
134
+
135
+ // ==================== SELECT SESSION ====================
136
+
137
+ function selectSession(sessionId) {
138
+ if (currentSessionId === sessionId) return;
139
+
140
+ currentSessionId = sessionId;
141
+ // Don't set sessionStartTime to new Date() here,
142
+ // let fetchSessionData sync it from the server
143
+
144
+ showSessionDisplay();
145
+ startCodeRefresh();
146
+ startDurationCounter();
147
+ }
148
+
149
+ // ==================== SESSION DISPLAY ====================
150
+
151
+ function showSessionDisplay() {
152
+ document.getElementById('codeDisplayCard').classList.remove('hidden');
153
+ document.getElementById('statsCard').classList.remove('hidden');
154
+ document.getElementById('attendanceListCard').classList.remove('hidden');
155
+ }
156
+
157
+ function hideSessionDisplay() {
158
+ document.getElementById('codeDisplayCard').classList.add('hidden');
159
+ document.getElementById('statsCard').classList.add('hidden');
160
+ document.getElementById('attendanceListCard').classList.add('hidden');
161
+ }
162
+
163
+ // ==================== CODE REFRESH ====================
164
+
165
+ async function startCodeRefresh() {
166
+ // Initial fetch
167
+ await fetchSessionData();
168
+
169
+ // Refresh every 5 seconds
170
+ if (refreshInterval) clearInterval(refreshInterval);
171
+ refreshInterval = setInterval(fetchSessionData, 5000);
172
+ }
173
+
174
+ async function fetchSessionData() {
175
+ if (!currentSessionId) return;
176
+
177
+ try {
178
+ const response = await fetch(`/api/get_session/${currentSessionId}`);
179
+ const data = await response.json();
180
+
181
+ if (data.success) {
182
+ const session = data.data;
183
+ const codeStatus = session.code_status;
184
+
185
+ // Sync session start time from server if not already set or different
186
+ if (session.start_time) {
187
+ const serverStartTime = new Date(session.start_time);
188
+ if (!sessionStartTime || Math.abs(sessionStartTime - serverStartTime) > 5000) {
189
+ sessionStartTime = serverStartTime;
190
+ console.log('Synced session start time with server:', sessionStartTime);
191
+ }
192
+ }
193
+
194
+ // Update code display
195
+ if (codeStatus && codeStatus.code) {
196
+ document.getElementById('attendanceCodeDisplay').textContent = codeStatus.code;
197
+ document.getElementById('codeTimer').textContent =
198
+ `Refreshes in: ${codeStatus.time_remaining}s`;
199
+ }
200
+
201
+ // Fetch attendance records
202
+ await fetchAttendanceRecords();
203
+ }
204
+ } catch (error) {
205
+ console.error('Error fetching session data:', error);
206
+ }
207
+ }
208
+
209
+ // ==================== ATTENDANCE RECORDS ====================
210
+
211
+ async function fetchAttendanceRecords() {
212
+ if (!currentSessionId) return;
213
+
214
+ try {
215
+ const response = await fetch(`/api/get_attendance/${currentSessionId}`);
216
+ const data = await response.json();
217
+
218
+ if (data.success) {
219
+ const records = data.data.records;
220
+
221
+ // Update count
222
+ document.getElementById('presentCount').textContent = records.length;
223
+
224
+ // Update list
225
+ const listDiv = document.getElementById('attendanceList');
226
+
227
+ if (records.length > 0) {
228
+ listDiv.innerHTML = records.map(record => {
229
+ const time = new Date(record.marked_at).toLocaleTimeString();
230
+ return `
231
+ <div class="glass-card mb-1" style="padding: 0.75rem;">
232
+ <div style="display: flex; justify-content: space-between; align-items: center;">
233
+ <div>
234
+ <strong>${record.student.name}</strong><br>
235
+ <small class="text-muted">${record.student.student_id}</small>
236
+ </div>
237
+ <div class="text-right">
238
+ <div class="status-badge status-success" style="font-size: 0.75rem; padding: 0.25rem 0.5rem;">
239
+ βœ“ ${time}
240
+ </div>
241
+ <small class="text-muted" style="display: block; margin-top: 0.25rem;">
242
+ ${record.distance_from_classroom.toFixed(0)}m away
243
+ </small>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ `;
248
+ }).join('');
249
+ } else {
250
+ listDiv.innerHTML = '<p class="text-muted">No attendance records yet</p>';
251
+ }
252
+ }
253
+ } catch (error) {
254
+ console.error('Error fetching attendance:', error);
255
+ }
256
+ }
257
+
258
+ // ==================== DURATION COUNTER ====================
259
+
260
+ function startDurationCounter() {
261
+ if (durationInterval) clearInterval(durationInterval);
262
+
263
+ durationInterval = setInterval(() => {
264
+ if (!sessionStartTime) {
265
+ document.getElementById('sessionDuration').textContent = '00:00:00';
266
+ return;
267
+ }
268
+
269
+ const now = new Date();
270
+ const diffSeconds = Math.floor((now - sessionStartTime) / 1000);
271
+
272
+ if (diffSeconds < 0) {
273
+ document.getElementById('sessionDuration').textContent = '00:00:00';
274
+ return;
275
+ }
276
+
277
+ const hours = Math.floor(diffSeconds / 3600);
278
+ const minutes = Math.floor((diffSeconds % 3600) / 60);
279
+ const seconds = diffSeconds % 60;
280
+
281
+ const formatted = [
282
+ hours.toString().padStart(2, '0'),
283
+ minutes.toString().padStart(2, '0'),
284
+ seconds.toString().padStart(2, '0')
285
+ ].join(':');
286
+
287
+ document.getElementById('sessionDuration').textContent = formatted;
288
+ document.getElementById('sessionDuration').style.fontSize = '1.5rem'; // Make it fit better
289
+ }, 1000);
290
+ }
291
+
292
+ // ==================== END SESSION ====================
293
+
294
+ async function endSession() {
295
+ if (!currentSessionId) return;
296
+
297
+ if (!confirm('Are you sure you want to end this session?')) return;
298
+
299
+ showLoading('Ending session...');
300
+
301
+ try {
302
+ const response = await fetch(`/api/end_session/${currentSessionId}`, {
303
+ method: 'POST'
304
+ });
305
+
306
+ const data = await response.json();
307
+ hideLoading();
308
+
309
+ if (data.success) {
310
+ showMessage('Session ended successfully', 'success');
311
+
312
+ // Clear intervals
313
+ if (refreshInterval) clearInterval(refreshInterval);
314
+ if (durationInterval) clearInterval(durationInterval);
315
+
316
+ // Reset
317
+ currentSessionId = null;
318
+ sessionStartTime = null;
319
+ hideSessionDisplay();
320
+ loadActiveSessions();
321
+ } else {
322
+ showMessage(data.error, 'error');
323
+ }
324
+ } catch (error) {
325
+ hideLoading();
326
+ console.error('End session error:', error);
327
+ showMessage('Failed to end session', 'error');
328
+ }
329
+ }
330
+
331
+ // ==================== EXPORT ATTENDANCE ====================
332
+
333
+ async function exportAttendance() {
334
+ if (!currentSessionId) return;
335
+
336
+ try {
337
+ const response = await fetch(`/api/get_attendance/${currentSessionId}`);
338
+ const data = await response.json();
339
+
340
+ if (data.success) {
341
+ const session = data.data.session;
342
+ const records = data.data.records;
343
+
344
+ // Create CSV content
345
+ let csv = 'Student ID,Name,Email,Time,Distance (m),Status\n';
346
+
347
+ records.forEach(record => {
348
+ const student = record.student;
349
+ const dateObj = new Date(record.marked_at);
350
+ const time = `${dateObj.toLocaleDateString()} ${dateObj.toLocaleTimeString()}`;
351
+
352
+ // Helper to escape CSV fields
353
+ const escape = (field) => `"${String(field).replace(/"/g, '""')}"`;
354
+
355
+ csv += `${escape(student.student_id)},${escape(student.name)},${escape(student.email || 'N/A')},${escape(time)},${escape(record.distance_from_classroom.toFixed(2))},${escape(record.status)}\n`;
356
+ });
357
+
358
+ // Download CSV
359
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
360
+ const url = window.URL.createObjectURL(blob);
361
+ const a = document.createElement('a');
362
+ a.href = url;
363
+ a.download = `attendance_${session.course_name}_${new Date().toISOString().split('T')[0]}.csv`;
364
+ a.click();
365
+ window.URL.revokeObjectURL(url);
366
+
367
+ showMessage('Attendance exported successfully', 'success');
368
+ }
369
+ } catch (error) {
370
+ console.error('Export error:', error);
371
+ showMessage('Failed to export attendance', 'error');
372
+ }
373
+ }
374
+
375
+ // ==================== UTILITY FUNCTIONS ====================
376
+
377
+ function showLoading(text = 'Processing...') {
378
+ document.getElementById('loadingText').textContent = text;
379
+ document.getElementById('loadingOverlay').classList.remove('hidden');
380
+ }
381
+
382
+ function hideLoading() {
383
+ document.getElementById('loadingOverlay').classList.add('hidden');
384
+ }
385
+
386
+ function showMessage(message, type = 'info') {
387
+ const messageDiv = document.getElementById('messageDisplay');
388
+ const alertClass = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
389
+
390
+ messageDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
391
+ messageDiv.classList.remove('hidden');
392
+
393
+ setTimeout(() => {
394
+ messageDiv.classList.add('hidden');
395
+ }, 4000);
396
+ }
static/js/student.js ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Student Portal JavaScript
2
+ // Handles face verification, location validation, and attendance marking
3
+
4
+ let videoStream = null;
5
+ let currentStudentId = null;
6
+ let currentSessionId = null;
7
+ let userLocation = null;
8
+ let faceVerified = false;
9
+ let locationVerified = false;
10
+
11
+ // ==================== INITIALIZATION ====================
12
+
13
+ document.addEventListener('DOMContentLoaded', () => {
14
+ loadActiveSessions();
15
+ setupEventListeners();
16
+ });
17
+
18
+ function setupEventListeners() {
19
+ document.getElementById('startBtn').addEventListener('click', startVerification);
20
+ document.getElementById('enablePermissionsBtn').addEventListener('click', enablePermissions);
21
+ document.getElementById('verifyFaceBtn').addEventListener('click', verifyFace);
22
+ document.getElementById('verifyLocationBtn').addEventListener('click', verifyLocation);
23
+ document.getElementById('submitAttendanceBtn').addEventListener('click', submitAttendance);
24
+
25
+ // Enable start button when both fields are filled
26
+ document.getElementById('studentId').addEventListener('input', checkStartButton);
27
+ document.getElementById('sessionSelect').addEventListener('change', checkStartButton);
28
+
29
+ // Auto-uppercase attendance code
30
+ document.getElementById('attendanceCode').addEventListener('input', (e) => {
31
+ e.target.value = e.target.value.toUpperCase();
32
+ });
33
+ }
34
+
35
+ function checkStartButton() {
36
+ const studentId = document.getElementById('studentId').value.trim();
37
+ const sessionId = document.getElementById('sessionSelect').value;
38
+ document.getElementById('startBtn').disabled = !(studentId && sessionId);
39
+ }
40
+
41
+ // ==================== LOAD SESSIONS ====================
42
+
43
+ async function loadActiveSessions() {
44
+ try {
45
+ const response = await fetch('/api/get_active_sessions');
46
+ const data = await response.json();
47
+
48
+ const select = document.getElementById('sessionSelect');
49
+ select.innerHTML = '<option value="">Select a session...</option>';
50
+
51
+ if (data.success && data.data.length > 0) {
52
+ data.data.forEach(session => {
53
+ const option = document.createElement('option');
54
+ option.value = session.id;
55
+ option.textContent = `${session.course_name} - ${session.lecturer_name}`;
56
+ select.appendChild(option);
57
+ });
58
+ } else {
59
+ select.innerHTML = '<option value="">No active sessions available</option>';
60
+ }
61
+ } catch (error) {
62
+ console.error('Error loading sessions:', error);
63
+ showError('Failed to load sessions. Please refresh the page.');
64
+ }
65
+ }
66
+
67
+ // ==================== STEP 1: START VERIFICATION ====================
68
+
69
+ function startVerification() {
70
+ currentStudentId = document.getElementById('studentId').value.trim();
71
+ currentSessionId = document.getElementById('sessionSelect').value;
72
+
73
+ if (!currentStudentId || !currentSessionId) {
74
+ showError('Please enter your Student ID and select a session.');
75
+ return;
76
+ }
77
+
78
+ // Hide step 1, show step 2
79
+ document.getElementById('step1').classList.add('hidden');
80
+ document.getElementById('step2').classList.remove('hidden');
81
+ }
82
+
83
+ // ==================== STEP 2: ENABLE PERMISSIONS ====================
84
+
85
+ async function enablePermissions() {
86
+ showLoading('Requesting permissions...');
87
+
88
+ try {
89
+ // Request camera permission
90
+ const cameraStream = await navigator.mediaDevices.getUserMedia({
91
+ video: { facingMode: 'user' }
92
+ });
93
+ videoStream = cameraStream;
94
+ updateStatus('cameraStatus', 'success', 'πŸ“· Camera: Enabled');
95
+
96
+ // Request location permission
97
+ const position = await new Promise((resolve, reject) => {
98
+ navigator.geolocation.getCurrentPosition(resolve, reject, {
99
+ enableHighAccuracy: true,
100
+ timeout: 10000,
101
+ maximumAge: 0
102
+ });
103
+ });
104
+
105
+ userLocation = {
106
+ latitude: position.coords.latitude,
107
+ longitude: position.coords.longitude
108
+ };
109
+ updateStatus('gpsStatus', 'success', 'πŸ“ GPS: Enabled');
110
+
111
+ hideLoading();
112
+
113
+ // Move to step 3
114
+ setTimeout(() => {
115
+ document.getElementById('step2').classList.add('hidden');
116
+ document.getElementById('step3').classList.remove('hidden');
117
+ startCamera();
118
+ }, 500);
119
+
120
+ } catch (error) {
121
+ hideLoading();
122
+ console.error('Permission error:', error);
123
+
124
+ if (error.code === 1) {
125
+ showError('Camera or GPS permission denied. Please enable permissions in your browser settings.');
126
+ } else if (error.code === 2) {
127
+ showError('Location unavailable. Please ensure GPS is enabled on your device.');
128
+ } else {
129
+ showError('Failed to enable permissions: ' + error.message);
130
+ }
131
+ }
132
+ }
133
+
134
+ function startCamera() {
135
+ const video = document.getElementById('videoPreview');
136
+ video.srcObject = videoStream;
137
+ }
138
+
139
+ // ==================== STEP 3: FACE VERIFICATION ====================
140
+
141
+ async function verifyFace() {
142
+ showLoading('Verifying your face...');
143
+
144
+ try {
145
+ // Capture image from video
146
+ const imageData = captureImage();
147
+
148
+ // Send to API
149
+ const response = await fetch('/api/verify_face', {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ body: JSON.stringify({
153
+ student_id: currentStudentId,
154
+ image: imageData
155
+ })
156
+ });
157
+
158
+ const data = await response.json();
159
+ hideLoading();
160
+
161
+ if (data.success) {
162
+ faceVerified = true;
163
+ showStatus('faceStatus', 'success', `βœ… ${data.message}`);
164
+
165
+ // Move to step 4
166
+ setTimeout(() => {
167
+ stopCamera();
168
+ document.getElementById('step3').classList.add('hidden');
169
+ document.getElementById('step4').classList.remove('hidden');
170
+ }, 1000);
171
+ } else {
172
+ showStatus('faceStatus', 'error', `❌ ${data.error}`);
173
+ }
174
+
175
+ } catch (error) {
176
+ hideLoading();
177
+ console.error('Face verification error:', error);
178
+ showStatus('faceStatus', 'error', '❌ Face verification failed. Please try again.');
179
+ }
180
+ }
181
+
182
+ function captureImage() {
183
+ const video = document.getElementById('videoPreview');
184
+ const canvas = document.getElementById('canvas');
185
+
186
+ canvas.width = video.videoWidth;
187
+ canvas.height = video.videoHeight;
188
+
189
+ const ctx = canvas.getContext('2d');
190
+ ctx.drawImage(video, 0, 0);
191
+
192
+ return canvas.toDataURL('image/jpeg', 0.8);
193
+ }
194
+
195
+ function stopCamera() {
196
+ if (videoStream) {
197
+ videoStream.getTracks().forEach(track => track.stop());
198
+ }
199
+ }
200
+
201
+ // ==================== STEP 4: LOCATION VERIFICATION ====================
202
+
203
+ async function verifyLocation() {
204
+ if (!userLocation) {
205
+ showError('Location not available. Please enable GPS.');
206
+ return;
207
+ }
208
+
209
+ showLoading('Verifying your location...');
210
+
211
+ try {
212
+ const response = await fetch('/api/verify_location', {
213
+ method: 'POST',
214
+ headers: { 'Content-Type': 'application/json' },
215
+ body: JSON.stringify({
216
+ latitude: userLocation.latitude,
217
+ longitude: userLocation.longitude,
218
+ session_id: currentSessionId
219
+ })
220
+ });
221
+
222
+ const data = await response.json();
223
+ hideLoading();
224
+
225
+ if (data.success) {
226
+ locationVerified = true;
227
+ showStatus('locationStatus', 'success', `βœ… ${data.message}`);
228
+
229
+ // Move to step 5
230
+ setTimeout(() => {
231
+ document.getElementById('step4').classList.add('hidden');
232
+ document.getElementById('step5').classList.remove('hidden');
233
+ }, 1000);
234
+ } else {
235
+ showStatus('locationStatus', 'error', `❌ ${data.error}`);
236
+ }
237
+
238
+ } catch (error) {
239
+ hideLoading();
240
+ console.error('Location verification error:', error);
241
+ showStatus('locationStatus', 'error', '❌ Location verification failed.');
242
+ }
243
+ }
244
+
245
+ // ==================== STEP 5: SUBMIT ATTENDANCE ====================
246
+
247
+ async function submitAttendance() {
248
+ const code = document.getElementById('attendanceCode').value.trim();
249
+
250
+ if (!code || code.length !== 6) {
251
+ showError('Please enter a valid 6-digit code.');
252
+ return;
253
+ }
254
+
255
+ if (!faceVerified || !locationVerified) {
256
+ showError('Please complete face and location verification first.');
257
+ return;
258
+ }
259
+
260
+ showLoading('Marking attendance...');
261
+
262
+ try {
263
+ const response = await fetch('/api/mark_attendance', {
264
+ method: 'POST',
265
+ headers: { 'Content-Type': 'application/json' },
266
+ body: JSON.stringify({
267
+ student_id: currentStudentId,
268
+ session_id: currentSessionId,
269
+ code: code,
270
+ latitude: userLocation.latitude,
271
+ longitude: userLocation.longitude
272
+ })
273
+ });
274
+
275
+ const data = await response.json();
276
+ hideLoading();
277
+
278
+ if (data.success) {
279
+ // Show success
280
+ document.getElementById('step5').classList.add('hidden');
281
+ document.getElementById('successMessage').classList.remove('hidden');
282
+ document.getElementById('successDetails').textContent =
283
+ `Your attendance has been recorded at ${new Date().toLocaleTimeString()}.`;
284
+ } else {
285
+ showError(data.error);
286
+ }
287
+
288
+ } catch (error) {
289
+ hideLoading();
290
+ console.error('Submit attendance error:', error);
291
+ showError('Failed to mark attendance. Please try again.');
292
+ }
293
+ }
294
+
295
+ // ==================== UTILITY FUNCTIONS ====================
296
+
297
+ function showLoading(text = 'Processing...') {
298
+ document.getElementById('loadingText').textContent = text;
299
+ document.getElementById('loadingOverlay').classList.remove('hidden');
300
+ }
301
+
302
+ function hideLoading() {
303
+ document.getElementById('loadingOverlay').classList.add('hidden');
304
+ }
305
+
306
+ function showError(message) {
307
+ const errorDiv = document.getElementById('errorDisplay');
308
+ errorDiv.innerHTML = `<div class="alert alert-error">${message}</div>`;
309
+ errorDiv.classList.remove('hidden');
310
+
311
+ setTimeout(() => {
312
+ errorDiv.classList.add('hidden');
313
+ }, 5000);
314
+ }
315
+
316
+ function showStatus(elementId, type, message) {
317
+ const element = document.getElementById(elementId);
318
+ const alertClass = type === 'success' ? 'alert-success' : 'alert-error';
319
+ element.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
320
+ }
321
+
322
+ function updateStatus(elementId, type, text) {
323
+ const element = document.getElementById(elementId);
324
+ element.className = `status-badge status-${type}`;
325
+ element.innerHTML = `<span>${text.split(':')[0]}</span><span>${text.split(':')[1]}</span>`;
326
+ }