Pavanupadhyay27 commited on
Commit
411cf16
·
1 Parent(s): 15adfd0

Deploy NetraID backend

Browse files
backend/app/api/v1/employees.py CHANGED
@@ -68,6 +68,17 @@ def create_employee(
68
  existing_email = crud.get_employee_by_email(db, email=emp.email)
69
  if existing_email:
70
  raise HTTPException(status_code=400, detail="Employee email already exists")
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  user_id = None
73
  if emp.create_user_login:
 
68
  existing_email = crud.get_employee_by_email(db, email=emp.email)
69
  if existing_email:
70
  raise HTTPException(status_code=400, detail="Employee email already exists")
71
+
72
+ # Check duplicate name (case-insensitive)
73
+ existing_name = crud.get_employee_by_name(db, name=emp.name)
74
+ if existing_name:
75
+ raise HTTPException(status_code=400, detail="An employee with this name is already registered")
76
+
77
+ # Check duplicate phone
78
+ if emp.phone:
79
+ existing_phone = crud.get_employee_by_phone(db, phone=emp.phone)
80
+ if existing_phone:
81
+ raise HTTPException(status_code=400, detail="This phone number is already registered to another employee")
82
 
83
  user_id = None
84
  if emp.create_user_login:
backend/app/api/v1/enrollment.py CHANGED
@@ -58,11 +58,26 @@ async def upload_face_image(
58
  raise HTTPException(status_code=400, detail=f"Face detection confidence too low ({confidence:.2f}). Please upload a clearer image.")
59
 
60
  # Optional liveness check on enrollment (preventing enroll spoofing)
61
- liveness_score, is_live = face_engine.check_liveness(img, face["bbox"])
62
- # In enrollment we want to warn or prevent spoofing. Let's allow it but log a warning if is_live is False,
63
- # or block. For highest security, we enforce liveness on enrollment!
64
- if not is_live and not face_engine.mock_mode:
65
- raise HTTPException(status_code=400, detail=f"Liveness detection failed ({liveness_score:.2f}). Please upload a real, non-spoofed image.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  # Align face (112x112)
68
  aligned_face = face_engine.align_face(img, face["landmarks"])
 
58
  raise HTTPException(status_code=400, detail=f"Face detection confidence too low ({confidence:.2f}). Please upload a clearer image.")
59
 
60
  # Optional liveness check on enrollment (preventing enroll spoofing)
61
+ liveness_enabled_setting = crud.get_setting_by_key(db, "ENROLLMENT_LIVENESS_CHECK")
62
+ liveness_enabled = liveness_enabled_setting.value.lower() == "true" if liveness_enabled_setting else True
63
+
64
+ liveness_threshold_setting = crud.get_setting_by_key(db, "ENROLLMENT_LIVENESS_THRESHOLD")
65
+ liveness_threshold = float(liveness_threshold_setting.value) if liveness_threshold_setting else 0.70
66
+
67
+ liveness_score, is_live = face_engine.check_liveness(img, face["bbox"], threshold=liveness_threshold)
68
+
69
+ # In enrollment we want to prevent spoofing. However, liveness models are calibrated for direct frontal views.
70
+ # Profile/tilted views (left, right, up, down) often yield lower liveness scores and cause false rejections.
71
+ # Therefore, we strictly enforce liveness on the "front" pose only, and bypass it for other poses.
72
+ if liveness_enabled and not is_live and not face_engine.mock_mode:
73
+ if pose_type.strip().lower() == "front":
74
+ raise HTTPException(status_code=400, detail=f"Liveness check failed ({liveness_score:.2f}). Please upload a real photo.")
75
+ else:
76
+ logger.warning(
77
+ f"Liveness check failed during enrollment for non-frontal pose '{pose_type}' "
78
+ f"(score: {liveness_score:.2f}, threshold: {liveness_threshold:.2f}). "
79
+ f"Bypassing check to prevent false rejection."
80
+ )
81
 
82
  # Align face (112x112)
83
  aligned_face = face_engine.align_face(img, face["landmarks"])
backend/app/api/v1/kiosk.py CHANGED
@@ -35,6 +35,14 @@ def _publish_log(log_obj, employee=None):
35
  "employee_id": employee.employee_id,
36
  "designation": employee.designation,
37
  "department": employee.department.name if employee.department else "General",
 
 
 
 
 
 
 
 
38
  }
39
  payload = {
40
  "id": log_obj.id,
@@ -44,6 +52,7 @@ def _publish_log(log_obj, employee=None):
44
  "liveness_score": log_obj.liveness_score,
45
  "is_spoof": log_obj.is_spoof,
46
  "status": log_obj.status,
 
47
  "employee": emp_data,
48
  }
49
  event_bus.publish_scan_event(payload)
@@ -53,6 +62,7 @@ def _publish_log(log_obj, employee=None):
53
  class KioskScanRequest(BaseModel):
54
  image: str = Field(..., description="Base64 encoded image frame (JPEG/PNG data URL)")
55
  camera: str = Field("Main Kiosk", description="Identifier of the kiosk scanner device")
 
56
 
57
  @router.post("/scan")
58
  def scan_face(
@@ -118,7 +128,7 @@ def scan_face(
118
  landmarks = face["landmarks"]
119
 
120
  # 3. Liveness Check
121
- liveness_score, is_live = face_engine.check_liveness(img, bbox)
122
  if not is_live and not face_engine.mock_mode:
123
  # Save spoof log
124
  log_entry = crud.create_attendance_log(
@@ -134,7 +144,7 @@ def scan_face(
134
  _publish_log(log_entry)
135
  return {
136
  "status": "spoof_detected",
137
- "message": "Spoofing attack detected! Verification denied.",
138
  "confidence": float(confidence),
139
  "liveness_score": float(liveness_score),
140
  "should_retry": False
@@ -152,28 +162,24 @@ def scan_face(
152
  if not all_embeddings:
153
  match_result = None
154
  else:
155
- best_emb = None
156
- best_dist = 10.0
157
-
158
- for emb_record in all_embeddings:
159
- try:
160
- # cached arrays are already numpy arrays
161
- db_vec = emb_record["embedding"]
162
- sim = face_engine.cosine_similarity(embedding, db_vec)
163
- dist = 1.0 - sim
164
- if dist < best_dist:
165
- best_dist = dist
166
- class MockEmb:
167
- id = emb_record["id"]
168
- employee_id = emb_record["employee_id"]
169
- best_emb = MockEmb()
170
- except Exception as e:
171
- logger.error(f"Error comparing local vector: {e}")
172
- continue
173
-
174
- if best_emb is not None:
175
- match_result = (best_emb, best_dist)
176
- else:
177
  match_result = None
178
 
179
  if not match_result:
@@ -248,74 +254,300 @@ def scan_face(
248
  else:
249
  _last_greeted_employee_id = employee.id
250
 
251
- # 6. Mark Attendance
252
- attendance_record = crud.mark_kiosk_attendance(
253
- db=db,
254
- employee_id=employee.id,
255
- timestamp=now,
256
- camera=payload.camera,
257
- confidence=similarity
258
- )
259
-
260
- # Save log
261
- log_entry = crud.create_attendance_log(
262
- db=db,
263
- employee_id=employee.id,
264
- camera=payload.camera,
265
- confidence=similarity,
266
- liveness_score=liveness_score,
267
- is_spoof=False,
268
- status="Match Success",
269
- timestamp=now
270
  )
271
- _publish_log(log_entry, employee)
272
-
273
- # 7. Generate Greeting Text
274
- current_hour = now.hour
275
- if 5 <= current_hour < 12:
276
- salutation = "Good Morning"
277
- icon = "☀️"
278
- elif 12 <= current_hour < 17:
279
- salutation = "Good Afternoon"
280
- icon = "🌤️"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  else:
282
- salutation = "Good Evening"
283
- icon = "🌙"
284
-
285
- is_checkout = attendance_record.check_out is not None
286
- action_text = "Checkout Recorded Successfully" if is_checkout else "Attendance Recorded Successfully"
287
- closing_text = "Have a Great Day" if not is_checkout else "Have a Relaxing Evening"
288
-
289
- greeting_text = f"Welcome {employee.name}. {salutation}. {action_text}. {closing_text}."
290
-
291
- # URL for speech audio download
292
- tts_url = f"{settings.API_V1_STR}/kiosk/tts?text={urllib.parse.quote(greeting_text)}" if (voice_enabled and should_greet) else None
293
-
294
- return {
295
- "status": "success",
296
- "employee": {
297
- "id": employee.id,
298
- "employee_id": employee.employee_id,
299
- "name": employee.name,
300
- "designation": employee.designation,
301
- "department": employee.department.name if employee.department else "General"
302
- },
303
- "attendance": {
304
- "date": str(attendance_record.date),
305
- "check_in": str(attendance_record.check_in.time().strftime("%H:%M:%S")) if attendance_record.check_in else None,
306
- "check_out": str(attendance_record.check_out.time().strftime("%H:%M:%S")) if attendance_record.check_out else None,
307
- "status": attendance_record.status
308
- },
309
- "confidence": similarity,
310
- "liveness_score": liveness_score,
311
- "greeting": {
312
- "title": f"Welcome, {employee.name}",
313
- "subtitle": f"{salutation} {icon}",
314
- "detail": action_text,
315
- "closing": closing_text
316
- },
317
- "tts_url": tts_url
318
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
  @router.get("/tts")
321
  def play_tts(text: str):
 
35
  "employee_id": employee.employee_id,
36
  "designation": employee.designation,
37
  "department": employee.department.name if employee.department else "General",
38
+ "images": [
39
+ {
40
+ "id": img.id,
41
+ "file_path": img.file_path,
42
+ "pose_type": img.pose_type,
43
+ "created_at": img.created_at.isoformat() if img.created_at else None
44
+ } for img in employee.images
45
+ ]
46
  }
47
  payload = {
48
  "id": log_obj.id,
 
52
  "liveness_score": log_obj.liveness_score,
53
  "is_spoof": log_obj.is_spoof,
54
  "status": log_obj.status,
55
+ "image_path": log_obj.image_path,
56
  "employee": emp_data,
57
  }
58
  event_bus.publish_scan_event(payload)
 
62
  class KioskScanRequest(BaseModel):
63
  image: str = Field(..., description="Base64 encoded image frame (JPEG/PNG data URL)")
64
  camera: str = Field("Main Kiosk", description="Identifier of the kiosk scanner device")
65
+ confirm_checkout: bool = Field(False, description="Whether the check-out is confirmed by the employee")
66
 
67
  @router.post("/scan")
68
  def scan_face(
 
128
  landmarks = face["landmarks"]
129
 
130
  # 3. Liveness Check
131
+ liveness_score, is_live = face_engine.check_liveness(img, bbox, threshold=liveness_threshold)
132
  if not is_live and not face_engine.mock_mode:
133
  # Save spoof log
134
  log_entry = crud.create_attendance_log(
 
144
  _publish_log(log_entry)
145
  return {
146
  "status": "spoof_detected",
147
+ "message": "Liveness check failed! Verification denied.",
148
  "confidence": float(confidence),
149
  "liveness_score": float(liveness_score),
150
  "should_retry": False
 
162
  if not all_embeddings:
163
  match_result = None
164
  else:
165
+ try:
166
+ # High-performance vectorized search using NumPy matrix multiplication.
167
+ # ArcFace embeddings are L2-normalized, so cosine similarity is just the dot product.
168
+ embeddings_matrix = np.stack([emb["embedding"] for emb in all_embeddings]) # shape (N, 512)
169
+ similarities = np.dot(embeddings_matrix, embedding) # shape (N,)
170
+ best_idx = int(np.argmax(similarities))
171
+ best_similarity = float(similarities[best_idx])
172
+
173
+ best_emb_record = all_embeddings[best_idx]
174
+ class MockEmb:
175
+ id = best_emb_record["id"]
176
+ employee_id = best_emb_record["employee_id"]
177
+
178
+ # distance = 1 - similarity
179
+ best_dist = 1.0 - best_similarity
180
+ match_result = (MockEmb(), best_dist)
181
+ except Exception as e:
182
+ logger.error(f"Error in vectorized face matching: {e}")
 
 
 
 
183
  match_result = None
184
 
185
  if not match_result:
 
254
  else:
255
  _last_greeted_employee_id = employee.id
256
 
257
+ from sqlalchemy import select, and_
258
+ from datetime import time, timedelta
259
+
260
+ # 6. Check state of attendance
261
+ stmt = select(models.Attendance).where(
262
+ and_(
263
+ models.Attendance.employee_id == employee.id,
264
+ models.Attendance.date == now.date()
265
+ )
 
 
 
 
 
 
 
 
 
 
266
  )
267
+ attendance_record = db.execute(stmt).scalar_one_or_none()
268
+
269
+ if not attendance_record:
270
+ # --- First scan of the day: Check-In ---
271
+ start_time_setting = crud.get_setting_by_key(db, "CHECK_IN_START")
272
+ grace_period_setting = crud.get_setting_by_key(db, "GRACE_PERIOD_MINUTES")
273
+ start_str = start_time_setting.value if start_time_setting else "09:00"
274
+ grace_mins = int(grace_period_setting.value) if grace_period_setting else 15
275
+ try:
276
+ hr, mn = map(int, start_str.split(":"))
277
+ check_in_deadline = datetime.combine(now.date(), time(hr, mn)) + timedelta(minutes=grace_mins)
278
+ except Exception:
279
+ check_in_deadline = datetime.combine(now.date(), time(9, 15))
280
+
281
+ is_late = now > check_in_deadline
282
+ status = "Late" if is_late else "Present"
283
+
284
+ attendance_record = models.Attendance(
285
+ employee_id=employee.id,
286
+ date=now.date(),
287
+ check_in=now,
288
+ late_arrival=is_late,
289
+ status=status
290
+ )
291
+ db.add(attendance_record)
292
+ db.commit()
293
+ db.refresh(attendance_record)
294
+
295
+ # Save log
296
+ log_entry = crud.create_attendance_log(
297
+ db=db,
298
+ employee_id=employee.id,
299
+ camera=payload.camera,
300
+ confidence=similarity,
301
+ liveness_score=liveness_score,
302
+ is_spoof=False,
303
+ status="Match Success",
304
+ timestamp=now
305
+ )
306
+ _publish_log(log_entry, employee)
307
+
308
+ # Generate check-in greeting based on timing
309
+ current_hour = now.hour
310
+ if 5 <= current_hour < 12:
311
+ salutation = "Good Morning"
312
+ icon = "☀️"
313
+ elif 12 <= current_hour < 17:
314
+ salutation = "Good Afternoon"
315
+ icon = "🌤️"
316
+ else:
317
+ salutation = "Good Evening"
318
+ icon = "🌙"
319
+
320
+ greeting_text = f"Welcome {employee.name}. {salutation}. Attendance Recorded Successfully. Have a Great Day."
321
+ tts_url = f"{settings.API_V1_STR}/kiosk/tts?text={urllib.parse.quote(greeting_text)}" if (voice_enabled and should_greet) else None
322
+
323
+ return {
324
+ "status": "success",
325
+ "employee": {
326
+ "id": employee.id,
327
+ "employee_id": employee.employee_id,
328
+ "name": employee.name,
329
+ "designation": employee.designation,
330
+ "department": employee.department.name if employee.department else "General"
331
+ },
332
+ "attendance": {
333
+ "date": str(attendance_record.date),
334
+ "check_in": str(attendance_record.check_in.time().strftime("%H:%M:%S")) if attendance_record.check_in else None,
335
+ "check_out": None,
336
+ "status": attendance_record.status,
337
+ "working_hours": 0.0
338
+ },
339
+ "confidence": similarity,
340
+ "liveness_score": liveness_score,
341
+ "greeting": {
342
+ "title": f"Welcome, {employee.name}",
343
+ "subtitle": f"{salutation} {icon}",
344
+ "detail": "Attendance Recorded Successfully",
345
+ "closing": "Have a Great Day"
346
+ },
347
+ "tts_url": tts_url
348
+ }
349
+
350
  else:
351
+ # --- Attendance record exists for today ---
352
+ if attendance_record.check_out is not None:
353
+ # Already checked out -> Locked!
354
+ # Check if emergency bypass is enabled
355
+ if getattr(attendance_record, "emergency_allowed", False):
356
+ # Emergency check-in!
357
+ attendance_record.check_out = None
358
+ attendance_record.emergency_allowed = False
359
+ db.commit()
360
+
361
+ log_entry = crud.create_attendance_log(
362
+ db=db,
363
+ employee_id=employee.id,
364
+ camera=payload.camera,
365
+ confidence=similarity,
366
+ liveness_score=liveness_score,
367
+ is_spoof=False,
368
+ status="Match Success",
369
+ timestamp=now
370
+ )
371
+ _publish_log(log_entry, employee)
372
+
373
+ current_hour = now.hour
374
+ if 5 <= current_hour < 12:
375
+ salutation = "Good Morning"
376
+ icon = "☀️"
377
+ elif 12 <= current_hour < 17:
378
+ salutation = "Good Afternoon"
379
+ icon = "🌤️"
380
+ else:
381
+ salutation = "Good Evening"
382
+ icon = "🌙"
383
+
384
+ greeting_text = f"Welcome {employee.name}. {salutation}. Emergency Attendance Recorded. Have a Great Day."
385
+ tts_url = f"{settings.API_V1_STR}/kiosk/tts?text={urllib.parse.quote(greeting_text)}" if (voice_enabled and should_greet) else None
386
+
387
+ return {
388
+ "status": "success",
389
+ "employee": {
390
+ "id": employee.id,
391
+ "employee_id": employee.employee_id,
392
+ "name": employee.name,
393
+ "designation": employee.designation,
394
+ "department": employee.department.name if employee.department else "General"
395
+ },
396
+ "attendance": {
397
+ "date": str(attendance_record.date),
398
+ "check_in": str(attendance_record.check_in.time().strftime("%H:%M:%S")) if attendance_record.check_in else None,
399
+ "check_out": None,
400
+ "status": attendance_record.status,
401
+ "working_hours": 0.0
402
+ },
403
+ "confidence": similarity,
404
+ "liveness_score": liveness_score,
405
+ "greeting": {
406
+ "title": f"Welcome back, {employee.name}",
407
+ "subtitle": f"{salutation} {icon}",
408
+ "detail": "Emergency Check-In Recorded",
409
+ "closing": "Have a Great Day"
410
+ },
411
+ "tts_url": tts_url
412
+ }
413
+ else:
414
+ # Locked for the day!
415
+ log_entry = crud.create_attendance_log(
416
+ db=db,
417
+ employee_id=employee.id,
418
+ camera=payload.camera,
419
+ confidence=similarity,
420
+ liveness_score=liveness_score,
421
+ is_spoof=False,
422
+ status="Attendance Locked",
423
+ timestamp=now
424
+ )
425
+ _publish_log(log_entry, employee)
426
+
427
+ return {
428
+ "status": "locked",
429
+ "message": "Attendance locked until tomorrow. Emergency entry must be approved by Admin.",
430
+ "should_retry": False
431
+ }
432
+
433
+ else:
434
+ # Checked in, but not checked out yet -> Prompt for check-out
435
+ if not payload.confirm_checkout:
436
+ # Ask user if they want to check out
437
+ # Calculate working hours so far
438
+ diff = now - attendance_record.check_in
439
+ hours = round(diff.total_seconds() / 3600.0, 2)
440
+
441
+ # Format: only say "Thank you" before checkout confirmation
442
+ greeting_text = "Thank you"
443
+ tts_url = f"{settings.API_V1_STR}/kiosk/tts?text={urllib.parse.quote(greeting_text)}" if (voice_enabled and should_greet) else None
444
+
445
+ return {
446
+ "status": "ask_checkout",
447
+ "employee": {
448
+ "id": employee.id,
449
+ "employee_id": employee.employee_id,
450
+ "name": employee.name,
451
+ "designation": employee.designation,
452
+ "department": employee.department.name if employee.department else "General"
453
+ },
454
+ "attendance": {
455
+ "date": str(attendance_record.date),
456
+ "check_in": str(attendance_record.check_in.time().strftime("%H:%M:%S")),
457
+ "status": attendance_record.status
458
+ },
459
+ "working_hours_so_far": hours,
460
+ "confidence": similarity,
461
+ "liveness_score": liveness_score,
462
+ "greeting": {
463
+ "title": "Identity Verified",
464
+ "subtitle": "Thank You",
465
+ "detail": "Tap Yes to confirm Check Out",
466
+ "closing": f"Duration: {hours} hours"
467
+ },
468
+ "tts_url": tts_url
469
+ }
470
+ else:
471
+ # User confirmed checkout!
472
+ attendance_record.check_out = now
473
+ diff = now - attendance_record.check_in
474
+ hours = round(diff.total_seconds() / 3600.0, 2)
475
+ attendance_record.working_hours = hours
476
+
477
+ # Early departure and overtime
478
+ end_time_setting = crud.get_setting_by_key(db, "CHECK_OUT_END")
479
+ end_str = end_time_setting.value if end_time_setting else "17:00"
480
+ try:
481
+ ehr, emn = map(int, end_str.split(":"))
482
+ departure_deadline = datetime.combine(now.date(), time(ehr, emn))
483
+ except Exception:
484
+ departure_deadline = datetime.combine(now.date(), time(17, 0))
485
+ attendance_record.early_departure = now < departure_deadline
486
+ attendance_record.overtime = max(0.0, round(hours - 8.0, 2))
487
+
488
+ # Update status based on hours
489
+ if hours < 8.0:
490
+ attendance_record.status = "Half Day"
491
+ else:
492
+ if attendance_record.status in ["Half Day", "Absent"]:
493
+ attendance_record.status = "Present"
494
+
495
+ db.commit()
496
+ db.refresh(attendance_record)
497
+
498
+ log_entry = crud.create_attendance_log(
499
+ db=db,
500
+ employee_id=employee.id,
501
+ camera=payload.camera,
502
+ confidence=similarity,
503
+ liveness_score=liveness_score,
504
+ is_spoof=False,
505
+ status="Match Success",
506
+ timestamp=now
507
+ )
508
+ _publish_log(log_entry, employee)
509
+
510
+ # Play timing greeting after confirmation
511
+ current_hour = now.hour
512
+ if 5 <= current_hour < 12:
513
+ salutation = "Good Morning"
514
+ icon = "☀️"
515
+ elif 12 <= current_hour < 17:
516
+ salutation = "Good Afternoon"
517
+ icon = "🌤️"
518
+ else:
519
+ salutation = "Good Evening"
520
+ icon = "🌙"
521
+
522
+ greeting_text = f"Welcome {employee.name}. {salutation}. Checkout Recorded Successfully. Have a Relaxing Evening."
523
+ tts_url = f"{settings.API_V1_STR}/kiosk/tts?text={urllib.parse.quote(greeting_text)}" if (voice_enabled and should_greet) else None
524
+
525
+ return {
526
+ "status": "success",
527
+ "employee": {
528
+ "id": employee.id,
529
+ "employee_id": employee.employee_id,
530
+ "name": employee.name,
531
+ "designation": employee.designation,
532
+ "department": employee.department.name if employee.department else "General"
533
+ },
534
+ "attendance": {
535
+ "date": str(attendance_record.date),
536
+ "check_in": str(attendance_record.check_in.time().strftime("%H:%M:%S")),
537
+ "check_out": str(attendance_record.check_out.time().strftime("%H:%M:%S")),
538
+ "status": attendance_record.status,
539
+ "working_hours": hours
540
+ },
541
+ "confidence": similarity,
542
+ "liveness_score": liveness_score,
543
+ "greeting": {
544
+ "title": f"Goodbye, {employee.name}",
545
+ "subtitle": f"{salutation} {icon}",
546
+ "detail": "Checkout Recorded Successfully",
547
+ "closing": f"Worked: {hours} hours"
548
+ },
549
+ "tts_url": tts_url
550
+ }
551
 
552
  @router.get("/tts")
553
  def play_tts(text: str):
backend/app/core/config.py CHANGED
@@ -54,6 +54,10 @@ class Settings(BaseSettings):
54
  KIOSK_LIVENESS_THRESHOLD: float = 0.75
55
  FORCE_MOCK_MODE: bool = False
56
 
 
 
 
 
57
  # Paths
58
  UPLOAD_DIR: str = "./uploads"
59
  MODELS_DIR: str = "./models"
 
54
  KIOSK_LIVENESS_THRESHOLD: float = 0.75
55
  FORCE_MOCK_MODE: bool = False
56
 
57
+ # Performance Optimizations
58
+ LOW_MEMORY_MODE: bool = True # Set to False on VPS/Dedicated servers to run faster
59
+ ORT_INTRA_OP_NUM_THREADS: int = 0 # 0 = automatic core detection (faster)
60
+
61
  # Paths
62
  UPLOAD_DIR: str = "./uploads"
63
  MODELS_DIR: str = "./models"
backend/app/core/init_db.py CHANGED
@@ -19,6 +19,24 @@ def init_db(db: Session):
19
  # Create all tables if they don't exist
20
  logger.info("Creating all database tables if they do not exist...")
21
  Base.metadata.create_all(bind=engine)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  # 1. Seed Roles
24
  roles = [
@@ -57,6 +75,8 @@ def init_db(db: Session):
57
  {"key": "GRACE_PERIOD_MINUTES", "value": "0", "description": "Grace period for check-ins in minutes"},
58
  {"key": "KIOSK_FACE_THRESHOLD", "value": str(settings.KIOSK_FACE_THRESHOLD), "description": "Cosine similarity threshold for face match"},
59
  {"key": "KIOSK_LIVENESS_THRESHOLD", "value": str(settings.KIOSK_LIVENESS_THRESHOLD), "description": "Softmax probability threshold for face liveness"},
 
 
60
  {"key": "VOICE_GREETING_ENABLED", "value": "true", "description": "Enable voice greeting on successful attendance"},
61
  {"key": "SYSTEM_MAINTENANCE_MODE", "value": "false", "description": "Toggle maintenance mode to suspend active check-ins"},
62
  {"key": "MAX_ENROLLMENT_IMAGES", "value": "5", "description": "Maximum face images captured during registration"},
@@ -81,6 +101,8 @@ def init_db(db: Session):
81
  )
82
  crud.create_user(db, admin_create)
83
  else:
84
- logger.info(f"Super Admin user {admin_email} already exists.")
 
 
85
 
86
  logger.info("Database initialization and seeding completed successfully.")
 
19
  # Create all tables if they don't exist
20
  logger.info("Creating all database tables if they do not exist...")
21
  Base.metadata.create_all(bind=engine)
22
+
23
+ # Ensure emergency_allowed column exists
24
+ try:
25
+ db.execute(text("SELECT emergency_allowed FROM attendance LIMIT 1"))
26
+ except Exception:
27
+ db.rollback()
28
+ logger.info("Adding emergency_allowed column to attendance table...")
29
+ db.execute(text("ALTER TABLE attendance ADD COLUMN emergency_allowed BOOLEAN DEFAULT 0"))
30
+ db.commit()
31
+
32
+ # Ensure image_path column exists in attendance_logs table
33
+ try:
34
+ db.execute(text("SELECT image_path FROM attendance_logs LIMIT 1"))
35
+ except Exception:
36
+ db.rollback()
37
+ logger.info("Adding image_path column to attendance_logs table...")
38
+ db.execute(text("ALTER TABLE attendance_logs ADD COLUMN image_path VARCHAR(255) DEFAULT NULL"))
39
+ db.commit()
40
 
41
  # 1. Seed Roles
42
  roles = [
 
75
  {"key": "GRACE_PERIOD_MINUTES", "value": "0", "description": "Grace period for check-ins in minutes"},
76
  {"key": "KIOSK_FACE_THRESHOLD", "value": str(settings.KIOSK_FACE_THRESHOLD), "description": "Cosine similarity threshold for face match"},
77
  {"key": "KIOSK_LIVENESS_THRESHOLD", "value": str(settings.KIOSK_LIVENESS_THRESHOLD), "description": "Softmax probability threshold for face liveness"},
78
+ {"key": "ENROLLMENT_LIVENESS_CHECK", "value": "true", "description": "Enforce liveness check during employee facial enrollment"},
79
+ {"key": "ENROLLMENT_LIVENESS_THRESHOLD", "value": "0.70", "description": "Liveness probability threshold specifically for employee facial enrollment"},
80
  {"key": "VOICE_GREETING_ENABLED", "value": "true", "description": "Enable voice greeting on successful attendance"},
81
  {"key": "SYSTEM_MAINTENANCE_MODE", "value": "false", "description": "Toggle maintenance mode to suspend active check-ins"},
82
  {"key": "MAX_ENROLLMENT_IMAGES", "value": "5", "description": "Maximum face images captured during registration"},
 
101
  )
102
  crud.create_user(db, admin_create)
103
  else:
104
+ logger.info(f"Super Admin user {admin_email} already exists. Syncing password with configuration...")
105
+ admin_user.hashed_password = crud.get_password_hash(settings.INITIAL_ADMIN_PASSWORD)
106
+ db.commit()
107
 
108
  logger.info("Database initialization and seeding completed successfully.")
backend/app/crud/crud.py CHANGED
@@ -100,6 +100,18 @@ def get_employee_by_uuid(db: Session, employee_id: str):
100
  def get_employee_by_email(db: Session, email: str):
101
  return db.execute(select(models.Employee).where(models.Employee.email == email)).scalar_one_or_none()
102
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def get_employees(
104
  db: Session,
105
  skip: int = 0,
@@ -259,7 +271,7 @@ def set_setting(db: Session, key: str, value: str, description: str = None):
259
  return db_setting
260
 
261
  # --- Attendance Logs CRUD ---
262
- def create_attendance_log(db: Session, employee_id: int, camera: str, confidence: float, liveness_score: float, is_spoof: bool, status: str, timestamp: datetime = None):
263
  if not timestamp:
264
  timestamp = datetime.now()
265
  log = models.AttendanceLog(
@@ -269,7 +281,8 @@ def create_attendance_log(db: Session, employee_id: int, camera: str, confidence
269
  liveness_score=liveness_score,
270
  is_spoof=is_spoof,
271
  status=status,
272
- timestamp=timestamp
 
273
  )
274
  db.add(log)
275
  db.commit()
 
100
  def get_employee_by_email(db: Session, email: str):
101
  return db.execute(select(models.Employee).where(models.Employee.email == email)).scalar_one_or_none()
102
 
103
+ def get_employee_by_name(db: Session, name: str):
104
+ return db.execute(select(models.Employee).where(func.lower(models.Employee.name) == func.lower(name))).scalars().first()
105
+
106
+ def get_employee_by_phone(db: Session, phone: str):
107
+ import re
108
+ cleaned = re.sub(r'[\s\-()]', '', phone)
109
+ all_emps = db.execute(select(models.Employee).where(models.Employee.phone.isnot(None))).scalars().all()
110
+ for emp in all_emps:
111
+ if re.sub(r'[\s\-()]', '', emp.phone) == cleaned:
112
+ return emp
113
+ return None
114
+
115
  def get_employees(
116
  db: Session,
117
  skip: int = 0,
 
271
  return db_setting
272
 
273
  # --- Attendance Logs CRUD ---
274
+ def create_attendance_log(db: Session, employee_id: int, camera: str, confidence: float, liveness_score: float, is_spoof: bool, status: str, timestamp: datetime = None, image_path: str = None):
275
  if not timestamp:
276
  timestamp = datetime.now()
277
  log = models.AttendanceLog(
 
281
  liveness_score=liveness_score,
282
  is_spoof=is_spoof,
283
  status=status,
284
+ timestamp=timestamp,
285
+ image_path=image_path
286
  )
287
  db.add(log)
288
  db.commit()
backend/app/main.py CHANGED
@@ -95,10 +95,12 @@ def debug_db():
95
 
96
  @app.get("/health", tags=["Status"])
97
  def health_check():
 
98
  return {
99
  "status": "healthy",
100
  "project": settings.PROJECT_NAME,
101
- "version": "1.0.0"
 
102
  }
103
 
104
 
 
95
 
96
  @app.get("/health", tags=["Status"])
97
  def health_check():
98
+ from app.services.singletons import face_engine
99
  return {
100
  "status": "healthy",
101
  "project": settings.PROJECT_NAME,
102
+ "version": "1.0.0",
103
+ "mock_mode": face_engine.mock_mode
104
  }
105
 
106
 
backend/app/models/models.py CHANGED
@@ -134,6 +134,7 @@ class Attendance(Base):
134
  early_departure = Column(Boolean, default=False)
135
  overtime = Column(Float, default=0.0) # Calculated in hours
136
  status = Column(String(20), default="Absent") # Present, Absent, Late, Half Day, Leave, Holiday, WFH
 
137
 
138
  employee = relationship("Employee", back_populates="attendance_records")
139
 
@@ -148,6 +149,7 @@ class AttendanceLog(Base):
148
  liveness_score = Column(Float, nullable=True)
149
  is_spoof = Column(Boolean, default=False)
150
  status = Column(String(50), nullable=False) # Match Success, Spoof Rejected, Unknown Person, Low Confidence
 
151
 
152
  employee = relationship("Employee", back_populates="attendance_logs")
153
 
 
134
  early_departure = Column(Boolean, default=False)
135
  overtime = Column(Float, default=0.0) # Calculated in hours
136
  status = Column(String(20), default="Absent") # Present, Absent, Late, Half Day, Leave, Holiday, WFH
137
+ emergency_allowed = Column(Boolean, default=False)
138
 
139
  employee = relationship("Employee", back_populates="attendance_records")
140
 
 
149
  liveness_score = Column(Float, nullable=True)
150
  is_spoof = Column(Boolean, default=False)
151
  status = Column(String(50), nullable=False) # Match Success, Spoof Rejected, Unknown Person, Low Confidence
152
+ image_path = Column(String(255), nullable=True)
153
 
154
  employee = relationship("Employee", back_populates="attendance_logs")
155
 
backend/app/schemas/schemas.py CHANGED
@@ -1,6 +1,7 @@
1
- from pydantic import BaseModel, EmailStr, Field, ConfigDict
2
  from typing import Optional, List
3
  from datetime import datetime, date, time, timedelta
 
4
 
5
  # Role Schemas
6
  class RoleBase(BaseModel):
@@ -72,6 +73,17 @@ class EmployeeBase(BaseModel):
72
  status: str = "Active"
73
  department_id: Optional[int] = None
74
 
 
 
 
 
 
 
 
 
 
 
 
75
  class EmployeeCreate(EmployeeBase):
76
  create_user_login: bool = False
77
  password: Optional[str] = None # Required if create_user_login is True
@@ -124,6 +136,7 @@ class AttendanceOut(AttendanceBase):
124
  late_arrival: bool
125
  early_departure: bool
126
  overtime: float
 
127
  employee: Optional[EmployeeOut] = None
128
  model_config = ConfigDict(from_attributes=True)
129
 
@@ -131,6 +144,7 @@ class AttendanceUpdate(BaseModel):
131
  check_in: Optional[datetime] = None
132
  check_out: Optional[datetime] = None
133
  status: Optional[str] = None
 
134
 
135
  # Attendance Log Schemas
136
  class AttendanceLogOut(BaseModel):
@@ -142,6 +156,7 @@ class AttendanceLogOut(BaseModel):
142
  liveness_score: Optional[float] = None
143
  is_spoof: bool
144
  status: str
 
145
  employee: Optional[EmployeeOut] = None
146
  model_config = ConfigDict(from_attributes=True)
147
 
 
1
+ from pydantic import BaseModel, EmailStr, Field, ConfigDict, field_validator
2
  from typing import Optional, List
3
  from datetime import datetime, date, time, timedelta
4
+ import re
5
 
6
  # Role Schemas
7
  class RoleBase(BaseModel):
 
73
  status: str = "Active"
74
  department_id: Optional[int] = None
75
 
76
+ @field_validator('phone')
77
+ @classmethod
78
+ def validate_indian_phone(cls, v: Optional[str]) -> Optional[str]:
79
+ if v is None or v.strip() == "":
80
+ return None
81
+ # Clean phone number (remove spaces, hyphens, brackets)
82
+ cleaned = re.sub(r'[\s\-()]', '', v)
83
+ if not re.match(r'^(?:\+91|91|0)?[6-9]\d{9}$', cleaned):
84
+ raise ValueError('Invalid Indian mobile number. Must be a 10-digit number starting with 6-9, optionally prefixed with +91, 91, or 0.')
85
+ return v
86
+
87
  class EmployeeCreate(EmployeeBase):
88
  create_user_login: bool = False
89
  password: Optional[str] = None # Required if create_user_login is True
 
136
  late_arrival: bool
137
  early_departure: bool
138
  overtime: float
139
+ emergency_allowed: bool = False
140
  employee: Optional[EmployeeOut] = None
141
  model_config = ConfigDict(from_attributes=True)
142
 
 
144
  check_in: Optional[datetime] = None
145
  check_out: Optional[datetime] = None
146
  status: Optional[str] = None
147
+ emergency_allowed: Optional[bool] = None
148
 
149
  # Attendance Log Schemas
150
  class AttendanceLogOut(BaseModel):
 
156
  liveness_score: Optional[float] = None
157
  is_spoof: bool
158
  status: str
159
+ image_path: Optional[str] = None
160
  employee: Optional[EmployeeOut] = None
161
  model_config = ConfigDict(from_attributes=True)
162
 
backend/app/services/face_engine.py CHANGED
@@ -67,15 +67,22 @@ class FaceEngine:
67
  def _init_sessions(self):
68
  try:
69
  import gc
70
- # Initialize ONNX Runtime Inference Sessions with memory-optimized settings
71
- # to prevent OOM crashes on low-resource servers (like Render's 512MB Free Tier)
72
  opts = ort.SessionOptions()
73
- opts.intra_op_num_threads = 1
74
- opts.inter_op_num_threads = 1
75
- opts.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
76
- opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
77
- opts.enable_cpu_mem_arena = False
78
- opts.add_session_config_entry("memory.enable_memory_arena_shrinkage", "cpu:0")
 
 
 
 
 
 
 
 
 
79
 
80
  providers = ['CPUExecutionProvider']
81
  # If GPU is available (optional setup)
@@ -387,7 +394,7 @@ class FaceEngine:
387
  vec = np.random.randn(512).astype(np.float32)
388
  return vec / np.linalg.norm(vec)
389
 
390
- def check_liveness(self, image_np, bbox):
391
  """
392
  Silent Face Anti-Spoofing MiniFASNet model.
393
  Crops face, resizes, runs liveness model.
@@ -464,7 +471,8 @@ class FaceEngine:
464
  else:
465
  avg_score = score_27
466
 
467
- is_live = avg_score >= settings.KIOSK_LIVENESS_THRESHOLD
 
468
  return avg_score, is_live
469
 
470
  except Exception as e:
 
67
  def _init_sessions(self):
68
  try:
69
  import gc
 
 
70
  opts = ort.SessionOptions()
71
+ low_mem = getattr(settings, "LOW_MEMORY_MODE", True)
72
+ if low_mem:
73
+ opts.intra_op_num_threads = 1
74
+ opts.inter_op_num_threads = 1
75
+ opts.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
76
+ opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
77
+ opts.enable_cpu_mem_arena = False
78
+ opts.add_session_config_entry("memory.enable_memory_arena_shrinkage", "cpu:0")
79
+ logger.info("ONNX Runtime running in LOW MEMORY MODE (optimized for <= 512MB RAM instances).")
80
+ else:
81
+ # High-performance multi-threaded settings for production (VPS/Dedicated)
82
+ threads = getattr(settings, "ORT_INTRA_OP_NUM_THREADS", 0)
83
+ opts.intra_op_num_threads = threads
84
+ opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
85
+ logger.info(f"ONNX Runtime running in HIGH PERFORMANCE MODE (intra_op_threads={threads if threads > 0 else 'auto'}, all optimizations enabled).")
86
 
87
  providers = ['CPUExecutionProvider']
88
  # If GPU is available (optional setup)
 
394
  vec = np.random.randn(512).astype(np.float32)
395
  return vec / np.linalg.norm(vec)
396
 
397
+ def check_liveness(self, image_np, bbox, threshold=None):
398
  """
399
  Silent Face Anti-Spoofing MiniFASNet model.
400
  Crops face, resizes, runs liveness model.
 
471
  else:
472
  avg_score = score_27
473
 
474
+ liveness_threshold = threshold if threshold is not None else settings.KIOSK_LIVENESS_THRESHOLD
475
+ is_live = avg_score >= liveness_threshold
476
  return avg_score, is_live
477
 
478
  except Exception as e:
backend/app/tests/test_employees.py CHANGED
@@ -8,7 +8,7 @@ def test_read_employees(mock_get, authenticated_client):
8
  employee_id="EMP101",
9
  name="John Doe",
10
  email="john@netraid.ai",
11
- phone="1234567890",
12
  designation="Software Engineer",
13
  joining_date="2026-06-10",
14
  status="Active"
@@ -30,11 +30,15 @@ def test_read_employee_not_found(mock_get_id, authenticated_client):
30
 
31
  @patch("app.api.v1.employees.crud.get_employee_by_uuid")
32
  @patch("app.api.v1.employees.crud.get_employee_by_email")
 
 
33
  @patch("app.api.v1.employees.crud.create_employee")
34
  @patch("app.api.v1.employees.crud.create_audit_log")
35
- def test_create_employee(mock_audit, mock_create, mock_get_email, mock_get_uuid, authenticated_client):
36
  mock_get_uuid.return_value = None
37
  mock_get_email.return_value = None
 
 
38
 
39
  mock_employee = models.Employee(
40
  id=2,
@@ -50,7 +54,7 @@ def test_create_employee(mock_audit, mock_create, mock_get_email, mock_get_uuid,
50
  "employee_id": "EMP102",
51
  "name": "Jane Smith",
52
  "email": "jane@netraid.ai",
53
- "phone": "9876543210",
54
  "designation": "HR Manager",
55
  "joining_date": "2026-06-10",
56
  "status": "Active",
 
8
  employee_id="EMP101",
9
  name="John Doe",
10
  email="john@netraid.ai",
11
+ phone="9876543210",
12
  designation="Software Engineer",
13
  joining_date="2026-06-10",
14
  status="Active"
 
30
 
31
  @patch("app.api.v1.employees.crud.get_employee_by_uuid")
32
  @patch("app.api.v1.employees.crud.get_employee_by_email")
33
+ @patch("app.api.v1.employees.crud.get_employee_by_name")
34
+ @patch("app.api.v1.employees.crud.get_employee_by_phone")
35
  @patch("app.api.v1.employees.crud.create_employee")
36
  @patch("app.api.v1.employees.crud.create_audit_log")
37
+ def test_create_employee(mock_audit, mock_create, mock_get_phone, mock_get_name, mock_get_email, mock_get_uuid, authenticated_client):
38
  mock_get_uuid.return_value = None
39
  mock_get_email.return_value = None
40
+ mock_get_name.return_value = None
41
+ mock_get_phone.return_value = None
42
 
43
  mock_employee = models.Employee(
44
  id=2,
 
54
  "employee_id": "EMP102",
55
  "name": "Jane Smith",
56
  "email": "jane@netraid.ai",
57
+ "phone": "9876543211",
58
  "designation": "HR Manager",
59
  "joining_date": "2026-06-10",
60
  "status": "Active",
frontend/app/attendance/page.tsx CHANGED
@@ -3,6 +3,7 @@
3
  import React, { useState } from "react";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
  import SidebarLayout from "@/components/SidebarLayout";
 
6
  import { fetchApi, parseDateTime, getLocalDateString, getBackendUrl } from "@/app/utils/api";
7
  import { Clock, Search, Edit3, Calendar, Activity, AlertTriangle, X, CheckCircle2, ChevronDown, Check } from "lucide-react";
8
 
@@ -34,7 +35,7 @@ function EmployeeAvatar({ emp, avatarColor, size = "md" }: { emp: any; avatarCol
34
  const [error, setError] = useState(false);
35
  const hasFrontImage = emp.images?.some((img: any) => img.pose_type.toLowerCase() === "front");
36
 
37
- const sc = size === "sm" ? "w-7.5 h-7.5 text-[10px]" : "w-8 h-8 text-[11px]";
38
 
39
  if (hasFrontImage && !error) {
40
  const baseUrl = getBackendUrl().replace("/api/v1", "");
@@ -42,14 +43,14 @@ function EmployeeAvatar({ emp, avatarColor, size = "md" }: { emp: any; avatarCol
42
  <img
43
  src={`${baseUrl}/uploads/${emp.employee_id}/front.jpg`}
44
  alt={emp.name}
45
- className={`${sc.split(" ")[0]} ${sc.split(" ")[1]} rounded-lg object-cover border border-zinc-250 shadow-sm shrink-0`}
46
  onError={() => setError(true)}
47
  />
48
  );
49
  }
50
 
51
  return (
52
- <div className={`${sc} rounded-lg bg-gradient-to-br ${avatarColor} flex items-center justify-center shrink-0 border font-bold shadow-sm`}>
53
  {emp.name.charAt(0).toUpperCase()}
54
  </div>
55
  );
@@ -57,6 +58,7 @@ function EmployeeAvatar({ emp, avatarColor, size = "md" }: { emp: any; avatarCol
57
 
58
  export default function AttendancePage() {
59
  const queryClient = useQueryClient();
 
60
  const [activeTab, setActiveTab] = useState<Tab>("feed");
61
  const [selectedDate, setSelectedDate] = useState(getLocalDateString());
62
  const [search, setSearch] = useState("");
@@ -66,6 +68,7 @@ export default function AttendancePage() {
66
  const [checkInTime, setCheckInTime] = useState("");
67
  const [checkOutTime, setCheckOutTime] = useState("");
68
  const [statusVal, setStatusVal] = useState("Present");
 
69
 
70
  const { data: departments } = useQuery({
71
  queryKey: ["departments"],
@@ -95,7 +98,7 @@ export default function AttendancePage() {
95
  setShowEditDialog(false);
96
  setEditingRecord(null);
97
  },
98
- onError: (err: any) => alert(err.message || "Failed to update attendance.")
99
  });
100
 
101
  const handleEditClick = (record: any) => {
@@ -109,6 +112,7 @@ export default function AttendancePage() {
109
  setCheckInTime(fmt(record.check_in));
110
  setCheckOutTime(fmt(record.check_out));
111
  setStatusVal(record.status);
 
112
  setShowEditDialog(true);
113
  };
114
 
@@ -119,12 +123,17 @@ export default function AttendancePage() {
119
  if (!t) return null;
120
  const [yr, mo, dy] = selectedDate.split("-").map(Number);
121
  const [hr, mn] = t.split(":").map(Number);
122
- const localDate = new Date(yr, mo - 1, dy, hr, mn, 0);
123
- return localDate.toISOString();
124
  };
125
  updateMutation.mutate({
126
  id: editingRecord.id,
127
- payload: { status: statusVal, check_in: combine(checkInTime), check_out: combine(checkOutTime) }
 
 
 
 
 
128
  });
129
  };
130
 
@@ -152,7 +161,6 @@ export default function AttendancePage() {
152
  <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-5 border-b border-white/5">
153
  <div>
154
  <h1 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">Attendance Ledger</h1>
155
- <p className="text-xs text-slate-500 mt-0.5">Audit daily presence ledger records and raw biometric authentication logs.</p>
156
  </div>
157
 
158
  {/* Tab switcher */}
@@ -310,12 +318,12 @@ export default function AttendancePage() {
310
  <table className="data-table">
311
  <thead>
312
  <tr>
313
- <th className="text-left py-3.5 px-5">Timestamp</th>
314
- <th className="text-left py-3.5 px-5">Identity Profile</th>
315
- <th className="text-left py-3.5 px-5">Terminal</th>
316
- <th className="text-left py-3.5 px-5">Confidence</th>
317
- <th className="text-left py-3.5 px-5">Liveness</th>
318
- <th className="text-left py-3.5 px-5">Result</th>
319
  </tr>
320
  </thead>
321
  <tbody>
@@ -344,45 +352,93 @@ export default function AttendancePage() {
344
  const avatarColor = log.employee ? avatarColors[log.employee.id % avatarColors.length] : "";
345
  return (
346
  <tr key={log.id}>
347
- <td className="py-3.5 px-5 font-mono text-[11px] text-zinc-500">
348
- {parseDateTime(log.timestamp)?.toLocaleDateString([], { month: "short", day: "numeric" })} {parseDateTime(log.timestamp)?.toLocaleTimeString([], {hour:"2-digit",minute:"2-digit",second:"2-digit"})}
 
 
 
 
 
349
  </td>
350
- <td className="py-3.5 px-5">
351
  {log.employee ? (
352
  <div className="flex items-center gap-2.5">
353
  <EmployeeAvatar emp={log.employee} avatarColor={avatarColor} size="sm" />
354
  <div>
355
- <p className="text-[12.5px] font-semibold text-[var(--text-primary)]">{log.employee.name}</p>
356
- <p className="text-[10px] text-slate-500 font-mono mt-0.5">{log.employee.employee_id}</p>
357
  </div>
358
  </div>
359
  ) : (
360
- <span className="text-slate-500 text-[12.5px] italic">Unknown Identity</span>
 
 
 
 
 
 
 
 
361
  )}
362
  </td>
363
- <td className="py-3.5 px-5 text-[11px] text-slate-400 font-mono">{log.camera}</td>
364
- <td className="py-3.5 px-5 font-mono text-[11.5px]">
 
 
 
 
 
365
  {log.confidence ? (
366
- <span className={log.confidence >= 0.60 ? "text-[var(--text-primary)]" : "text-amber-500 font-semibold"}>
367
- {log.confidence.toFixed(3)}
368
- </span>
 
 
 
 
 
 
 
 
 
 
 
369
  ) : (
370
- <span className="text-slate-600">—</span>
371
  )}
372
  </td>
373
- <td className="py-3.5 px-5 font-mono text-[11.5px]">
374
  {log.liveness_score ? (
375
- <span className={log.liveness_score < 0.80 ? "text-rose-400 font-semibold" : "text-emerald-400"}>
376
- {log.liveness_score.toFixed(3)}
377
- </span>
 
 
 
 
 
 
 
 
 
 
 
378
  ) : (
379
- <span className="text-slate-600">—</span>
380
  )}
381
  </td>
382
- <td className="py-3.5 px-5">
383
- <span className={`badge ${isSuccess ? "badge-emerald" : isSpoof ? "badge-rose animate-pulse" : "badge-amber"} flex items-center gap-1.5 w-fit`}>
384
- {isSpoof && <AlertTriangle className="w-3 h-3 text-rose-400" />}
385
- {isSuccess ? "Matched" : isSpoof ? "Spoof Block" : log.status}
 
 
 
 
 
 
 
 
386
  </span>
387
  </td>
388
  </tr>
@@ -438,6 +494,21 @@ export default function AttendancePage() {
438
  <ChevronDown className="absolute right-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" />
439
  </div>
440
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
  <div className="flex justify-end gap-2.5 pt-3 border-t border-white/5">
443
  <button
 
3
  import React, { useState } from "react";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
  import SidebarLayout from "@/components/SidebarLayout";
6
+ import { useToast } from "@/app/utils/toast";
7
  import { fetchApi, parseDateTime, getLocalDateString, getBackendUrl } from "@/app/utils/api";
8
  import { Clock, Search, Edit3, Calendar, Activity, AlertTriangle, X, CheckCircle2, ChevronDown, Check } from "lucide-react";
9
 
 
35
  const [error, setError] = useState(false);
36
  const hasFrontImage = emp.images?.some((img: any) => img.pose_type.toLowerCase() === "front");
37
 
38
+ const sizeCls = size === "sm" ? "w-8 h-8 text-[10px]" : "w-10 h-10 text-[11px]";
39
 
40
  if (hasFrontImage && !error) {
41
  const baseUrl = getBackendUrl().replace("/api/v1", "");
 
43
  <img
44
  src={`${baseUrl}/uploads/${emp.employee_id}/front.jpg`}
45
  alt={emp.name}
46
+ className={`${size === "sm" ? "w-8 h-8" : "w-10 h-10"} rounded-lg object-cover border border-zinc-250 shadow-sm shrink-0`}
47
  onError={() => setError(true)}
48
  />
49
  );
50
  }
51
 
52
  return (
53
+ <div className={`${sizeCls} rounded-lg bg-gradient-to-br ${avatarColor} flex items-center justify-center shrink-0 border font-bold shadow-sm`}>
54
  {emp.name.charAt(0).toUpperCase()}
55
  </div>
56
  );
 
58
 
59
  export default function AttendancePage() {
60
  const queryClient = useQueryClient();
61
+ const { toast } = useToast();
62
  const [activeTab, setActiveTab] = useState<Tab>("feed");
63
  const [selectedDate, setSelectedDate] = useState(getLocalDateString());
64
  const [search, setSearch] = useState("");
 
68
  const [checkInTime, setCheckInTime] = useState("");
69
  const [checkOutTime, setCheckOutTime] = useState("");
70
  const [statusVal, setStatusVal] = useState("Present");
71
+ const [emergencyAllowed, setEmergencyAllowed] = useState(false);
72
 
73
  const { data: departments } = useQuery({
74
  queryKey: ["departments"],
 
98
  setShowEditDialog(false);
99
  setEditingRecord(null);
100
  },
101
+ onError: (err: any) => toast.error(err.message || "Failed to update attendance.")
102
  });
103
 
104
  const handleEditClick = (record: any) => {
 
112
  setCheckInTime(fmt(record.check_in));
113
  setCheckOutTime(fmt(record.check_out));
114
  setStatusVal(record.status);
115
+ setEmergencyAllowed(record.emergency_allowed || false);
116
  setShowEditDialog(true);
117
  };
118
 
 
123
  if (!t) return null;
124
  const [yr, mo, dy] = selectedDate.split("-").map(Number);
125
  const [hr, mn] = t.split(":").map(Number);
126
+ const pad = (n: number) => String(n).padStart(2, "0");
127
+ return `${yr}-${pad(mo)}-${pad(dy)}T${pad(hr)}:${pad(mn)}:00`;
128
  };
129
  updateMutation.mutate({
130
  id: editingRecord.id,
131
+ payload: {
132
+ status: statusVal,
133
+ check_in: combine(checkInTime),
134
+ check_out: combine(checkOutTime),
135
+ emergency_allowed: emergencyAllowed
136
+ }
137
  });
138
  };
139
 
 
161
  <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-5 border-b border-white/5">
162
  <div>
163
  <h1 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">Attendance Ledger</h1>
 
164
  </div>
165
 
166
  {/* Tab switcher */}
 
318
  <table className="data-table">
319
  <thead>
320
  <tr>
321
+ <th className="text-left py-3.5 px-5 whitespace-nowrap">Timestamp</th>
322
+ <th className="text-left py-3.5 px-5 whitespace-nowrap">Identity Profile</th>
323
+ <th className="text-left py-3.5 px-5 whitespace-nowrap">Terminal</th>
324
+ <th className="text-left py-3.5 px-5 whitespace-nowrap">Confidence</th>
325
+ <th className="text-left py-3.5 px-5 whitespace-nowrap">Liveness</th>
326
+ <th className="text-left py-3.5 px-5 whitespace-nowrap">Result</th>
327
  </tr>
328
  </thead>
329
  <tbody>
 
352
  const avatarColor = log.employee ? avatarColors[log.employee.id % avatarColors.length] : "";
353
  return (
354
  <tr key={log.id}>
355
+ <td className="py-3.5 px-5 font-mono text-[11px] text-slate-500 whitespace-nowrap">
356
+ <div className="font-semibold text-slate-700">
357
+ {parseDateTime(log.timestamp)?.toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" })}
358
+ </div>
359
+ <div className="text-[10px] text-slate-400 mt-0.5">
360
+ {parseDateTime(log.timestamp)?.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
361
+ </div>
362
  </td>
363
+ <td className="py-3.5 px-5 whitespace-nowrap">
364
  {log.employee ? (
365
  <div className="flex items-center gap-2.5">
366
  <EmployeeAvatar emp={log.employee} avatarColor={avatarColor} size="sm" />
367
  <div>
368
+ <p className="text-[12.5px] font-semibold text-slate-900 leading-none">{log.employee.name}</p>
369
+ <p className="text-[10px] text-slate-400 font-mono mt-1 leading-none">{log.employee.employee_id}</p>
370
  </div>
371
  </div>
372
  ) : (
373
+ <div className="flex items-center gap-2.5">
374
+ <div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center border border-dashed border-slate-200 shrink-0">
375
+ <AlertTriangle className="w-3.5 h-3.5 text-slate-400" />
376
+ </div>
377
+ <div>
378
+ <p className="text-[12.5px] font-medium text-slate-400 italic leading-none">Unknown Identity</p>
379
+ <p className="text-[10px] text-slate-350 font-mono mt-1 leading-none">External Scan</p>
380
+ </div>
381
+ </div>
382
  )}
383
  </td>
384
+ <td className="py-3.5 px-5 whitespace-nowrap">
385
+ <span className="inline-flex items-center gap-1.5 px-2 py-1 bg-slate-50 border border-slate-100 rounded-lg text-[10px] font-mono text-slate-600 font-semibold uppercase tracking-wider">
386
+ <span className="w-1.5 h-1.5 rounded-full bg-slate-400 shrink-0" />
387
+ {log.camera}
388
+ </span>
389
+ </td>
390
+ <td className="py-3.5 px-5 font-mono text-[11.5px] whitespace-nowrap">
391
  {log.confidence ? (
392
+ <div className="space-y-1">
393
+ <div className="flex items-center gap-1.5">
394
+ <span className={`w-1.5 h-1.5 rounded-full ${log.confidence >= 0.75 ? "bg-emerald-500" : log.confidence >= 0.60 ? "bg-amber-500" : "bg-rose-500"}`} />
395
+ <span className="font-semibold text-slate-700">
396
+ {(log.confidence * 100).toFixed(1)}%
397
+ </span>
398
+ </div>
399
+ <div className="w-16 h-1 bg-slate-100 rounded-full overflow-hidden">
400
+ <div
401
+ className={`h-full rounded-full ${log.confidence >= 0.75 ? "bg-emerald-500" : log.confidence >= 0.60 ? "bg-amber-500" : "bg-rose-500"}`}
402
+ style={{ width: `${log.confidence * 100}%` }}
403
+ />
404
+ </div>
405
+ </div>
406
  ) : (
407
+ <span className="text-slate-400 italic">—</span>
408
  )}
409
  </td>
410
+ <td className="py-3.5 px-5 font-mono text-[11.5px] whitespace-nowrap">
411
  {log.liveness_score ? (
412
+ <div className="space-y-1">
413
+ <div className="flex items-center gap-1.5">
414
+ <span className={`w-1.5 h-1.5 rounded-full ${log.liveness_score >= 0.85 ? "bg-emerald-500" : log.liveness_score >= 0.70 ? "bg-amber-500" : "bg-rose-500"}`} />
415
+ <span className="font-semibold text-slate-700">
416
+ {(log.liveness_score * 100).toFixed(1)}%
417
+ </span>
418
+ </div>
419
+ <div className="w-16 h-1 bg-slate-100 rounded-full overflow-hidden">
420
+ <div
421
+ className={`h-full rounded-full ${log.liveness_score >= 0.85 ? "bg-emerald-500" : log.liveness_score >= 0.70 ? "bg-amber-500" : "bg-rose-500"}`}
422
+ style={{ width: `${log.liveness_score * 100}%` }}
423
+ />
424
+ </div>
425
+ </div>
426
  ) : (
427
+ <span className="text-slate-400 italic">—</span>
428
  )}
429
  </td>
430
+ <td className="py-3.5 px-5 whitespace-nowrap">
431
+ <span className={`badge ${
432
+ isSuccess
433
+ ? "badge-emerald"
434
+ : isSpoof
435
+ ? "badge-rose animate-pulse"
436
+ : log.status.toLowerCase().includes("lock")
437
+ ? "badge-rose"
438
+ : "badge-amber"
439
+ } flex items-center gap-1.5 w-fit font-bold`}>
440
+ {isSpoof && <AlertTriangle className="w-3 h-3 text-rose-500" />}
441
+ {isSuccess ? "Matched" : isSpoof ? "Liveness Failed" : log.status}
442
  </span>
443
  </td>
444
  </tr>
 
494
  <ChevronDown className="absolute right-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" />
495
  </div>
496
  </div>
497
+
498
+ <div className="p-3 rounded-2xl bg-zinc-55 border border-zinc-150 space-y-1 mt-2">
499
+ <label className="flex items-center gap-2.5 cursor-pointer select-none">
500
+ <input
501
+ type="checkbox"
502
+ checked={emergencyAllowed}
503
+ onChange={() => setEmergencyAllowed(!emergencyAllowed)}
504
+ className="w-4 h-4 rounded border-slate-350 text-slate-950 bg-white focus:ring-slate-500/50 cursor-pointer"
505
+ />
506
+ <span className="text-[12px] font-bold text-slate-800">Allow Emergency Re-entry (Bypass Lock)</span>
507
+ </label>
508
+ <p className="text-[10px] text-slate-500 leading-normal pl-6.5">
509
+ Allows employee to scan again at the kiosk today after check-out. The lock will automatically re-apply upon their next scan.
510
+ </p>
511
+ </div>
512
 
513
  <div className="flex justify-end gap-2.5 pt-3 border-t border-white/5">
514
  <button
frontend/app/audit/page.tsx CHANGED
@@ -49,7 +49,6 @@ export default function AuditLogsPage() {
49
  <History className="w-5 h-5 text-zinc-700" />
50
  System Audit Logs
51
  </h1>
52
- <p className="text-xs text-zinc-500 mt-0.5">Track admin dashboard actions, user activities, and kiosk alerts.</p>
53
  </div>
54
  <button
55
  onClick={() => refetch()}
 
49
  <History className="w-5 h-5 text-zinc-700" />
50
  System Audit Logs
51
  </h1>
 
52
  </div>
53
  <button
54
  onClick={() => refetch()}
frontend/app/dashboard/page.tsx CHANGED
@@ -67,32 +67,21 @@ function StatCard({
67
  color: "blue" | "emerald" | "amber" | "rose" | "indigo";
68
  loading?: boolean; sublabel?: string; sparkData: number[];
69
  }) {
70
- const colors = {
71
- blue: { icon: "text-zinc-600", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" },
72
- emerald: { icon: "text-zinc-800", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" },
73
- amber: { icon: "text-zinc-650", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" },
74
- rose: { icon: "text-zinc-700", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" },
75
- indigo: { icon: "text-zinc-900", bg: "bg-zinc-100", border: "border-zinc-200 hover:border-zinc-400", val: "text-zinc-900" },
76
- };
77
- const c = colors[color];
78
-
79
  return (
80
- <div className={`glass-card rounded-2xl p-5 border ${c.border} stat-card flex flex-col justify-between h-[145px] hover:scale-[1.01] hover:shadow-[0_8px_30px_rgb(0,0,0,0.12)] transition-all duration-300`}>
81
  <div className="flex items-center justify-between">
82
- <p className="text-[11px] text-slate-400 font-semibold uppercase tracking-wider">{label}</p>
83
- <div className={`w-8.5 h-8.5 rounded-xl ${c.bg} ${c.icon} flex items-center justify-center border border-white/5`}>
84
- <Icon className="w-4 h-4" />
85
- </div>
86
  </div>
87
 
88
  <div className="flex items-end justify-between mt-auto">
89
- <div className="space-y-1">
90
  {loading ? (
91
- <div className="skeleton h-8 w-20" />
92
  ) : (
93
- <p className="text-2xl font-bold tracking-tight text-[var(--text-primary)]">{value}</p>
94
  )}
95
- {sublabel && <p className="text-[10px] text-slate-500 font-mono">{sublabel}</p>}
96
  </div>
97
  {!loading && <Sparkline color={color} data={sparkData} />}
98
  </div>
@@ -101,78 +90,79 @@ function StatCard({
101
  }
102
 
103
  const AVATAR_COLORS = [
104
- "from-blue-500/20 to-indigo-500/20 text-blue-400 border-blue-500/15",
105
- "from-emerald-500/20 to-teal-500/20 text-emerald-400 border-emerald-500/15",
106
- "from-amber-500/20 to-orange-500/20 text-amber-400 border-amber-500/15",
107
- "from-indigo-500/20 to-purple-500/20 text-indigo-400 border-indigo-500/15",
108
- "from-cyan-500/20 to-blue-500/20 text-cyan-400 border-cyan-500/15",
109
  ];
110
 
111
  function ScanLogItem({ log }: { log: any }) {
 
112
  const isSuccess = log.status === "Match Success";
113
  const isSpoof = log.is_spoof;
114
  const isUnknown = log.status === "Unknown Person";
115
 
116
- let dotColor = "bg-slate-600";
117
- let statusColor = "text-slate-500";
118
- let statusBg = "bg-slate-500/8 border-slate-500/15";
119
  let statusLabel = log.status;
120
 
121
  if (isSuccess) {
122
- dotColor = "bg-emerald-400";
123
- statusColor = "text-emerald-400";
124
- statusBg = "bg-emerald-500/8 border-emerald-500/15";
125
  statusLabel = "Matched";
126
  } else if (isSpoof) {
127
- dotColor = "bg-rose-500 animate-ping";
128
- statusColor = "text-rose-400";
129
- statusBg = "bg-rose-500/8 border-rose-500/20";
130
- statusLabel = "Spoof Blocked";
131
  } else if (isUnknown) {
132
- dotColor = "bg-amber-400";
133
- statusColor = "text-amber-400";
134
- statusBg = "bg-amber-500/8 border-amber-500/20";
135
  statusLabel = "Unknown";
136
  }
137
 
138
- // Generate a nice random avatar gradient color based on employee ID
139
- const colorIndex = log.employee ? (log.employee.id % AVATAR_COLORS.length) : 0;
140
- const avatarCls = AVATAR_COLORS[colorIndex];
141
 
142
  return (
143
- <div className="flex items-center gap-3.5 py-3 px-3.5 rounded-xl hover:bg-white/[0.02] border border-transparent hover:border-white/5 transition-all duration-200 group">
144
  <div className="relative shrink-0">
145
  {log.employee ? (
146
- <div className={`w-9 h-9 rounded-xl bg-gradient-to-br ${avatarCls} flex items-center justify-center font-bold text-xs border`}>
147
- {log.employee.name.charAt(0).toUpperCase()}
148
- </div>
 
 
 
 
 
 
 
 
 
149
  ) : (
150
- <div className="w-9 h-9 rounded-xl bg-slate-500/10 text-slate-500 flex items-center justify-center font-bold text-xs border border-white/5">
151
  ?
152
  </div>
153
  )}
154
- <div className={`absolute -bottom-1 -right-1 w-3 h-3 rounded-full bg-[#050810] flex items-center justify-center`}>
155
- <div className={`w-1.5 h-1.5 rounded-full ${dotColor.split(" ")[0]}`} />
156
  </div>
157
  </div>
158
 
159
  <div className="flex-1 min-w-0">
160
- <div className="flex items-center gap-2">
161
- <p className="text-[13px] font-semibold text-[var(--text-primary)] truncate">
162
- {log.employee ? log.employee.name : "Unknown Person"}
163
  </p>
164
- <span className="text-[9px] text-zinc-500 font-mono">· {log.camera}</span>
165
  </div>
166
- <p className="text-[10px] text-slate-500 truncate mt-0.5">
167
  {log.employee ? `${log.employee.designation} (${log.employee.employee_id})` : "Unauthorized access attempt"}
168
  </p>
169
  </div>
170
 
171
  <div className="shrink-0 text-right space-y-1">
172
- <p className="text-[10px] text-slate-500 font-mono leading-none">
173
  {parseDateTime(log.timestamp)?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) || ""}
174
  </p>
175
- <span className={`inline-block text-[9px] font-mono font-semibold px-2 py-0.5 rounded-full border ${statusBg} ${statusColor}`}>
176
  {statusLabel}
177
  </span>
178
  </div>
@@ -181,43 +171,11 @@ function ScanLogItem({ log }: { log: any }) {
181
  }
182
 
183
  const getDeptTheme = (code: string) => {
184
- const themes = {
185
- ENG: {
186
- bg: "bg-blue-50/50 hover:bg-blue-50/70 border-blue-200 text-blue-900",
187
- iconBg: "bg-blue-500/10 text-blue-600 border-blue-500/20",
188
- barBg: "bg-blue-100",
189
- barFill: "bg-gradient-to-r from-blue-500 to-blue-600"
190
- },
191
- HR: {
192
- bg: "bg-purple-50/50 hover:bg-purple-50/70 border-purple-200 text-purple-900",
193
- iconBg: "bg-purple-500/10 text-purple-600 border-purple-500/20",
194
- barBg: "bg-purple-100",
195
- barFill: "bg-gradient-to-r from-purple-500 to-purple-600"
196
- },
197
- MKT: {
198
- bg: "bg-amber-50/50 hover:bg-amber-50/70 border-amber-200 text-amber-900",
199
- iconBg: "bg-amber-500/10 text-amber-600 border-amber-500/20",
200
- barBg: "bg-amber-100",
201
- barFill: "bg-gradient-to-r from-amber-500 to-amber-600"
202
- },
203
- FIN: {
204
- bg: "bg-emerald-50/50 hover:bg-emerald-50/70 border-emerald-200 text-emerald-900",
205
- iconBg: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20",
206
- barBg: "bg-emerald-100",
207
- barFill: "bg-gradient-to-r from-emerald-500 to-emerald-600"
208
- },
209
- OPS: {
210
- bg: "bg-rose-50/50 hover:bg-rose-50/70 border-rose-200 text-rose-900",
211
- iconBg: "bg-rose-500/10 text-rose-600 border-rose-500/20",
212
- barBg: "bg-rose-100",
213
- barFill: "bg-gradient-to-r from-rose-500 to-rose-600"
214
- },
215
- };
216
- return themes[code as keyof typeof themes] || {
217
- bg: "bg-slate-50/50 hover:bg-slate-50/70 border-slate-200 text-slate-900",
218
- iconBg: "bg-slate-500/10 text-slate-600 border-slate-500/20",
219
- barBg: "bg-slate-100",
220
- barFill: "bg-gradient-to-r from-slate-500 to-slate-600"
221
  };
222
  };
223
 
@@ -226,8 +184,21 @@ export default function DashboardPage() {
226
  const [currentDate, setCurrentDate] = React.useState("");
227
  const [statusFilter, setStatusFilter] = React.useState<"ALL" | "MATCHED" | "UNKNOWN" | "SPOOF">("ALL");
228
  const [selectedDept, setSelectedDept] = React.useState<any | null>(null);
 
229
  const queryClient = useQueryClient();
230
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  const { data: deptAttendance, isLoading: loadingDeptAttendance } = useQuery({
232
  queryKey: ["dept-attendance", selectedDept?.id],
233
  queryFn: () => {
@@ -339,21 +310,30 @@ export default function DashboardPage() {
339
 
340
  const trendOption = () => {
341
  if (!trends) return {};
 
 
 
 
 
 
 
 
 
342
  return {
343
  backgroundColor: "transparent",
344
  tooltip: {
345
  trigger: "axis",
346
- backgroundColor: "#ffffff",
347
- borderColor: "rgba(24,24,27,0.08)",
348
  borderWidth: 1,
349
  shadowColor: "rgba(0,0,0,0.02)",
350
  shadowBlur: 10,
351
- textStyle: { color: "#18181b", fontSize: 11, fontFamily: "var(--font-inter)" },
352
  extraCssText: "border-radius:12px;padding:8px 12px;box-shadow: 0 4px 16px rgba(0,0,0,0.04);"
353
  },
354
  legend: {
355
  data: ["Present", "Late Arrivals"],
356
- textStyle: { color: "#71717a", fontSize: 10, fontFamily: "var(--font-inter)" },
357
  bottom: 0,
358
  icon: "circle",
359
  itemWidth: 8,
@@ -373,14 +353,14 @@ export default function DashboardPage() {
373
  }
374
  return d.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
375
  }),
376
- axisLine: { lineStyle: { color: "rgba(24,24,27,0.06)" } },
377
  axisTick: { show: false },
378
- axisLabel: { color: "#71717a", fontSize: 10, fontFamily: "var(--font-inter)" }
379
  },
380
  yAxis: {
381
  type: "value",
382
- splitLine: { lineStyle: { color: "rgba(24,24,27,0.04)", type: "dashed" } },
383
- axisLabel: { color: "#71717a", fontSize: 10, fontFamily: "var(--font-inter)" },
384
  axisLine: { show: false }
385
  },
386
  series: [
@@ -392,18 +372,18 @@ export default function DashboardPage() {
392
  symbolSize: 6,
393
  data: trends.map((t: any) => t.present),
394
  lineStyle: {
395
- color: "#18181b",
396
  width: 2.25,
397
- shadowColor: "rgba(24,24,27,0.1)",
398
  shadowBlur: 8,
399
  shadowOffsetY: 4
400
  },
401
- itemStyle: { color: "#18181b" },
402
  areaStyle: {
403
  color: {
404
  type: "linear", x: 0, y: 0, x2: 0, y2: 1,
405
  colorStops: [
406
- { offset: 0, color: "rgba(24,24,27,0.06)" },
407
  { offset: 1, color: "rgba(24,24,27,0)" }
408
  ]
409
  }
@@ -414,7 +394,7 @@ export default function DashboardPage() {
414
  type: "bar",
415
  data: trends.map((t: any) => t.late),
416
  itemStyle: {
417
- color: "#71717a",
418
  borderRadius: [3, 3, 0, 0]
419
  },
420
  barWidth: 6,
@@ -426,22 +406,32 @@ export default function DashboardPage() {
426
 
427
  const deptOption = () => {
428
  if (!deptStats) return {};
 
 
 
 
 
 
 
 
 
 
429
  return {
430
  backgroundColor: "transparent",
431
  tooltip: {
432
  trigger: "axis",
433
  axisPointer: { type: "shadow" },
434
- backgroundColor: "#ffffff",
435
- borderColor: "rgba(24,24,27,0.08)",
436
  borderWidth: 1,
437
- textStyle: { color: "#18181b", fontSize: 11 },
438
  extraCssText: "border-radius:12px;padding:8px 12px;box-shadow: 0 4px 16px rgba(0,0,0,0.04);"
439
  },
440
  grid: { top: 10, left: 90, right: 20, bottom: 20 },
441
  xAxis: {
442
  type: "value",
443
- splitLine: { lineStyle: { color: "rgba(24,24,27,0.04)", type: "dashed" } },
444
- axisLabel: { color: "#71717a", fontSize: 9 },
445
  axisLine: { show: false }
446
  },
447
  yAxis: {
@@ -449,14 +439,14 @@ export default function DashboardPage() {
449
  data: deptStats.map((d: any) => d.name || d.code),
450
  axisLine: { show: false },
451
  axisTick: { show: false },
452
- axisLabel: { color: "#18181b", fontSize: 10, fontFamily: "var(--font-inter)" }
453
  },
454
  series: [
455
  {
456
  name: "Total Employees",
457
  type: "bar",
458
  data: deptStats.map((d: any) => d.total_employees),
459
- itemStyle: { color: "rgba(24,24,27,0.04)", borderRadius: [0, 4, 4, 0] },
460
  barGap: "-100%",
461
  barWidth: 10
462
  },
@@ -470,8 +460,8 @@ export default function DashboardPage() {
470
  color: {
471
  type: "linear", x: 0, y: 0, x2: 1, y2: 0,
472
  colorStops: [
473
- { offset: 0, color: "#18181b" },
474
- { offset: 1, color: "#27272a" }
475
  ]
476
  }
477
  }
@@ -479,36 +469,29 @@ export default function DashboardPage() {
479
  ]
480
  };
481
  };
482
-
483
  return (
484
  <SidebarLayout>
485
- <div className="space-y-7 page-enter">
486
  {/* ─── Header ─── */}
487
- <div className="flex flex-col md:flex-row md:items-center justify-between pb-5 border-b border-zinc-200 gap-4">
488
- <div>
489
- <h1 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">
490
- Dashboard Overview
491
  </h1>
492
- <p className="text-xs text-slate-500 mt-0.5">
493
- Live biometric monitoring and attendance intelligence platform
494
- </p>
 
495
  </div>
496
- <div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 shrink-0">
497
- {/* Real-time Clock widget */}
 
498
  {currentTime && (
499
- <div className="flex items-center gap-2 px-3 py-1.5 bg-zinc-50 border border-zinc-200 rounded-xl font-mono text-[11px] text-zinc-700 shadow-2xs">
500
- <Calendar className="w-3.5 h-3.5 text-zinc-500" />
501
- <span>{currentDate}</span>
502
- <span className="text-zinc-300 font-sans mx-0.5">|</span>
503
- <Clock className="w-3.5 h-3.5 text-zinc-500" />
504
- <span className="tabular-nums font-semibold">{currentTime}</span>
505
- </div>
506
  )}
507
-
508
- <div className="flex items-center gap-2 text-[10px] font-semibold bg-emerald-50 border border-emerald-200 px-3 py-1.5 rounded-full text-emerald-800">
509
- <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
510
- <span className="font-mono uppercase tracking-wider">Live Feed Active</span>
511
- </div>
512
  </div>
513
  </div>
514
 
@@ -524,17 +507,9 @@ export default function DashboardPage() {
524
  {/* ─── Charts Row ─── */}
525
  <div className="grid grid-cols-1 lg:grid-cols-5 gap-5">
526
  {/* Trend Chart */}
527
- <div className="lg:col-span-3 glass-card rounded-2xl p-5 border border-white/6 flex flex-col justify-between">
528
- <div className="flex items-center justify-between mb-6">
529
- <div className="flex items-center gap-2.5">
530
- <div className="w-8 h-8 rounded-lg bg-blue-500/10 flex items-center justify-center border border-white/5">
531
- <TrendingUp className="w-4 h-4 text-blue-400" />
532
- </div>
533
- <div>
534
- <h2 className="text-xs font-semibold text-[var(--text-primary)] uppercase tracking-wider">Attendance Trends</h2>
535
- <p className="text-[10px] text-slate-500 mt-0.5">Biometric logs for the past 7 days</p>
536
- </div>
537
- </div>
538
  </div>
539
  <div className="h-56">
540
  {loadingTrends ? (
@@ -548,37 +523,29 @@ export default function DashboardPage() {
548
  </div>
549
 
550
  {/* Live Feed */}
551
- <div className="lg:col-span-2 glass-card rounded-2xl border border-white/6 flex flex-col">
552
- <div className="p-5 pb-3.5 border-b border-white/5">
553
  <div className="flex items-center justify-between">
554
- <div className="flex items-center gap-2.5">
555
- <div className="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center border border-white/5">
556
- <Activity className="w-4 h-4 text-indigo-400" />
557
- </div>
558
- <div>
559
- <h2 className="text-xs font-semibold text-[var(--text-primary)] uppercase tracking-wider">Live Activity Feed</h2>
560
- <p className="text-[10px] text-slate-500 mt-0.5">Real-time scan logs ticker</p>
561
- </div>
562
- </div>
563
- <div className="flex items-center gap-1.5 bg-indigo-500/5 px-2 py-1 rounded-md border border-indigo-500/15">
564
- <span className="w-1 h-1 rounded-full bg-indigo-400 animate-pulse" />
565
- <span className="text-[9px] text-indigo-400 font-mono tracking-wider">STREAMING</span>
566
  </div>
567
  </div>
568
 
569
  {/* Status Filter pills */}
570
- <div className="flex flex-wrap gap-1.5 mt-3.5">
571
  {(["ALL", "MATCHED", "UNKNOWN", "SPOOF"] as const).map((filter) => (
572
  <button
573
  key={filter}
574
  onClick={() => setStatusFilter(filter)}
575
- className={`px-2.5 py-1 rounded-lg text-[9px] font-bold tracking-wide transition-all border cursor-pointer uppercase font-mono ${
576
  statusFilter === filter
577
- ? "bg-zinc-800 border-zinc-700 text-white"
578
- : "bg-transparent border-white/5 text-slate-400 hover:text-slate-200 hover:border-white/10"
579
  }`}
580
  >
581
- {filter === "SPOOF" ? "Spoofs" : filter.toLowerCase()}
582
  </button>
583
  ))}
584
  </div>
@@ -620,10 +587,10 @@ export default function DashboardPage() {
620
  })()}
621
  </div>
622
 
623
- <div className="p-4 border-t border-white/5 bg-white/[0.01]">
624
  <Link
625
  href="/attendance"
626
- className="flex items-center justify-between text-[11px] text-blue-400 hover:text-blue-300 font-semibold transition-colors group"
627
  >
628
  <span>Access Complete Ledger Logs</span>
629
  <ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-0.5 transition-transform" />
@@ -633,17 +600,9 @@ export default function DashboardPage() {
633
  </div>
634
 
635
  {/* ─── Bottom Section: Department Breakdown ─── */}
636
- <div className="glass-card rounded-2xl p-5 border border-white/6">
637
- <div className="flex items-center justify-between mb-5">
638
- <div className="flex items-center gap-2.5">
639
- <div className="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center border border-white/5">
640
- <Users className="w-4 h-4 text-indigo-400" />
641
- </div>
642
- <div>
643
- <h2 className="text-xs font-semibold text-[var(--text-primary)] uppercase tracking-wider">Department Distribution</h2>
644
- <p className="text-[10px] text-slate-500 mt-0.5">Staff presence stats by company department</p>
645
- </div>
646
- </div>
647
  </div>
648
 
649
  <div className="min-h-36">
@@ -660,7 +619,6 @@ export default function DashboardPage() {
660
  ) : (
661
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
662
  {deptStats.map((d: any) => {
663
- const theme = getDeptTheme(d.code);
664
  const percent = d.total_employees > 0
665
  ? Math.round((d.present_today / d.total_employees) * 100)
666
  : 0;
@@ -669,36 +627,36 @@ export default function DashboardPage() {
669
  <div
670
  key={d.code}
671
  onClick={() => setSelectedDept(d)}
672
- className={`p-4 rounded-2xl border transition-all duration-300 flex flex-col justify-between h-32 hover:scale-[1.02] cursor-pointer ${theme.bg}`}
673
  >
674
  {/* Top Header Row */}
675
  <div className="flex items-center justify-between w-full">
676
- <span className={`text-[9.5px] font-bold font-mono px-2 py-0.5 rounded-md border ${theme.iconBg}`}>
677
  {d.code}
678
  </span>
679
 
680
  <div className="text-right">
681
- <span className="text-xs font-bold text-slate-800 tracking-tight">
682
- {d.present_today} <span className="text-slate-450 font-medium">/ {d.total_employees}</span>
683
  </span>
684
  </div>
685
  </div>
686
 
687
  {/* Department Name & Subheading */}
688
- <div className="mt-2 flex-1 min-w-0">
689
  <h3 className="text-xs font-bold text-slate-800 truncate" title={d.department}>
690
  {d.department}
691
  </h3>
692
- <p className="text-[9.5px] text-slate-500 font-mono mt-0.5">
693
- {percent}% present today
694
  </p>
695
  </div>
696
 
697
  {/* Progress Bar */}
698
- <div className="mt-2.5 w-full">
699
- <div className={`w-full h-1.5 rounded-full overflow-hidden ${theme.barBg}`}>
700
  <div
701
- className={`h-full rounded-full transition-all duration-500 ${theme.barFill}`}
702
  style={{ width: `${d.total_employees > 0 ? percent : 0}%` }}
703
  />
704
  </div>
@@ -732,7 +690,7 @@ export default function DashboardPage() {
732
  onClick={() => setSelectedDept(null)}
733
  className="p-2 rounded-xl hover:bg-zinc-100 text-zinc-400 hover:text-zinc-650 transition-all cursor-pointer"
734
  >
735
- <X className="w-4.5 h-4.5" />
736
  </button>
737
  </div>
738
 
@@ -758,7 +716,7 @@ export default function DashboardPage() {
758
  return (
759
  <div key={rec.id} className="flex items-center justify-between py-3">
760
  <div className="flex items-center gap-3">
761
- <div className={`w-8.5 h-8.5 rounded-lg bg-gradient-to-br ${avatarColor} flex items-center justify-center shrink-0 border font-bold text-[10.5px] shadow-sm`}>
762
  {rec.employee.name.charAt(0).toUpperCase()}
763
  </div>
764
  <div>
 
67
  color: "blue" | "emerald" | "amber" | "rose" | "indigo";
68
  loading?: boolean; sublabel?: string; sparkData: number[];
69
  }) {
 
 
 
 
 
 
 
 
 
70
  return (
71
+ <div className="bg-white border border-zinc-100 rounded-xl p-4 flex flex-col justify-between h-[125px] transition-all hover:border-zinc-200">
72
  <div className="flex items-center justify-between">
73
+ <p className="text-[10px] text-slate-400 font-bold uppercase tracking-wider">{label}</p>
74
+ <Icon className="w-3.5 h-3.5 text-slate-405" />
 
 
75
  </div>
76
 
77
  <div className="flex items-end justify-between mt-auto">
78
+ <div className="space-y-0.5">
79
  {loading ? (
80
+ <div className="skeleton h-7 w-16" />
81
  ) : (
82
+ <p className="text-xl font-bold tracking-tight text-slate-900">{value}</p>
83
  )}
84
+ {sublabel && <p className="text-[9px] text-slate-400 font-mono">{sublabel}</p>}
85
  </div>
86
  {!loading && <Sparkline color={color} data={sparkData} />}
87
  </div>
 
90
  }
91
 
92
  const AVATAR_COLORS = [
93
+ "bg-zinc-100 text-zinc-800 border-zinc-200/50",
 
 
 
 
94
  ];
95
 
96
  function ScanLogItem({ log }: { log: any }) {
97
+ const [imgError, setImgError] = React.useState(false);
98
  const isSuccess = log.status === "Match Success";
99
  const isSpoof = log.is_spoof;
100
  const isUnknown = log.status === "Unknown Person";
101
 
102
+ let dotColor = "bg-slate-400";
103
+ let statusColor = "text-zinc-600 bg-zinc-50 border-zinc-100";
 
104
  let statusLabel = log.status;
105
 
106
  if (isSuccess) {
107
+ dotColor = "bg-emerald-500";
108
+ statusColor = "text-emerald-700 bg-emerald-50/50 border-emerald-100/55";
 
109
  statusLabel = "Matched";
110
  } else if (isSpoof) {
111
+ dotColor = "bg-rose-500 animate-pulse";
112
+ statusColor = "text-rose-700 bg-rose-50/50 border-rose-100/55";
113
+ statusLabel = "Liveness Failed";
 
114
  } else if (isUnknown) {
115
+ dotColor = "bg-amber-500";
116
+ statusColor = "text-amber-700 bg-amber-50/50 border-amber-100/55";
 
117
  statusLabel = "Unknown";
118
  }
119
 
120
+ const hasFrontImage = log.employee?.images?.some((img: any) => img.pose_type.toLowerCase() === "front");
121
+ const baseUrl = getBackendUrl().replace("/api/v1", "");
 
122
 
123
  return (
124
+ <div className="flex items-center gap-3 py-2 px-2.5 rounded-lg hover:bg-zinc-50 transition-all duration-150 group">
125
  <div className="relative shrink-0">
126
  {log.employee ? (
127
+ hasFrontImage && !imgError ? (
128
+ <img
129
+ src={`${baseUrl}/uploads/${log.employee.employee_id}/front.jpg`}
130
+ alt={log.employee.name}
131
+ className="w-8 h-8 rounded-full object-cover border border-zinc-200/40"
132
+ onError={() => setImgError(true)}
133
+ />
134
+ ) : (
135
+ <div className="w-8 h-8 rounded-full bg-zinc-100 border border-zinc-200/40 flex items-center justify-center font-bold text-xs text-zinc-800">
136
+ {log.employee.name.charAt(0).toUpperCase()}
137
+ </div>
138
+ )
139
  ) : (
140
+ <div className="w-8 h-8 rounded-full bg-zinc-50 border border-zinc-250/30 text-zinc-400 flex items-center justify-center font-bold text-xs">
141
  ?
142
  </div>
143
  )}
144
+ <div className="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full bg-white flex items-center justify-center shadow-xs">
145
+ <div className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
146
  </div>
147
  </div>
148
 
149
  <div className="flex-1 min-w-0">
150
+ <div className="flex items-center gap-1.5">
151
+ <p className="text-[12.5px] font-semibold text-slate-800 truncate">
152
+ {log.employee ? log.employee.name : "Unknown"}
153
  </p>
154
+ <span className="text-[9px] text-zinc-400 font-mono">· {log.camera}</span>
155
  </div>
156
+ <p className="text-[9.5px] text-slate-400 truncate mt-0.5">
157
  {log.employee ? `${log.employee.designation} (${log.employee.employee_id})` : "Unauthorized access attempt"}
158
  </p>
159
  </div>
160
 
161
  <div className="shrink-0 text-right space-y-1">
162
+ <p className="text-[9.5px] text-slate-400 font-mono leading-none">
163
  {parseDateTime(log.timestamp)?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) || ""}
164
  </p>
165
+ <span className={`inline-block text-[8.5px] font-mono font-bold px-1.5 py-0.5 rounded border ${statusColor}`}>
166
  {statusLabel}
167
  </span>
168
  </div>
 
171
  }
172
 
173
  const getDeptTheme = (code: string) => {
174
+ return {
175
+ bg: "bg-white border-zinc-100 hover:border-zinc-200 text-slate-900",
176
+ iconBg: "bg-zinc-50 text-zinc-600 border-zinc-200/50",
177
+ barBg: "bg-zinc-150",
178
+ barFill: "bg-zinc-950"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  };
180
  };
181
 
 
184
  const [currentDate, setCurrentDate] = React.useState("");
185
  const [statusFilter, setStatusFilter] = React.useState<"ALL" | "MATCHED" | "UNKNOWN" | "SPOOF">("ALL");
186
  const [selectedDept, setSelectedDept] = React.useState<any | null>(null);
187
+ const [isDark, setIsDark] = React.useState(false);
188
  const queryClient = useQueryClient();
189
 
190
+ React.useEffect(() => {
191
+ setIsDark(document.documentElement.classList.contains("dark"));
192
+ const observer = new MutationObserver(() => {
193
+ setIsDark(document.documentElement.classList.contains("dark"));
194
+ });
195
+ observer.observe(document.documentElement, {
196
+ attributes: true,
197
+ attributeFilter: ["class"]
198
+ });
199
+ return () => observer.disconnect();
200
+ }, []);
201
+
202
  const { data: deptAttendance, isLoading: loadingDeptAttendance } = useQuery({
203
  queryKey: ["dept-attendance", selectedDept?.id],
204
  queryFn: () => {
 
310
 
311
  const trendOption = () => {
312
  if (!trends) return {};
313
+ const labelColor = isDark ? "#a1a1aa" : "#71717a";
314
+ const gridColor = isDark ? "rgba(255,255,255,0.06)" : "rgba(24,24,27,0.06)";
315
+ const lineColor = isDark ? "#ffffff" : "#18181b";
316
+ const areaColor = isDark ? "rgba(255,255,255,0.08)" : "rgba(24,24,27,0.06)";
317
+ const barColor = isDark ? "#52525b" : "#71717a";
318
+ const tooltipBg = isDark ? "#18181b" : "#ffffff";
319
+ const tooltipColor = isDark ? "#f4f4f5" : "#18181b";
320
+ const tooltipBorder = isDark ? "#27272a" : "rgba(24,24,27,0.08)";
321
+
322
  return {
323
  backgroundColor: "transparent",
324
  tooltip: {
325
  trigger: "axis",
326
+ backgroundColor: tooltipBg,
327
+ borderColor: tooltipBorder,
328
  borderWidth: 1,
329
  shadowColor: "rgba(0,0,0,0.02)",
330
  shadowBlur: 10,
331
+ textStyle: { color: tooltipColor, fontSize: 11, fontFamily: "var(--font-inter)" },
332
  extraCssText: "border-radius:12px;padding:8px 12px;box-shadow: 0 4px 16px rgba(0,0,0,0.04);"
333
  },
334
  legend: {
335
  data: ["Present", "Late Arrivals"],
336
+ textStyle: { color: labelColor, fontSize: 10, fontFamily: "var(--font-inter)" },
337
  bottom: 0,
338
  icon: "circle",
339
  itemWidth: 8,
 
353
  }
354
  return d.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
355
  }),
356
+ axisLine: { lineStyle: { color: gridColor } },
357
  axisTick: { show: false },
358
+ axisLabel: { color: labelColor, fontSize: 10, fontFamily: "var(--font-inter)" }
359
  },
360
  yAxis: {
361
  type: "value",
362
+ splitLine: { lineStyle: { color: gridColor, type: "dashed" } },
363
+ axisLabel: { color: labelColor, fontSize: 10, fontFamily: "var(--font-inter)" },
364
  axisLine: { show: false }
365
  },
366
  series: [
 
372
  symbolSize: 6,
373
  data: trends.map((t: any) => t.present),
374
  lineStyle: {
375
+ color: lineColor,
376
  width: 2.25,
377
+ shadowColor: isDark ? "rgba(255,255,255,0.05)" : "rgba(24,24,27,0.1)",
378
  shadowBlur: 8,
379
  shadowOffsetY: 4
380
  },
381
+ itemStyle: { color: lineColor },
382
  areaStyle: {
383
  color: {
384
  type: "linear", x: 0, y: 0, x2: 0, y2: 1,
385
  colorStops: [
386
+ { offset: 0, color: areaColor },
387
  { offset: 1, color: "rgba(24,24,27,0)" }
388
  ]
389
  }
 
394
  type: "bar",
395
  data: trends.map((t: any) => t.late),
396
  itemStyle: {
397
+ color: barColor,
398
  borderRadius: [3, 3, 0, 0]
399
  },
400
  barWidth: 6,
 
406
 
407
  const deptOption = () => {
408
  if (!deptStats) return {};
409
+ const labelColor = isDark ? "#a1a1aa" : "#71717a";
410
+ const textPrimary = isDark ? "#f4f4f5" : "#18181b";
411
+ const gridColor = isDark ? "rgba(255,255,255,0.06)" : "rgba(24,24,27,0.04)";
412
+ const totalBarColor = isDark ? "rgba(255,255,255,0.06)" : "rgba(24,24,27,0.04)";
413
+ const fillStart = isDark ? "#ffffff" : "#18181b";
414
+ const fillEnd = isDark ? "#d4d4d8" : "#27272a";
415
+ const tooltipBg = isDark ? "#18181b" : "#ffffff";
416
+ const tooltipColor = isDark ? "#f4f4f5" : "#18181b";
417
+ const tooltipBorder = isDark ? "#27272a" : "rgba(24,24,27,0.08)";
418
+
419
  return {
420
  backgroundColor: "transparent",
421
  tooltip: {
422
  trigger: "axis",
423
  axisPointer: { type: "shadow" },
424
+ backgroundColor: tooltipBg,
425
+ borderColor: tooltipBorder,
426
  borderWidth: 1,
427
+ textStyle: { color: tooltipColor, fontSize: 11 },
428
  extraCssText: "border-radius:12px;padding:8px 12px;box-shadow: 0 4px 16px rgba(0,0,0,0.04);"
429
  },
430
  grid: { top: 10, left: 90, right: 20, bottom: 20 },
431
  xAxis: {
432
  type: "value",
433
+ splitLine: { lineStyle: { color: gridColor, type: "dashed" } },
434
+ axisLabel: { color: labelColor, fontSize: 9 },
435
  axisLine: { show: false }
436
  },
437
  yAxis: {
 
439
  data: deptStats.map((d: any) => d.name || d.code),
440
  axisLine: { show: false },
441
  axisTick: { show: false },
442
+ axisLabel: { color: textPrimary, fontSize: 10, fontFamily: "var(--font-inter)" }
443
  },
444
  series: [
445
  {
446
  name: "Total Employees",
447
  type: "bar",
448
  data: deptStats.map((d: any) => d.total_employees),
449
+ itemStyle: { color: totalBarColor, borderRadius: [0, 4, 4, 0] },
450
  barGap: "-100%",
451
  barWidth: 10
452
  },
 
460
  color: {
461
  type: "linear", x: 0, y: 0, x2: 1, y2: 0,
462
  colorStops: [
463
+ { offset: 0, color: fillStart },
464
+ { offset: 1, color: fillEnd }
465
  ]
466
  }
467
  }
 
469
  ]
470
  };
471
  };
 
472
  return (
473
  <SidebarLayout>
474
+ <div className="space-y-6 page-enter">
475
  {/* ─── Header ─── */}
476
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between pb-4 border-b border-zinc-100 gap-4">
477
+ <div className="flex items-center gap-3">
478
+ <h1 className="text-lg font-bold text-[var(--text-primary)] tracking-tight">
479
+ Dashboard
480
  </h1>
481
+ <div className="flex items-center gap-1.5 text-[9px] font-bold tracking-wider text-emerald-600 bg-emerald-500/5 px-2 py-0.5 rounded border border-emerald-500/10 uppercase font-mono">
482
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
483
+ <span>Live Feed Active</span>
484
+ </div>
485
  </div>
486
+
487
+ <div className="flex items-center gap-2 font-mono text-[11px] text-slate-400">
488
+ <span>{currentDate}</span>
489
  {currentTime && (
490
+ <>
491
+ <span className="text-zinc-200">|</span>
492
+ <span className="tabular-nums text-slate-800 font-medium">{currentTime}</span>
493
+ </>
 
 
 
494
  )}
 
 
 
 
 
495
  </div>
496
  </div>
497
 
 
507
  {/* ─── Charts Row ─── */}
508
  <div className="grid grid-cols-1 lg:grid-cols-5 gap-5">
509
  {/* Trend Chart */}
510
+ <div className="lg:col-span-3 bg-white border border-zinc-100 rounded-xl p-5 flex flex-col justify-between">
511
+ <div className="flex items-center justify-between mb-4">
512
+ <h2 className="text-[11px] font-bold text-slate-450 uppercase tracking-wider">Attendance Trends</h2>
 
 
 
 
 
 
 
 
513
  </div>
514
  <div className="h-56">
515
  {loadingTrends ? (
 
523
  </div>
524
 
525
  {/* Live Feed */}
526
+ <div className="lg:col-span-2 bg-white border border-zinc-100 rounded-xl flex flex-col">
527
+ <div className="p-5 pb-3 border-b border-zinc-100">
528
  <div className="flex items-center justify-between">
529
+ <h2 className="text-[11px] font-bold text-slate-450 uppercase tracking-wider">Live Activity</h2>
530
+ <div className="flex items-center gap-1.5 bg-zinc-50 border border-zinc-100 px-2 py-0.5 rounded text-[9px] text-zinc-500 font-mono tracking-wider">
531
+ <span className="w-1 h-1 rounded-full bg-emerald-500 animate-pulse" />
532
+ <span>STREAMING</span>
 
 
 
 
 
 
 
 
533
  </div>
534
  </div>
535
 
536
  {/* Status Filter pills */}
537
+ <div className="flex flex-wrap gap-1 mt-2.5">
538
  {(["ALL", "MATCHED", "UNKNOWN", "SPOOF"] as const).map((filter) => (
539
  <button
540
  key={filter}
541
  onClick={() => setStatusFilter(filter)}
542
+ className={`px-2 py-0.5 rounded text-[9px] font-bold tracking-wide transition-all border cursor-pointer uppercase font-mono ${
543
  statusFilter === filter
544
+ ? "bg-zinc-950 border-zinc-950 text-white"
545
+ : "bg-transparent border-zinc-100 text-slate-400 hover:text-slate-700 hover:border-zinc-200"
546
  }`}
547
  >
548
+ {filter === "SPOOF" ? "Liveness Failed" : filter.toLowerCase()}
549
  </button>
550
  ))}
551
  </div>
 
587
  })()}
588
  </div>
589
 
590
+ <div className="p-3 border-t border-zinc-100 bg-zinc-55/30">
591
  <Link
592
  href="/attendance"
593
+ className="flex items-center justify-between text-[11px] text-zinc-500 hover:text-zinc-900 font-semibold transition-colors group"
594
  >
595
  <span>Access Complete Ledger Logs</span>
596
  <ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-0.5 transition-transform" />
 
600
  </div>
601
 
602
  {/* ─── Bottom Section: Department Breakdown ─── */}
603
+ <div className="bg-white border border-zinc-100 rounded-xl p-5">
604
+ <div className="flex items-center justify-between mb-4">
605
+ <h2 className="text-[11px] font-bold text-slate-450 uppercase tracking-wider">Departments</h2>
 
 
 
 
 
 
 
 
606
  </div>
607
 
608
  <div className="min-h-36">
 
619
  ) : (
620
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
621
  {deptStats.map((d: any) => {
 
622
  const percent = d.total_employees > 0
623
  ? Math.round((d.present_today / d.total_employees) * 100)
624
  : 0;
 
627
  <div
628
  key={d.code}
629
  onClick={() => setSelectedDept(d)}
630
+ className="p-4 rounded-xl border border-zinc-100 bg-white hover:border-zinc-300 transition-all duration-200 flex flex-col justify-between h-28 cursor-pointer group"
631
  >
632
  {/* Top Header Row */}
633
  <div className="flex items-center justify-between w-full">
634
+ <span className="text-[9px] font-bold font-mono px-1.5 py-0.5 rounded bg-zinc-50 text-zinc-650 border border-zinc-100 uppercase tracking-wider">
635
  {d.code}
636
  </span>
637
 
638
  <div className="text-right">
639
+ <span className="text-xs font-semibold text-slate-800">
640
+ {d.present_today} <span className="text-slate-450 font-normal">/ {d.total_employees}</span>
641
  </span>
642
  </div>
643
  </div>
644
 
645
  {/* Department Name & Subheading */}
646
+ <div className="mt-1.5 flex-1 min-w-0">
647
  <h3 className="text-xs font-bold text-slate-800 truncate" title={d.department}>
648
  {d.department}
649
  </h3>
650
+ <p className="text-[9.5px] text-slate-450 font-mono mt-0.5">
651
+ {percent}% present
652
  </p>
653
  </div>
654
 
655
  {/* Progress Bar */}
656
+ <div className="mt-2 w-full">
657
+ <div className="w-full h-1 bg-zinc-100 rounded-full overflow-hidden">
658
  <div
659
+ className="h-full bg-zinc-950 rounded-full transition-all duration-500 group-hover:bg-zinc-800"
660
  style={{ width: `${d.total_employees > 0 ? percent : 0}%` }}
661
  />
662
  </div>
 
690
  onClick={() => setSelectedDept(null)}
691
  className="p-2 rounded-xl hover:bg-zinc-100 text-zinc-400 hover:text-zinc-650 transition-all cursor-pointer"
692
  >
693
+ <X className="w-4 h-4" />
694
  </button>
695
  </div>
696
 
 
716
  return (
717
  <div key={rec.id} className="flex items-center justify-between py-3">
718
  <div className="flex items-center gap-3">
719
+ <div className={`w-8 h-8 rounded-lg bg-gradient-to-br ${avatarColor} flex items-center justify-center shrink-0 border font-bold text-[10.5px] shadow-sm`}>
720
  {rec.employee.name.charAt(0).toUpperCase()}
721
  </div>
722
  <div>
frontend/app/employees/page.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
  X, Users, CheckCircle2, XCircle, ChevronDown, UserCheck, ShieldAlert,
10
  Download, Mail, Phone, Calendar, Briefcase, Clock, TrendingUp
11
  } from "lucide-react";
 
12
  import Link from "next/link";
13
 
14
  // Avatar gradient styles
@@ -64,6 +65,7 @@ function InputField({ label, required, children }: { label: string; required?: b
64
 
65
  export default function EmployeesPage() {
66
  const queryClient = useQueryClient();
 
67
  const [search, setSearch] = useState("");
68
  const [deptFilter, setDeptFilter] = useState("");
69
  const [statusFilter, setStatusFilter] = useState("");
@@ -74,11 +76,15 @@ export default function EmployeesPage() {
74
  const [importErrors, setImportErrors] = useState<string[]>([]);
75
  const [submitting, setSubmitting] = useState(false);
76
  const [selectedEmployee, setSelectedEmployee] = useState<any | null>(null);
 
 
77
 
78
  const [empId, setEmpId] = useState("");
79
  const [name, setName] = useState("");
80
  const [email, setEmail] = useState("");
81
  const [phone, setPhone] = useState("");
 
 
82
  const [designation, setDesignation] = useState("");
83
  const [joiningDate, setJoiningDate] = useState(getLocalDateString());
84
  const [statusVal, setStatusVal] = useState("Active");
@@ -120,7 +126,9 @@ export default function EmployeesPage() {
120
  resetForm();
121
  setShowAddDialog(false);
122
  },
123
- onError: (err: any) => alert(err.message || "Failed to create employee.")
 
 
124
  });
125
 
126
  const deleteMutation = useMutation({
@@ -128,18 +136,62 @@ export default function EmployeesPage() {
128
  onSuccess: () => {
129
  queryClient.invalidateQueries({ queryKey: ["employees"] });
130
  queryClient.invalidateQueries({ queryKey: ["dashboard-summary"] });
 
131
  },
132
- onError: (err: any) => alert(err.message || "Failed to delete employee.")
133
  });
134
 
135
  const resetForm = () => {
136
  setEmpId(""); setName(""); setEmail(""); setPhone(""); setDesignation("");
137
  setJoiningDate(getLocalDateString()); setStatusVal("Active");
138
  setDeptId(""); setCreateUserLogin(false); setPassword("");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  };
140
 
 
 
141
  const handleAddSubmit = (e: React.FormEvent) => {
142
  e.preventDefault();
 
 
 
 
 
 
 
 
 
 
143
  const payload: any = {
144
  employee_id: empId, name, email, phone: phone || null,
145
  designation: designation || null, joining_date: joiningDate,
@@ -151,8 +203,16 @@ export default function EmployeesPage() {
151
  };
152
 
153
  const handleDelete = (id: number, name: string) => {
154
- if (confirm(`Are you absolutely sure you want to delete ${name}? All facial biometric profile records and logs will be permanently deleted.`))
155
- deleteMutation.mutate(id);
 
 
 
 
 
 
 
 
156
  };
157
 
158
  const handleImportSubmit = async (e: React.FormEvent) => {
@@ -185,8 +245,9 @@ export default function EmployeesPage() {
185
  a.click();
186
  a.remove();
187
  window.URL.revokeObjectURL(url);
 
188
  } catch (err: any) {
189
- alert(err.message || "Failed to export CSV.");
190
  }
191
  };
192
 
@@ -203,7 +264,6 @@ export default function EmployeesPage() {
203
  <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-5 border-b border-white/5">
204
  <div>
205
  <h1 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">Staff Management</h1>
206
- <p className="text-xs text-slate-500 mt-0.5">Register staff, manage details, and initiate camera enrollment.</p>
207
  </div>
208
  <div className="flex items-center gap-2.5 shrink-0">
209
  <button
@@ -367,10 +427,9 @@ export default function EmployeesPage() {
367
  <div className="flex items-center justify-between mb-6 pb-4 border-b border-white/5">
368
  <div>
369
  <h3 className="text-sm font-bold text-[var(--text-primary)] uppercase tracking-wider">Register Employee</h3>
370
- <p className="text-xs text-slate-500 mt-0.5">Fill in the fields to create a new profile.</p>
371
  </div>
372
  <button
373
- onClick={() => setShowAddDialog(false)}
374
  className="p-2 rounded-xl hover:bg-white/6 text-slate-500 hover:text-slate-300 transition-all cursor-pointer"
375
  >
376
  <X className="w-4 h-4" />
@@ -378,6 +437,26 @@ export default function EmployeesPage() {
378
  </div>
379
 
380
  <form onSubmit={handleAddSubmit} className="space-y-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  <div className="grid grid-cols-2 gap-3.5">
382
  <InputField label="Employee ID" required>
383
  <input type="text" required placeholder="EMP001" value={empId}
@@ -395,8 +474,21 @@ export default function EmployeesPage() {
395
  onChange={(e) => setEmail(e.target.value)} className={inputCls} />
396
  </InputField>
397
  <InputField label="Phone Number">
398
- <input type="text" placeholder="+1 555-0199" value={phone}
399
- onChange={(e) => setPhone(e.target.value)} className={inputCls} />
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  </InputField>
401
  </div>
402
 
@@ -454,15 +546,15 @@ export default function EmployeesPage() {
454
  <div className="flex justify-end gap-2.5 pt-3 border-t border-white/5">
455
  <button
456
  type="button"
457
- onClick={() => setShowAddDialog(false)}
458
  className="btn-ghost h-9.5 px-4 text-[12px] rounded-xl cursor-pointer hover:bg-white/[0.04]"
459
  >
460
  Cancel
461
  </button>
462
  <button
463
  type="submit"
464
- disabled={createMutation.isPending}
465
- className="btn-primary h-9.5 px-5 text-[12px] flex items-center gap-2 rounded-xl cursor-pointer"
466
  >
467
  {createMutation.isPending ? (
468
  <><div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" /><span>Saving...</span></>
@@ -483,7 +575,6 @@ export default function EmployeesPage() {
483
  <div className="flex items-center justify-between mb-6 pb-4 border-b border-white/5">
484
  <div>
485
  <h3 className="text-sm font-bold text-[var(--text-primary)] uppercase tracking-wider">Bulk Import via CSV</h3>
486
- <p className="text-xs text-slate-500 mt-0.5">Register multiple staff via CSV upload.</p>
487
  </div>
488
  <button
489
  onClick={() => { setShowImportDialog(false); setSelectedFile(null); setImportMessage(null); setImportErrors([]); }}
@@ -705,6 +796,49 @@ export default function EmployeesPage() {
705
  </div>
706
  </div>
707
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
  </>
709
  );
710
  }
 
9
  X, Users, CheckCircle2, XCircle, ChevronDown, UserCheck, ShieldAlert,
10
  Download, Mail, Phone, Calendar, Briefcase, Clock, TrendingUp
11
  } from "lucide-react";
12
+ import { useToast } from "@/app/utils/toast";
13
  import Link from "next/link";
14
 
15
  // Avatar gradient styles
 
65
 
66
  export default function EmployeesPage() {
67
  const queryClient = useQueryClient();
68
+ const { toast } = useToast();
69
  const [search, setSearch] = useState("");
70
  const [deptFilter, setDeptFilter] = useState("");
71
  const [statusFilter, setStatusFilter] = useState("");
 
76
  const [importErrors, setImportErrors] = useState<string[]>([]);
77
  const [submitting, setSubmitting] = useState(false);
78
  const [selectedEmployee, setSelectedEmployee] = useState<any | null>(null);
79
+ const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null);
80
+ const [deleteConfirmName, setDeleteConfirmName] = useState<string>("");
81
 
82
  const [empId, setEmpId] = useState("");
83
  const [name, setName] = useState("");
84
  const [email, setEmail] = useState("");
85
  const [phone, setPhone] = useState("");
86
+ const [phoneError, setPhoneError] = useState<string | null>(null);
87
+ const [submissionError, setSubmissionError] = useState<string | null>(null);
88
  const [designation, setDesignation] = useState("");
89
  const [joiningDate, setJoiningDate] = useState(getLocalDateString());
90
  const [statusVal, setStatusVal] = useState("Active");
 
126
  resetForm();
127
  setShowAddDialog(false);
128
  },
129
+ onError: (err: any) => {
130
+ setSubmissionError(err.message || "Failed to create employee.");
131
+ }
132
  });
133
 
134
  const deleteMutation = useMutation({
 
136
  onSuccess: () => {
137
  queryClient.invalidateQueries({ queryKey: ["employees"] });
138
  queryClient.invalidateQueries({ queryKey: ["dashboard-summary"] });
139
+ toast.success("Employee record deleted successfully.");
140
  },
141
+ onError: (err: any) => toast.error(err.message || "Failed to delete employee.")
142
  });
143
 
144
  const resetForm = () => {
145
  setEmpId(""); setName(""); setEmail(""); setPhone(""); setDesignation("");
146
  setJoiningDate(getLocalDateString()); setStatusVal("Active");
147
  setDeptId(""); setCreateUserLogin(false); setPassword("");
148
+ setPhoneError(null);
149
+ setSubmissionError(null);
150
+ };
151
+
152
+ // Live duplicate checking against loaded employees
153
+ const getDuplicateWarning = () => {
154
+ if (!employees) return null;
155
+
156
+ if (empId.trim()) {
157
+ const match = employees.find((e: any) => e.employee_id.toLowerCase() === empId.trim().toLowerCase());
158
+ if (match) return `Employee ID "${empId}" is already assigned to ${match.name}.`;
159
+ }
160
+
161
+ if (name.trim()) {
162
+ const match = employees.find((e: any) => e.name.toLowerCase() === name.trim().toLowerCase());
163
+ if (match) return `An employee named "${name}" is already registered.`;
164
+ }
165
+
166
+ if (phone.trim()) {
167
+ const cleanedInput = phone.replace(/[\s\-()]/g, "");
168
+ if (cleanedInput) {
169
+ const match = employees.find((e: any) => {
170
+ if (!e.phone) return false;
171
+ const cleanedExisting = e.phone.replace(/[\s\-()]/g, "");
172
+ return cleanedExisting === cleanedInput;
173
+ });
174
+ if (match) return `Phone number is already registered to ${match.name}.`;
175
+ }
176
+ }
177
+
178
+ return null;
179
  };
180
 
181
+ const duplicateWarning = getDuplicateWarning();
182
+
183
  const handleAddSubmit = (e: React.FormEvent) => {
184
  e.preventDefault();
185
+
186
+ if (phone) {
187
+ const cleaned = phone.replace(/[\s\-()]/g, "");
188
+ if (!/^(?:\+91|91|0)?[6-9]\d{9}$/.test(cleaned)) {
189
+ setPhoneError("Invalid Indian mobile number. Must be a 10-digit number starting with 6-9, optionally prefixed with +91, 91, or 0.");
190
+ return;
191
+ }
192
+ }
193
+ setPhoneError(null);
194
+
195
  const payload: any = {
196
  employee_id: empId, name, email, phone: phone || null,
197
  designation: designation || null, joining_date: joiningDate,
 
203
  };
204
 
205
  const handleDelete = (id: number, name: string) => {
206
+ setDeleteConfirmId(id);
207
+ setDeleteConfirmName(name);
208
+ };
209
+
210
+ const confirmDelete = () => {
211
+ if (deleteConfirmId !== null) {
212
+ deleteMutation.mutate(deleteConfirmId);
213
+ setDeleteConfirmId(null);
214
+ setDeleteConfirmName("");
215
+ }
216
  };
217
 
218
  const handleImportSubmit = async (e: React.FormEvent) => {
 
245
  a.click();
246
  a.remove();
247
  window.URL.revokeObjectURL(url);
248
+ toast.success("Employee ledger exported successfully.");
249
  } catch (err: any) {
250
+ toast.error(err.message || "Failed to export CSV.");
251
  }
252
  };
253
 
 
264
  <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-5 border-b border-white/5">
265
  <div>
266
  <h1 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">Staff Management</h1>
 
267
  </div>
268
  <div className="flex items-center gap-2.5 shrink-0">
269
  <button
 
427
  <div className="flex items-center justify-between mb-6 pb-4 border-b border-white/5">
428
  <div>
429
  <h3 className="text-sm font-bold text-[var(--text-primary)] uppercase tracking-wider">Register Employee</h3>
 
430
  </div>
431
  <button
432
+ onClick={() => { setShowAddDialog(false); resetForm(); }}
433
  className="p-2 rounded-xl hover:bg-white/6 text-slate-500 hover:text-slate-300 transition-all cursor-pointer"
434
  >
435
  <X className="w-4 h-4" />
 
437
  </div>
438
 
439
  <form onSubmit={handleAddSubmit} className="space-y-4">
440
+ {duplicateWarning && (
441
+ <div className="flex items-start gap-3 p-3 rounded-xl bg-amber-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 text-xs animate-fadeInUp">
442
+ <ShieldAlert className="w-4 h-4 shrink-0 mt-0.5 text-amber-500" />
443
+ <div className="flex-1 space-y-1">
444
+ <p className="font-bold uppercase tracking-wider text-[9px] font-mono leading-none">Duplicate Detected</p>
445
+ <p className="leading-relaxed font-medium mt-1">{duplicateWarning}</p>
446
+ </div>
447
+ </div>
448
+ )}
449
+
450
+ {submissionError && (
451
+ <div className="flex items-start gap-3 p-3 rounded-xl bg-rose-500/10 border border-rose-500/25 text-rose-600 dark:text-rose-400 text-xs animate-fadeInUp">
452
+ <XCircle className="w-4 h-4 shrink-0 mt-0.5 text-rose-500" />
453
+ <div className="flex-1 space-y-1">
454
+ <p className="font-bold uppercase tracking-wider text-[9px] font-mono leading-none">Registration Failed</p>
455
+ <p className="leading-relaxed font-medium mt-1">{submissionError}</p>
456
+ </div>
457
+ </div>
458
+ )}
459
+
460
  <div className="grid grid-cols-2 gap-3.5">
461
  <InputField label="Employee ID" required>
462
  <input type="text" required placeholder="EMP001" value={empId}
 
474
  onChange={(e) => setEmail(e.target.value)} className={inputCls} />
475
  </InputField>
476
  <InputField label="Phone Number">
477
+ <div className="relative">
478
+ <input
479
+ type="text"
480
+ placeholder="+91 98765 43210"
481
+ value={phone}
482
+ onChange={(e) => {
483
+ setPhone(e.target.value);
484
+ if (phoneError) setPhoneError(null);
485
+ }}
486
+ className={`${inputCls} ${phoneError ? "border-rose-500 focus:border-rose-500" : ""}`}
487
+ />
488
+ {phoneError && (
489
+ <p className="text-[10px] text-rose-500 mt-1 font-medium leading-tight">{phoneError}</p>
490
+ )}
491
+ </div>
492
  </InputField>
493
  </div>
494
 
 
546
  <div className="flex justify-end gap-2.5 pt-3 border-t border-white/5">
547
  <button
548
  type="button"
549
+ onClick={() => { setShowAddDialog(false); resetForm(); }}
550
  className="btn-ghost h-9.5 px-4 text-[12px] rounded-xl cursor-pointer hover:bg-white/[0.04]"
551
  >
552
  Cancel
553
  </button>
554
  <button
555
  type="submit"
556
+ disabled={createMutation.isPending || !!duplicateWarning}
557
+ className="btn-primary h-9.5 px-5 text-[12px] flex items-center gap-2 rounded-xl cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
558
  >
559
  {createMutation.isPending ? (
560
  <><div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" /><span>Saving...</span></>
 
575
  <div className="flex items-center justify-between mb-6 pb-4 border-b border-white/5">
576
  <div>
577
  <h3 className="text-sm font-bold text-[var(--text-primary)] uppercase tracking-wider">Bulk Import via CSV</h3>
 
578
  </div>
579
  <button
580
  onClick={() => { setShowImportDialog(false); setSelectedFile(null); setImportMessage(null); setImportErrors([]); }}
 
796
  </div>
797
  </div>
798
  )}
799
+
800
+ {/* Delete Confirmation Modal */}
801
+ {deleteConfirmId !== null && (
802
+ <div className="modal-backdrop z-50">
803
+ <div className="modal-content max-w-sm border border-red-500/10 shadow-[0_12px_40px_rgba(239,68,68,0.12)]">
804
+ <div className="flex flex-col items-center text-center p-2 space-y-4">
805
+ <div className="w-12 h-12 rounded-2xl bg-rose-500/10 border border-rose-500/15 flex items-center justify-center text-rose-500 shadow-inner">
806
+ <Trash2 className="w-6 h-6" />
807
+ </div>
808
+ <div>
809
+ <h3 className="text-sm font-bold text-[var(--text-primary)] uppercase tracking-wider">Confirm Deletion</h3>
810
+ <p className="text-xs text-slate-500 mt-2 leading-relaxed">
811
+ Are you absolutely sure you want to delete <span className="font-semibold text-[var(--text-primary)]">{deleteConfirmName}</span>?
812
+ </p>
813
+ <p className="text-[10.5px] text-rose-500 font-medium bg-rose-500/5 border border-rose-500/10 rounded-xl p-2.5 mt-3 leading-normal">
814
+ Warning: All facial biometric profile records and logs will be permanently deleted. This action cannot be undone.
815
+ </p>
816
+ </div>
817
+ <div className="flex gap-2.5 w-full pt-2 border-t border-white/5">
818
+ <button
819
+ type="button"
820
+ onClick={() => { setDeleteConfirmId(null); setDeleteConfirmName(""); }}
821
+ className="flex-1 btn-ghost h-9.5 text-[12px] rounded-xl cursor-pointer hover:bg-white/[0.04]"
822
+ >
823
+ Cancel
824
+ </button>
825
+ <button
826
+ type="button"
827
+ onClick={confirmDelete}
828
+ disabled={deleteMutation.isPending}
829
+ className="flex-1 bg-rose-600 hover:bg-rose-500 active:bg-rose-700 text-white font-bold text-[12px] rounded-xl cursor-pointer h-9.5 flex items-center justify-center gap-2 shadow-md shadow-rose-950/20 border border-rose-500/20"
830
+ >
831
+ {deleteMutation.isPending ? (
832
+ <div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
833
+ ) : (
834
+ "Delete Record"
835
+ )}
836
+ </button>
837
+ </div>
838
+ </div>
839
+ </div>
840
+ </div>
841
+ )}
842
  </>
843
  );
844
  }
frontend/app/enroll/[id]/page.tsx CHANGED
@@ -7,14 +7,15 @@ import SidebarLayout from "@/components/SidebarLayout";
7
  import { fetchApi, getBackendUrl } from "@/app/utils/api";
8
  import {
9
  Camera, Upload, CheckCircle2, ChevronLeft, XCircle, Video,
10
- RefreshCw, AlertCircle, Trash2, Play, Pause, Save, RotateCcw, Shield, Activity, Sparkles
 
11
  } from "lucide-react";
12
 
13
  interface PoseInfo {
14
  label: string;
15
  hint: string;
16
  speech: string;
17
- icon: string;
18
  }
19
 
20
  const POSES: Record<string, PoseInfo> = {
@@ -22,61 +23,61 @@ const POSES: Record<string, PoseInfo> = {
22
  label: "Front Profile",
23
  hint: "Look straight into the camera with a neutral expression.",
24
  speech: "Please look straight into the camera.",
25
- icon: "🧑"
26
  },
27
  left: {
28
  label: "Left Profile",
29
  hint: "Turn your head slowly to the left.",
30
  speech: "Please turn your head to the left.",
31
- icon: "👈"
32
  },
33
  right: {
34
  label: "Right Profile",
35
  hint: "Turn your head slowly to the right.",
36
  speech: "Please turn your head to the right.",
37
- icon: "👉"
38
  },
39
  up: {
40
  label: "Looking Up",
41
  hint: "Tilt your chin upwards slightly.",
42
  speech: "Please tilt your head upwards.",
43
- icon: "⬆️"
44
  },
45
  down: {
46
  label: "Looking Down",
47
  hint: "Tilt your chin downwards slightly.",
48
  speech: "Please tilt your head downwards.",
49
- icon: "⬇️"
50
  },
51
  smile: {
52
  label: "Smiling Face",
53
  hint: "Give a natural, relaxed smile.",
54
  speech: "Now, smile naturally.",
55
- icon: "😊"
56
  },
57
  neutral: {
58
  label: "Neutral Face",
59
  hint: "Keep a relaxed, standard neutral expression.",
60
  speech: "Relax your face, show a neutral expression.",
61
- icon: "😐"
62
  },
63
  indoor: {
64
  label: "Indoor Light",
65
  hint: "Look straight with standard indoor room lighting.",
66
  speech: "Look straight for typical indoor lighting.",
67
- icon: "💡"
68
  },
69
  outdoor: {
70
  label: "Outdoor Light",
71
  hint: "Look straight with bright/outdoor lighting.",
72
  speech: "Look straight for bright light capture.",
73
- icon: "☀️"
74
  },
75
  glasses: {
76
  label: "Glasses Option",
77
  hint: "Put on glasses if you wear them, otherwise look straight.",
78
  speech: "If you wear glasses, put them on. Otherwise, look straight.",
79
- icon: "🕶️"
80
  }
81
  };
82
 
@@ -110,6 +111,7 @@ export default function EnrollPage() {
110
  const [successMsg, setSuccessMsg] = useState<string | null>(null);
111
  const [webcamActive, setWebcamActive] = useState(false);
112
  const [singleRetakePose, setSingleRetakePose] = useState<string | null>(null);
 
113
 
114
  const videoRef = useRef<HTMLVideoElement>(null);
115
  const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -249,15 +251,16 @@ export default function EnrollPage() {
249
  }
250
 
251
  countdownIntervalRef.current = setInterval(() => {
252
- setCountdown((prev) => {
253
- if (prev <= 1) {
254
- clearInterval(countdownIntervalRef.current!);
255
- captureFrame(currentKey);
256
- return 3;
257
  }
 
 
258
  playSound("beep");
259
- return prev - 1;
260
- });
261
  }, 1000);
262
 
263
  return () => {
@@ -304,6 +307,7 @@ export default function EnrollPage() {
304
  setSingleRetakePose(poseKey);
305
  setSelectedPose(poseKey);
306
  setCountdown(3);
 
307
  setCaptureState("capturing");
308
 
309
  // Set index to match keys
@@ -428,6 +432,27 @@ export default function EnrollPage() {
428
  border-width: 2px;
429
  pointer-events: none;
430
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  `}</style>
432
 
433
  <canvas ref={canvasRef} className="hidden" />
@@ -459,7 +484,7 @@ export default function EnrollPage() {
459
 
460
  {enrolledCount > 0 && (
461
  <button
462
- onClick={() => { if (confirm("Completely wipe all registered biometric vectors and images? This cannot be undone.")) clearMutation.mutate(); }}
463
  className="flex items-center gap-2 text-[11.5px] font-bold text-rose-600 hover:text-white bg-white hover:bg-rose-600 border border-rose-200 hover:border-rose-600 px-4 py-2 rounded-xl transition-all cursor-pointer shadow-sm"
464
  >
465
  <Trash2 className="w-3.5 h-3.5" />
@@ -551,24 +576,33 @@ export default function EnrollPage() {
551
  To capture accurate biometric details under varying orientations and lighting, we index 10 distinct facial angles.
552
  </p>
553
 
554
- <div className="grid grid-cols-2 sm:grid-cols-5 gap-3 pt-2">
555
  {POSE_KEYS.map((key) => {
556
  const done = status?.enrolled_poses?.some((p: string) => p.toLowerCase() === key.toLowerCase());
 
557
  return (
558
  <div
559
  key={key}
560
- className={`p-3 rounded-xl border flex flex-col items-center text-center justify-center transition-all ${
561
  done
562
- ? "bg-emerald-50/50 border-emerald-200 text-emerald-800"
563
- : "bg-slate-50/50 border-slate-200 text-slate-400"
564
  }`}
565
  >
566
- <span className="text-xl mb-1.5 filter drop-shadow-sm">{POSES[key].icon}</span>
567
- <span className="text-[10px] font-bold uppercase tracking-wider font-mono">{POSES[key].label.split(" ")[0]}</span>
 
 
 
 
 
 
 
 
568
  {done ? (
569
- <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500 mt-2 shrink-0" />
570
  ) : (
571
- <div className="w-3.5 h-3.5 rounded-full border border-slate-250 mt-2 shrink-0 bg-white" />
572
  )}
573
  </div>
574
  );
@@ -585,7 +619,14 @@ export default function EnrollPage() {
585
  <div className="w-full bg-slate-950 text-white rounded-2xl p-5 flex items-center justify-between shadow-lg relative overflow-hidden">
586
  <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)] pointer-events-none" />
587
  <div className="flex items-center gap-4 relative z-10">
588
- <span className="text-3xl filter drop-shadow-sm">{POSES[POSE_KEYS[currentPoseIndex]]?.icon}</span>
 
 
 
 
 
 
 
589
  <div>
590
  <p className="text-[10px] font-bold text-cyan-400 font-mono tracking-widest uppercase">
591
  SCAN PHASE {currentPoseIndex + 1} OF {POSE_KEYS.length}
@@ -611,17 +652,34 @@ export default function EnrollPage() {
611
  {/* Native video element */}
612
  <video
613
  ref={videoRef}
614
- className="w-full h-full object-cover scale-x-[-1]"
615
  autoPlay playsInline muted
616
  />
617
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
618
  {/* Cybernetic HUD elements */}
619
- <div className="scanner-line" />
620
- <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
621
- <div className="scanner-target">
622
- <div className="w-4 h-4 border border-cyan-400 rounded-full animate-ping" />
 
 
623
  </div>
624
- </div>
625
 
626
  {/* HUD corners */}
627
  <div className="hud-corner corner-bracket-tl top-6 left-6 border-t-2 border-l-2" />
@@ -631,20 +689,28 @@ export default function EnrollPage() {
631
 
632
  {/* Scanner stats HUD */}
633
  <div className="absolute top-6 left-12 right-12 flex justify-between text-[9px] font-mono font-bold text-cyan-400/80 pointer-events-none uppercase">
634
- <span>SYS.STATUS: ACQUIRING_DATA</span>
635
- <span>FPS: 60 · ISO: 200 · SHUTTER: AUTO</span>
636
  </div>
637
 
638
  <div className="absolute bottom-6 left-12 right-12 flex justify-between items-center text-[9px] font-mono font-bold text-cyan-400/80 pointer-events-none">
639
  <span>ANGLE: {POSE_KEYS[currentPoseIndex]?.toUpperCase()}</span>
640
- <span>LIVENESS CHECK: ACTIVE</span>
641
  </div>
642
  </div>
643
 
644
  {/* Controls */}
645
  <div className="flex gap-4 w-full max-w-lg">
646
  <button
647
- onClick={() => setIsPaused(!isPaused)}
 
 
 
 
 
 
 
 
648
  className="flex-1 h-11 bg-white hover:bg-slate-50 border border-slate-250 text-slate-800 font-bold text-xs uppercase tracking-wider rounded-xl flex items-center justify-center gap-2 transition-all shadow-sm cursor-pointer"
649
  >
650
  {isPaused ? <Play className="w-3.5 h-3.5 fill-current" /> : <Pause className="w-3.5 h-3.5 fill-current" />}
@@ -652,7 +718,7 @@ export default function EnrollPage() {
652
  </button>
653
 
654
  <button
655
- onClick={() => {
656
  if (singleRetakePose) {
657
  stopWebcam();
658
  setSingleRetakePose(null);
@@ -661,6 +727,10 @@ export default function EnrollPage() {
661
  // Skip pose
662
  setCurrentPoseIndex(prev => prev + 1);
663
  setCountdown(3);
 
 
 
 
664
  }
665
  }}
666
  className="flex-1 h-11 bg-slate-950 hover:bg-slate-900 border border-slate-950 text-white font-bold text-xs uppercase tracking-wider rounded-xl flex items-center justify-center gap-2 transition-all shadow-md cursor-pointer"
@@ -710,7 +780,7 @@ export default function EnrollPage() {
710
  </div>
711
 
712
  <div className="mt-2.5 flex items-center justify-between text-[11px] font-bold">
713
- <span className="text-slate-900 uppercase font-mono">{POSES[key].label.split(" ")[0]}</span>
714
  {imgUrl ? (
715
  <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" />
716
  ) : (
@@ -766,7 +836,7 @@ export default function EnrollPage() {
766
  <div className="space-y-1.5">
767
  <div className="relative h-2 bg-slate-100 rounded-full overflow-hidden border border-slate-200">
768
  <div
769
- className="absolute inset-y-0 left-0 bg-gradient-to-right from-cyan-400 to-cyan-500 transition-all duration-300"
770
  style={{ width: `${uploadProgress}%` }}
771
  />
772
  </div>
@@ -781,8 +851,10 @@ export default function EnrollPage() {
781
  {/* ─── State 5: SUCCESS SCREEN ─── */}
782
  {captureState === "success" && (
783
  <div className="max-w-md mx-auto bg-white border border-slate-200 rounded-3xl p-8 shadow-2xl text-center space-y-6 animate-scaleIn">
784
- <div className="relative w-20 h-20 mx-auto bg-emerald-50 rounded-full flex items-center justify-center border border-emerald-100">
785
- <CheckCircle2 className="w-10 h-10 text-emerald-500" />
 
 
786
  </div>
787
 
788
  <div className="space-y-2">
@@ -795,14 +867,14 @@ export default function EnrollPage() {
795
  <div className="flex gap-3 justify-center pt-2">
796
  <button
797
  onClick={() => router.push("/employees")}
798
- className="h-10 px-6 bg-slate-100 hover:bg-slate-150 border border-slate-200 text-slate-800 font-bold text-xs uppercase tracking-wider rounded-xl transition-all cursor-pointer"
799
  >
800
  Employees List
801
  </button>
802
 
803
  <button
804
  onClick={startAutoCapture}
805
- className="h-10 px-6 bg-slate-950 hover:bg-slate-900 border border-slate-950 text-white font-bold text-xs uppercase tracking-wider rounded-xl transition-all shadow-md cursor-pointer"
806
  >
807
  Re-enroll Profile
808
  </button>
@@ -823,6 +895,52 @@ export default function EnrollPage() {
823
  </div>
824
  )}
825
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  </SidebarLayout>
827
  );
828
  }
 
7
  import { fetchApi, getBackendUrl } from "@/app/utils/api";
8
  import {
9
  Camera, Upload, CheckCircle2, ChevronLeft, XCircle, Video,
10
+ RefreshCw, AlertCircle, Trash2, Play, Pause, Save, RotateCcw, Shield, Activity, Sparkles,
11
+ User, ArrowLeft, ArrowRight, ArrowUp, ArrowDown, Smile, Meh, Lightbulb, Sun, Glasses
12
  } from "lucide-react";
13
 
14
  interface PoseInfo {
15
  label: string;
16
  hint: string;
17
  speech: string;
18
+ icon: any;
19
  }
20
 
21
  const POSES: Record<string, PoseInfo> = {
 
23
  label: "Front Profile",
24
  hint: "Look straight into the camera with a neutral expression.",
25
  speech: "Please look straight into the camera.",
26
+ icon: User
27
  },
28
  left: {
29
  label: "Left Profile",
30
  hint: "Turn your head slowly to the left.",
31
  speech: "Please turn your head to the left.",
32
+ icon: ArrowLeft
33
  },
34
  right: {
35
  label: "Right Profile",
36
  hint: "Turn your head slowly to the right.",
37
  speech: "Please turn your head to the right.",
38
+ icon: ArrowRight
39
  },
40
  up: {
41
  label: "Looking Up",
42
  hint: "Tilt your chin upwards slightly.",
43
  speech: "Please tilt your head upwards.",
44
+ icon: ArrowUp
45
  },
46
  down: {
47
  label: "Looking Down",
48
  hint: "Tilt your chin downwards slightly.",
49
  speech: "Please tilt your head downwards.",
50
+ icon: ArrowDown
51
  },
52
  smile: {
53
  label: "Smiling Face",
54
  hint: "Give a natural, relaxed smile.",
55
  speech: "Now, smile naturally.",
56
+ icon: Smile
57
  },
58
  neutral: {
59
  label: "Neutral Face",
60
  hint: "Keep a relaxed, standard neutral expression.",
61
  speech: "Relax your face, show a neutral expression.",
62
+ icon: Meh
63
  },
64
  indoor: {
65
  label: "Indoor Light",
66
  hint: "Look straight with standard indoor room lighting.",
67
  speech: "Look straight for typical indoor lighting.",
68
+ icon: Lightbulb
69
  },
70
  outdoor: {
71
  label: "Outdoor Light",
72
  hint: "Look straight with bright/outdoor lighting.",
73
  speech: "Look straight for bright light capture.",
74
+ icon: Sun
75
  },
76
  glasses: {
77
  label: "Glasses Option",
78
  hint: "Put on glasses if you wear them, otherwise look straight.",
79
  speech: "If you wear glasses, put them on. Otherwise, look straight.",
80
+ icon: Glasses
81
  }
82
  };
83
 
 
111
  const [successMsg, setSuccessMsg] = useState<string | null>(null);
112
  const [webcamActive, setWebcamActive] = useState(false);
113
  const [singleRetakePose, setSingleRetakePose] = useState<string | null>(null);
114
+ const [showClearConfirm, setShowClearConfirm] = useState(false);
115
 
116
  const videoRef = useRef<HTMLVideoElement>(null);
117
  const canvasRef = useRef<HTMLCanvasElement>(null);
 
251
  }
252
 
253
  countdownIntervalRef.current = setInterval(() => {
254
+ if (countdown <= 1) {
255
+ if (countdownIntervalRef.current) {
256
+ clearInterval(countdownIntervalRef.current);
257
+ countdownIntervalRef.current = null;
 
258
  }
259
+ captureFrame(currentKey);
260
+ } else {
261
  playSound("beep");
262
+ setCountdown(countdown - 1);
263
+ }
264
  }, 1000);
265
 
266
  return () => {
 
307
  setSingleRetakePose(poseKey);
308
  setSelectedPose(poseKey);
309
  setCountdown(3);
310
+ setIsPaused(false);
311
  setCaptureState("capturing");
312
 
313
  // Set index to match keys
 
432
  border-width: 2px;
433
  pointer-events: none;
434
  }
435
+ @keyframes draw-check {
436
+ 0% { stroke-dashoffset: 48; }
437
+ 100% { stroke-dashoffset: 0; }
438
+ }
439
+ @keyframes scale-up {
440
+ 0% { transform: scale(0); opacity: 0; }
441
+ 100% { transform: scale(1); opacity: 1; }
442
+ }
443
+ @keyframes success-glowing {
444
+ 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
445
+ 70% { box-shadow: 0 0 0 15px rgba(16, 185, 129, 0); }
446
+ 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
447
+ }
448
+ .success-circle {
449
+ animation: scale-up 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards, success-glowing 2s infinite;
450
+ }
451
+ .success-check {
452
+ stroke-dasharray: 48;
453
+ stroke-dashoffset: 48;
454
+ animation: draw-check 0.6s cubic-bezier(0.65, 0, 0.45, 1) 0.3s forwards;
455
+ }
456
  `}</style>
457
 
458
  <canvas ref={canvasRef} className="hidden" />
 
484
 
485
  {enrolledCount > 0 && (
486
  <button
487
+ onClick={() => setShowClearConfirm(true)}
488
  className="flex items-center gap-2 text-[11.5px] font-bold text-rose-600 hover:text-white bg-white hover:bg-rose-600 border border-rose-200 hover:border-rose-600 px-4 py-2 rounded-xl transition-all cursor-pointer shadow-sm"
489
  >
490
  <Trash2 className="w-3.5 h-3.5" />
 
576
  To capture accurate biometric details under varying orientations and lighting, we index 10 distinct facial angles.
577
  </p>
578
 
579
+ <div className="grid grid-cols-2 sm:grid-cols-5 gap-3.5 pt-2">
580
  {POSE_KEYS.map((key) => {
581
  const done = status?.enrolled_poses?.some((p: string) => p.toLowerCase() === key.toLowerCase());
582
+ const Icon = POSES[key].icon;
583
  return (
584
  <div
585
  key={key}
586
+ className={`p-4 rounded-2xl border flex flex-col items-center text-center justify-center transition-all hover:scale-[1.03] duration-200 select-none ${
587
  done
588
+ ? "bg-emerald-500/[0.03] border-emerald-500/20 text-emerald-800 shadow-[0_2px_12px_rgba(16,185,129,0.02)]"
589
+ : "bg-slate-50/50 border-slate-200/80 hover:border-slate-350"
590
  }`}
591
  >
592
+ <div className={`w-9 h-9 rounded-xl flex items-center justify-center mb-2.5 transition-all ${
593
+ done
594
+ ? "bg-emerald-500/10 text-emerald-600"
595
+ : "bg-slate-100 text-slate-400"
596
+ }`}>
597
+ <Icon className="w-4.5 h-4.5" />
598
+ </div>
599
+ <span className={`text-[10px] font-bold uppercase tracking-wider font-mono ${done ? "text-emerald-800" : "text-slate-505"}`}>
600
+ {POSES[key].label.replace(" Profile", "").replace(" Face", "").replace(" Option", "").replace(" Light", "")}
601
+ </span>
602
  {done ? (
603
+ <CheckCircle2 className="w-4 h-4 text-emerald-500 mt-3 shrink-0" />
604
  ) : (
605
+ <div className="w-4 h-4 rounded-full border-2 border-slate-200 mt-3 shrink-0 bg-white" />
606
  )}
607
  </div>
608
  );
 
619
  <div className="w-full bg-slate-950 text-white rounded-2xl p-5 flex items-center justify-between shadow-lg relative overflow-hidden">
620
  <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)] pointer-events-none" />
621
  <div className="flex items-center gap-4 relative z-10">
622
+ {(() => {
623
+ const Icon = POSES[POSE_KEYS[currentPoseIndex]]?.icon;
624
+ return (
625
+ <div className="w-12 h-12 rounded-xl bg-white/10 flex items-center justify-center shrink-0 border border-white/10 text-cyan-400">
626
+ {Icon && <Icon className="w-6 h-6" />}
627
+ </div>
628
+ );
629
+ })()}
630
  <div>
631
  <p className="text-[10px] font-bold text-cyan-400 font-mono tracking-widest uppercase">
632
  SCAN PHASE {currentPoseIndex + 1} OF {POSE_KEYS.length}
 
652
  {/* Native video element */}
653
  <video
654
  ref={videoRef}
655
+ className={`w-full h-full object-cover scale-x-[-1] transition-opacity duration-300 ${isPaused ? "opacity-20" : "opacity-100"}`}
656
  autoPlay playsInline muted
657
  />
658
 
659
+ {/* Paused Overlay */}
660
+ {isPaused && (
661
+ <div className="absolute inset-0 flex flex-col items-center justify-center bg-black/40 backdrop-blur-sm z-20 transition-all duration-300">
662
+ <div className="w-16 h-16 rounded-2xl bg-white/10 border border-white/20 flex items-center justify-center shadow-lg text-white mb-3 animate-pulse">
663
+ <Pause className="w-8 h-8 fill-current text-cyan-400" />
664
+ </div>
665
+ <p className="text-[11px] font-bold text-cyan-400 font-mono tracking-widest uppercase">
666
+ SYS.STATUS: SCAN_PAUSED
667
+ </p>
668
+ <p className="text-xs text-slate-300 font-medium mt-1">
669
+ Camera turned off to preserve resources & privacy.
670
+ </p>
671
+ </div>
672
+ )}
673
+
674
  {/* Cybernetic HUD elements */}
675
+ {!isPaused && <div className="scanner-line" />}
676
+ {!isPaused && (
677
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
678
+ <div className="scanner-target">
679
+ <div className="w-4 h-4 border border-cyan-400 rounded-full animate-ping" />
680
+ </div>
681
  </div>
682
+ )}
683
 
684
  {/* HUD corners */}
685
  <div className="hud-corner corner-bracket-tl top-6 left-6 border-t-2 border-l-2" />
 
689
 
690
  {/* Scanner stats HUD */}
691
  <div className="absolute top-6 left-12 right-12 flex justify-between text-[9px] font-mono font-bold text-cyan-400/80 pointer-events-none uppercase">
692
+ <span>SYS.STATUS: {isPaused ? "SCAN_PAUSED" : "ACQUIRING_DATA"}</span>
693
+ <span>FPS: {isPaused ? "0" : "60"} · ISO: 200 · SHUTTER: AUTO</span>
694
  </div>
695
 
696
  <div className="absolute bottom-6 left-12 right-12 flex justify-between items-center text-[9px] font-mono font-bold text-cyan-400/80 pointer-events-none">
697
  <span>ANGLE: {POSE_KEYS[currentPoseIndex]?.toUpperCase()}</span>
698
+ <span>LIVENESS CHECK: {isPaused ? "INACTIVE" : "ACTIVE"}</span>
699
  </div>
700
  </div>
701
 
702
  {/* Controls */}
703
  <div className="flex gap-4 w-full max-w-lg">
704
  <button
705
+ onClick={async () => {
706
+ if (isPaused) {
707
+ setIsPaused(false);
708
+ await startWebcam();
709
+ } else {
710
+ setIsPaused(true);
711
+ stopWebcam();
712
+ }
713
+ }}
714
  className="flex-1 h-11 bg-white hover:bg-slate-50 border border-slate-250 text-slate-800 font-bold text-xs uppercase tracking-wider rounded-xl flex items-center justify-center gap-2 transition-all shadow-sm cursor-pointer"
715
  >
716
  {isPaused ? <Play className="w-3.5 h-3.5 fill-current" /> : <Pause className="w-3.5 h-3.5 fill-current" />}
 
718
  </button>
719
 
720
  <button
721
+ onClick={async () => {
722
  if (singleRetakePose) {
723
  stopWebcam();
724
  setSingleRetakePose(null);
 
727
  // Skip pose
728
  setCurrentPoseIndex(prev => prev + 1);
729
  setCountdown(3);
730
+ if (isPaused) {
731
+ setIsPaused(false);
732
+ await startWebcam();
733
+ }
734
  }
735
  }}
736
  className="flex-1 h-11 bg-slate-950 hover:bg-slate-900 border border-slate-950 text-white font-bold text-xs uppercase tracking-wider rounded-xl flex items-center justify-center gap-2 transition-all shadow-md cursor-pointer"
 
780
  </div>
781
 
782
  <div className="mt-2.5 flex items-center justify-between text-[11px] font-bold">
783
+ <span className="text-slate-900 uppercase font-mono">{POSES[key].label.replace(" Profile", "").replace(" Face", "").replace(" Option", "").replace(" Light", "")}</span>
784
  {imgUrl ? (
785
  <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" />
786
  ) : (
 
836
  <div className="space-y-1.5">
837
  <div className="relative h-2 bg-slate-100 rounded-full overflow-hidden border border-slate-200">
838
  <div
839
+ className="absolute inset-y-0 left-0 bg-gradient-to-r from-cyan-400 to-cyan-500 transition-all duration-300"
840
  style={{ width: `${uploadProgress}%` }}
841
  />
842
  </div>
 
851
  {/* ─── State 5: SUCCESS SCREEN ─── */}
852
  {captureState === "success" && (
853
  <div className="max-w-md mx-auto bg-white border border-slate-200 rounded-3xl p-8 shadow-2xl text-center space-y-6 animate-scaleIn">
854
+ <div className="relative w-20 h-20 mx-auto bg-emerald-50 rounded-full flex items-center justify-center border border-emerald-100 success-circle">
855
+ <svg className="w-10 h-10 text-emerald-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round">
856
+ <polyline points="20 6 9 17 4 12" className="success-check" />
857
+ </svg>
858
  </div>
859
 
860
  <div className="space-y-2">
 
867
  <div className="flex gap-3 justify-center pt-2">
868
  <button
869
  onClick={() => router.push("/employees")}
870
+ className="h-10 px-6 bg-slate-950 hover:bg-slate-900 border border-slate-950 text-white font-bold text-xs uppercase tracking-wider rounded-xl transition-all shadow-md cursor-pointer"
871
  >
872
  Employees List
873
  </button>
874
 
875
  <button
876
  onClick={startAutoCapture}
877
+ className="h-10 px-6 bg-slate-100 hover:bg-slate-150 border border-slate-200 text-slate-800 font-bold text-xs uppercase tracking-wider rounded-xl transition-all cursor-pointer"
878
  >
879
  Re-enroll Profile
880
  </button>
 
895
  </div>
896
  )}
897
  </div>
898
+
899
+ {/* Clear Biometric Confirmation Modal */}
900
+ {showClearConfirm && (
901
+ <div className="modal-backdrop z-50">
902
+ <div className="modal-content max-w-sm border border-red-500/10 shadow-[0_12px_40px_rgba(239,68,68,0.12)]">
903
+ <div className="flex flex-col items-center text-center p-2 space-y-4">
904
+ <div className="w-12 h-12 rounded-2xl bg-rose-500/10 border border-rose-500/15 flex items-center justify-center text-rose-500 shadow-inner">
905
+ <Trash2 className="w-6 h-6" />
906
+ </div>
907
+ <div>
908
+ <h3 className="text-sm font-bold text-[var(--text-primary)] uppercase tracking-wider">Clear Biometric Data</h3>
909
+ <p className="text-xs text-slate-500 mt-2 leading-relaxed">
910
+ Wipe all registered biometric vectors and images for <span className="font-semibold text-[var(--text-primary)]">{employee?.name}</span>?
911
+ </p>
912
+ <p className="text-[10.5px] text-rose-500 font-medium bg-rose-500/5 border border-rose-500/10 rounded-xl p-2.5 mt-3 leading-normal">
913
+ Warning: This cannot be undone and the user will not be able to log in or register attendance at the kiosk until re-enrolled.
914
+ </p>
915
+ </div>
916
+ <div className="flex gap-2.5 w-full pt-2 border-t border-white/5">
917
+ <button
918
+ type="button"
919
+ onClick={() => setShowClearConfirm(false)}
920
+ className="flex-1 btn-ghost h-9.5 text-[12px] rounded-xl cursor-pointer hover:bg-white/[0.04]"
921
+ >
922
+ Cancel
923
+ </button>
924
+ <button
925
+ type="button"
926
+ onClick={() => {
927
+ clearMutation.mutate();
928
+ setShowClearConfirm(false);
929
+ }}
930
+ disabled={clearMutation.isPending}
931
+ className="flex-1 bg-rose-600 hover:bg-rose-500 active:bg-rose-700 text-white font-bold text-[12px] rounded-xl cursor-pointer h-9.5 flex items-center justify-center gap-2 shadow-md shadow-rose-950/20 border border-rose-500/20"
932
+ >
933
+ {clearMutation.isPending ? (
934
+ <div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
935
+ ) : (
936
+ "Wipe Data"
937
+ )}
938
+ </button>
939
+ </div>
940
+ </div>
941
+ </div>
942
+ </div>
943
+ )}
944
  </SidebarLayout>
945
  );
946
  }
frontend/app/globals.css CHANGED
@@ -7,32 +7,32 @@
7
  --bg-base: #ffffff; /* Pure White */
8
  --bg-surface: #ffffff; /* Pure White */
9
  --bg-elevated: #ffffff; /* Pure White */
10
- --bg-overlay: rgba(255, 255, 255, 0.95);
11
-
12
- --accent-primary: #18181b; /* Zinc 900 / Dark Grey */
13
- --accent-cyan: #27272a; /* Zinc 800 */
14
- --accent-indigo: #27272a;
15
- --accent-emerald: #18181b;
16
- --accent-amber: #3f3f46;
17
- --accent-rose: #18181b;
18
-
19
- --border-subtle: #f4f4f5; /* Zinc 100 */
20
- --border-medium: #e4e4e7; /* Zinc 200 */
21
- --border-strong: #d4d4d8; /* Zinc 300 */
22
-
23
- --text-primary: #18181b; /* Zinc 900 - Dark Grey */
24
- --text-secondary: #3f3f46; /* Zinc 700 - Medium Dark Grey */
25
- --text-muted: #71717a; /* Zinc 500 - Medium Grey */
26
- --text-faint: #a1a1aa; /* Zinc 400 - Light Grey */
27
-
28
- --glow-blue: 0 4px 20px rgba(24, 24, 27, 0.03);
29
- --glow-cyan: 0 4px 20px rgba(24, 24, 27, 0.03);
30
- --radius-sm: 8px;
31
- --radius-md: 12px;
32
- --radius-lg: 16px;
33
- --radius-xl: 20px;
34
- --radius-2xl: 24px;
35
- --radius-3xl: 32px;
36
  }
37
 
38
  /* ─── Base Reset ─── */
@@ -91,15 +91,14 @@ h1, h2, h3, h4, h5, h6 {
91
  .glass-card {
92
  background: #ffffff;
93
  border: 1px solid var(--border-medium);
94
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.02);
95
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
96
  }
97
 
98
  .glass-card:hover {
99
  border-color: var(--border-strong);
100
  background: #ffffff;
101
- transform: translateY(-0.5px);
102
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
103
  }
104
 
105
  /* ─── Card Glow Variants (Clean zinc shadows) ─── */
@@ -125,14 +124,13 @@ h1, h2, h3, h4, h5, h6 {
125
  .btn-primary {
126
  background: var(--text-primary);
127
  color: white;
128
- font-weight: 650;
129
  border: 1px solid var(--text-primary);
130
- border-radius: var(--radius-lg);
131
- padding: 10px 20px;
132
- font-size: 13px;
133
  cursor: pointer;
134
- transition: all 0.15s ease;
135
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
136
  position: relative;
137
  overflow: hidden;
138
  }
@@ -140,26 +138,24 @@ h1, h2, h3, h4, h5, h6 {
140
  .btn-primary:hover {
141
  background: #27272a;
142
  border-color: #27272a;
143
- transform: translateY(-0.5px);
144
- box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12);
145
  }
146
- .btn-primary:active { transform: translateY(0); }
147
- .btn-primary:disabled { background: #e4e4e7; border-color: #e4e4e7; color: #a1a1aa; cursor: not-allowed; transform: none; box-shadow: none; }
148
 
149
  .btn-ghost {
150
  background: #ffffff;
151
  color: var(--text-secondary);
152
  border: 1px solid var(--border-medium);
153
- border-radius: var(--radius-lg);
154
- padding: 9px 18px;
155
- font-size: 13px;
156
  font-weight: 500;
157
  cursor: pointer;
158
- transition: all 0.15s ease;
159
  }
160
 
161
  .btn-ghost:hover {
162
- background: #f4f4f5;
163
  border-color: var(--border-strong);
164
  color: var(--text-primary);
165
  }
@@ -170,12 +166,12 @@ h1, h2, h3, h4, h5, h6 {
170
  background: #ffffff;
171
  border: 1px solid var(--border-medium);
172
  border-radius: var(--radius-md);
173
- padding: 0 14px;
174
- height: 40px;
175
  color: var(--text-primary);
176
- font-size: 13px;
177
  font-family: inherit;
178
- transition: all 0.15s ease;
179
  outline: none;
180
  }
181
 
@@ -184,7 +180,6 @@ h1, h2, h3, h4, h5, h6 {
184
  .input-field:focus {
185
  border-color: var(--text-primary);
186
  background: #ffffff;
187
- box-shadow: 0 0 0 3px rgba(24, 24, 27, 0.05);
188
  }
189
 
190
  select.input-field option {
@@ -254,21 +249,41 @@ select.input-field option {
254
  margin: 0;
255
  }
256
 
257
- /* ─── Ambient Background Effects (Hidden for strict minimalism) ─── */
258
- .ambient-bg {
 
259
  position: fixed;
260
  inset: 0;
261
  pointer-events: none;
262
  z-index: 0;
263
- background: #ffffff;
 
 
 
 
 
 
 
 
 
264
  }
265
 
266
- /* ─── Grid Mesh Background (Minimalist) ─── */
267
  .mesh-bg {
268
- background-image:
269
- linear-gradient(rgba(24, 24, 27, 0.01) 1px, transparent 1px),
270
- linear-gradient(90deg, rgba(24, 24, 27, 0.01) 1px, transparent 1px);
271
- background-size: 48px 48px;
 
 
 
 
 
 
 
 
 
 
 
272
  }
273
 
274
  /* ─── Kiosk Scanner Laser ─── */
@@ -362,30 +377,31 @@ select.input-field option {
362
  }
363
 
364
  .data-table thead th {
365
- padding: 12px 20px;
366
- font-size: 10px;
367
- font-weight: 650;
368
  text-transform: uppercase;
369
- letter-spacing: 0.08em;
370
- color: var(--text-secondary);
371
- font-family: 'JetBrains Mono', monospace;
372
- background: #f4f4f5;
 
373
  }
374
 
375
  .data-table tbody tr {
376
- border-bottom: 1px solid rgba(24, 24, 27, 0.04);
377
- transition: background 0.15s ease;
378
  }
379
 
380
  .data-table tbody tr:last-child { border-bottom: none; }
381
 
382
  .data-table tbody tr:hover {
383
- background: #f4f4f5;
384
  }
385
 
386
  .data-table tbody td {
387
- padding: 14px 20px;
388
- font-size: 12.5px;
389
  color: var(--text-secondary);
390
  }
391
 
@@ -557,3 +573,297 @@ select.input-field option {
557
  animation: laserFlicker 1.5s ease-in-out infinite;
558
  }
559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  --bg-base: #ffffff; /* Pure White */
8
  --bg-surface: #ffffff; /* Pure White */
9
  --bg-elevated: #ffffff; /* Pure White */
10
+ --bg-overlay: rgba(255, 255, 255, 0.98);
11
+
12
+ --accent-primary: #09090b; /* Zinc 950 */
13
+ --accent-cyan: #18181b; /* Zinc 900 */
14
+ --accent-indigo: #18181b;
15
+ --accent-emerald: #09090b;
16
+ --accent-amber: #27272a;
17
+ --accent-rose: #09090b;
18
+
19
+ --border-subtle: #fafafa; /* Zinc 50 */
20
+ --border-medium: #f4f4f5; /* Zinc 100 */
21
+ --border-strong: #e4e4e7; /* Zinc 200 */
22
+
23
+ --text-primary: #09090b; /* Zinc 950 */
24
+ --text-secondary: #27272a; /* Zinc 900 */
25
+ --text-muted: #71717a; /* Zinc 500 */
26
+ --text-faint: #a1a1aa; /* Zinc 400 */
27
+
28
+ --glow-blue: none;
29
+ --glow-cyan: none;
30
+ --radius-sm: 6px;
31
+ --radius-md: 8px;
32
+ --radius-lg: 12px;
33
+ --radius-xl: 14px;
34
+ --radius-2xl: 16px;
35
+ --radius-3xl: 20px;
36
  }
37
 
38
  /* ─── Base Reset ─── */
 
91
  .glass-card {
92
  background: #ffffff;
93
  border: 1px solid var(--border-medium);
94
+ box-shadow: none;
95
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
96
  }
97
 
98
  .glass-card:hover {
99
  border-color: var(--border-strong);
100
  background: #ffffff;
101
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.015);
 
102
  }
103
 
104
  /* ─── Card Glow Variants (Clean zinc shadows) ─── */
 
124
  .btn-primary {
125
  background: var(--text-primary);
126
  color: white;
127
+ font-weight: 500;
128
  border: 1px solid var(--text-primary);
129
+ border-radius: var(--radius-md);
130
+ padding: 8px 16px;
131
+ font-size: 12px;
132
  cursor: pointer;
133
+ transition: all 0.12s ease;
 
134
  position: relative;
135
  overflow: hidden;
136
  }
 
138
  .btn-primary:hover {
139
  background: #27272a;
140
  border-color: #27272a;
 
 
141
  }
142
+ .btn-primary:active { transform: scale(0.98); }
143
+ .btn-primary:disabled { background: #f4f4f5; border-color: #f4f4f5; color: #a1a1aa; cursor: not-allowed; transform: none; box-shadow: none; }
144
 
145
  .btn-ghost {
146
  background: #ffffff;
147
  color: var(--text-secondary);
148
  border: 1px solid var(--border-medium);
149
+ border-radius: var(--radius-md);
150
+ padding: 8px 16px;
151
+ font-size: 12px;
152
  font-weight: 500;
153
  cursor: pointer;
154
+ transition: all 0.12s ease;
155
  }
156
 
157
  .btn-ghost:hover {
158
+ background: #fafafa;
159
  border-color: var(--border-strong);
160
  color: var(--text-primary);
161
  }
 
166
  background: #ffffff;
167
  border: 1px solid var(--border-medium);
168
  border-radius: var(--radius-md);
169
+ padding: 0 12px;
170
+ height: 36px;
171
  color: var(--text-primary);
172
+ font-size: 12.5px;
173
  font-family: inherit;
174
+ transition: all 0.12s ease;
175
  outline: none;
176
  }
177
 
 
180
  .input-field:focus {
181
  border-color: var(--text-primary);
182
  background: #ffffff;
 
183
  }
184
 
185
  select.input-field option {
 
249
  margin: 0;
250
  }
251
 
252
+ /* ─── Ambient Background Effects (Flowing Cyber Wave & Tech Grid) ─── */
253
+ .ambient-bg,
254
+ .mesh-bg {
255
  position: fixed;
256
  inset: 0;
257
  pointer-events: none;
258
  z-index: 0;
259
+ background-color: var(--bg-base);
260
+ background-image:
261
+ radial-gradient(circle at 50% 50%, rgba(6, 182, 212, 0.08) 0%, rgba(59, 130, 246, 0.03) 40%, transparent 70%),
262
+ radial-gradient(circle at 50% 50%, transparent 30%, var(--bg-base) 85%),
263
+ linear-gradient(rgba(148, 163, 184, 0.03) 1px, transparent 1px),
264
+ linear-gradient(90deg, rgba(148, 163, 184, 0.03) 1px, transparent 1px),
265
+ url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 600' width='100%' height='100%'><defs><filter id='glow-light' x='-50%' y='-50%' width='200%' height='200%'><feGaussianBlur in='SourceGraphic' stdDeviation='4' result='blur'/><feMerge><feMergeNode in='blur'/><feMergeNode in='SourceGraphic'/></feMerge></filter><linearGradient id='grad-cyan-light' x1='0' y1='0' x2='0' y2='1'><stop offset='0%' stop-color='%2306b6d4' stop-opacity='0.15'/><stop offset='100%' stop-color='%2306b6d4' stop-opacity='0'/></linearGradient><linearGradient id='grad-indigo-light' x1='0' y1='0' x2='0' y2='1'><stop offset='0%' stop-color='%236366f1' stop-opacity='0.1'/><stop offset='100%' stop-color='%236366f1' stop-opacity='0'/></linearGradient></defs><style>.wave-1{animation:flow-1 38s linear infinite, bob-1 12s ease-in-out infinite alternate}.wave-2{animation:flow-2 28s linear infinite, bob-2 8s ease-in-out infinite alternate}.wave-3{animation:flow-3 22s linear infinite, bob-3 10s ease-in-out infinite alternate}.wave-4{animation:flow-4 16s linear infinite, bob-4 6s ease-in-out infinite alternate}@keyframes flow-1{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(-1440px,0,0)}}@keyframes flow-2{0%{transform:translate3d(-1440px,0,0)}100%{transform:translate3d(0,0,0)}}@keyframes flow-3{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(-1440px,0,0)}}@keyframes flow-4{0%{transform:translate3d(-1440px,0,0)}100%{transform:translate3d(0,0,0)}}@keyframes bob-1{0%{transform:translateY(-10px)}100%{transform:translateY(10px)}}@keyframes bob-2{0%{transform:translateY(8px)}100%{transform:translateY(-8px)}}@keyframes bob-3{0%{transform:translateY(-6px)}100%{transform:translateY(6px)}}@keyframes bob-4{0%{transform:translateY(5px)}100%{transform:translateY(-5px)}}</style><path class='wave-1' fill='url(%23grad-cyan-light)' d='M-1440,320 C-1080,240 -720,400 -360,320 C0,240 360,400 720,320 C1080,240 1440,400 1800,320 C2160,240 2520,400 2880,320 L2880,600 L-1440,600 Z'/><path class='wave-2' fill='url(%23grad-indigo-light)' d='M-1440,340 C-1080,440 -720,240 -360,340 C0,440 360,240 720,340 C1080,440 1440,240 1800,340 C2160,440 2520,240 2880,340 L2880,600 L-1440,600 Z'/><path class='wave-3' stroke='%2306b6d4' stroke-width='1.5' fill='none' opacity='0.45' filter='url(%23glow-light)' d='M-1440,310 C-1080,250 -720,370 -360,310 C0,250 360,370 720,310 C1080,250 1440,370 1800,310 C2160,250 2520,370 2880,310'/><path class='wave-4' stroke='%236366f1' stroke-width='1.25' fill='none' opacity='0.35' d='M-1440,330 C-1080,380 -720,280 -360,330 C0,380 360,280 720,330 C1080,380 1440,280 1800,330 C2160,380 2520,280 2880,330'/><path class='wave-3' stroke='%233b82f6' stroke-width='0.75' fill='none' opacity='0.5' d='M-1440,300 C-1080,220 -720,380 -360,300 C0,220 360,380 720,300 C1080,220 1440,380 1800,300 C2160,220 2520,380 2880,300'/></svg>");
266
+ background-size: cover, cover, 80px 80px, 80px 80px, cover;
267
+ background-position: center;
268
+ background-repeat: no-repeat, no-repeat, repeat, repeat, no-repeat;
269
  }
270
 
 
271
  .mesh-bg {
272
+ position: absolute;
273
+ }
274
+
275
+ /* Translucent Glass Overlay for Cards and Modals */
276
+ .glass-overlay {
277
+ background-color: rgba(255, 255, 255, 0.45) !important;
278
+ backdrop-filter: blur(12px);
279
+ -webkit-backdrop-filter: blur(12px);
280
+ border: 1px solid rgba(228, 228, 231, 0.6) !important;
281
+ }
282
+ .dark .glass-overlay {
283
+ background-color: rgba(9, 9, 11, 0.45) !important;
284
+ backdrop-filter: blur(12px);
285
+ -webkit-backdrop-filter: blur(12px);
286
+ border: 1px solid rgba(39, 39, 42, 0.4) !important;
287
  }
288
 
289
  /* ─── Kiosk Scanner Laser ─── */
 
377
  }
378
 
379
  .data-table thead th {
380
+ padding: 10px 16px;
381
+ font-size: 9.5px;
382
+ font-weight: 500;
383
  text-transform: uppercase;
384
+ letter-spacing: 0.05em;
385
+ color: var(--text-muted);
386
+ font-family: var(--font-inter), sans-serif;
387
+ background: #ffffff;
388
+ border-bottom: 1px solid var(--border-medium);
389
  }
390
 
391
  .data-table tbody tr {
392
+ border-bottom: 1px solid var(--border-medium);
393
+ transition: background 0.12s ease;
394
  }
395
 
396
  .data-table tbody tr:last-child { border-bottom: none; }
397
 
398
  .data-table tbody tr:hover {
399
+ background: #fafafa;
400
  }
401
 
402
  .data-table tbody td {
403
+ padding: 12px 16px;
404
+ font-size: 12px;
405
  color: var(--text-secondary);
406
  }
407
 
 
573
  animation: laserFlicker 1.5s ease-in-out infinite;
574
  }
575
 
576
+ /* ─── Dark Theme Overrides ─── */
577
+ .dark {
578
+ --bg-base: #09090b; /* Zinc 950 */
579
+ --bg-surface: #09090b; /* Zinc 950 */
580
+ --bg-elevated: #18181b; /* Zinc 900 */
581
+ --bg-overlay: rgba(9, 9, 11, 0.98);
582
+
583
+ --accent-primary: #ffffff;
584
+ --accent-cyan: #22d3ee;
585
+ --accent-indigo: #818cf8;
586
+ --accent-emerald: #34d399;
587
+ --accent-amber: #fbbf24;
588
+ --accent-rose: #f87171;
589
+
590
+ --border-subtle: #18181b; /* Zinc 900 */
591
+ --border-medium: #27272a; /* Zinc 800 */
592
+ --border-strong: #3f3f46; /* Zinc 700 */
593
+
594
+ --text-primary: #f4f4f5; /* Zinc 100 */
595
+ --text-secondary: #e4e4e7; /* Zinc 200 */
596
+ --text-muted: #a1a1aa; /* Zinc 400 */
597
+ --text-faint: #71717a; /* Zinc 500 */
598
+ }
599
+
600
+ /* Dynamic dark mode utility overrides for hardcoded Tailwind classes */
601
+ .dark .bg-white {
602
+ background-color: #18181b !important;
603
+ }
604
+
605
+ .dark .border-zinc-100,
606
+ .dark .border-slate-100,
607
+ .dark .border-slate-200,
608
+ .dark .border-slate-250 {
609
+ border-color: #27272a !important;
610
+ }
611
+
612
+ .dark .text-slate-900,
613
+ .dark .text-slate-800,
614
+ .dark .text-zinc-950,
615
+ .dark .text-zinc-900,
616
+ .dark .text-zinc-850,
617
+ .dark .text-zinc-805,
618
+ .dark .text-zinc-800,
619
+ .dark .text-zinc-755,
620
+ .dark .text-zinc-750 {
621
+ color: #f4f4f5 !important; /* Zinc 100 */
622
+ }
623
+
624
+ .dark .text-slate-555,
625
+ .dark .text-slate-550,
626
+ .dark .text-slate-500,
627
+ .dark .text-slate-450,
628
+ .dark .text-slate-400,
629
+ .dark .text-zinc-555,
630
+ .dark .text-zinc-550,
631
+ .dark .text-zinc-500,
632
+ .dark .text-zinc-450,
633
+ .dark .text-zinc-400 {
634
+ color: #a1a1aa !important; /* Zinc 400 */
635
+ }
636
+
637
+ .dark .text-slate-650,
638
+ .dark .text-slate-600,
639
+ .dark .text-slate-700,
640
+ .dark .text-zinc-650,
641
+ .dark .text-zinc-605,
642
+ .dark .text-zinc-600,
643
+ .dark .text-zinc-700 {
644
+ color: #d4d4d8 !important; /* Zinc 300 */
645
+ }
646
+
647
+ .dark .bg-zinc-50,
648
+ .dark .bg-zinc-55,
649
+ .dark .bg-slate-50,
650
+ .dark .bg-slate-100 {
651
+ background-color: #27272a !important;
652
+ }
653
+
654
+ .dark .bg-zinc-100 {
655
+ background-color: #18181b !important;
656
+ }
657
+
658
+ .dark .border-zinc-200 {
659
+ border-color: #3f3f46 !important;
660
+ }
661
+
662
+ .dark .hover\:bg-zinc-50:hover,
663
+ .dark .hover\:bg-zinc-100:hover,
664
+ .dark .hover\:bg-slate-50:hover,
665
+ .dark .hover\:bg-slate-100:hover {
666
+ background-color: #27272a !important;
667
+ }
668
+
669
+ .dark .hover\:bg-white\/\[0\.015\]:hover {
670
+ background-color: rgba(255, 255, 255, 0.03) !important;
671
+ }
672
+
673
+ .dark .hover\:border-zinc-205:hover,
674
+ .dark .hover\:border-zinc-200:hover,
675
+ .dark .hover\:border-zinc-300:hover,
676
+ .dark .hover\:border-slate-200:hover,
677
+ .dark .hover\:border-slate-300:hover {
678
+ border-color: #3f3f46 !important;
679
+ }
680
+
681
+ /* Component overrides */
682
+ .dark .glass,
683
+ .dark .glass-panel,
684
+ .dark .glass-card {
685
+ background: #18181b;
686
+ border-color: #27272a;
687
+ }
688
+
689
+ .dark .data-table thead th {
690
+ background: #18181b;
691
+ border-color: #27272a;
692
+ }
693
+
694
+ .dark .data-table tbody tr {
695
+ border-color: #27272a;
696
+ }
697
+
698
+ .dark .data-table tbody tr:hover {
699
+ background: #27272a !important;
700
+ }
701
+
702
+ .dark .modal-content {
703
+ background: #18181b;
704
+ border-color: #27272a;
705
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
706
+ }
707
+
708
+ .dark .input-field {
709
+ background: #18181b !important;
710
+ color: #f4f4f5 !important;
711
+ border-color: #27272a !important;
712
+ }
713
+
714
+ .dark .input-field:focus {
715
+ border-color: #ffffff !important;
716
+ }
717
+
718
+ .dark select.input-field option {
719
+ background: #18181b;
720
+ color: #f4f4f5;
721
+ }
722
+
723
+ .dark .bg-zinc-50\/30 {
724
+ background-color: rgba(39, 39, 42, 0.3) !important;
725
+ }
726
+
727
+ .dark .border-zinc-150 {
728
+ border-color: #27272a !important;
729
+ }
730
+
731
+ .dark .divide-zinc-100 > :not([hidden]) ~ :not([hidden]) {
732
+ border-color: #27272a !important;
733
+ }
734
+
735
+ .dark .badge-emerald {
736
+ background-color: rgba(16, 185, 129, 0.1) !important;
737
+ color: #34d399 !important;
738
+ border-color: rgba(16, 185, 129, 0.2) !important;
739
+ }
740
+
741
+ .dark .badge-amber {
742
+ background-color: rgba(245, 158, 11, 0.1) !important;
743
+ color: #fbbf24 !important;
744
+ border-color: rgba(245, 158, 11, 0.2) !important;
745
+ }
746
+
747
+ .dark .badge-rose {
748
+ background-color: rgba(244, 63, 94, 0.1) !important;
749
+ color: #f87171 !important;
750
+ border-color: rgba(244, 63, 94, 0.2) !important;
751
+ }
752
+
753
+ /* Custom dark mode overrides for buttons, active components, and alerts */
754
+ .dark .btn-primary {
755
+ background: #ffffff !important;
756
+ color: #09090b !important;
757
+ border-color: #ffffff !important;
758
+ }
759
+ .dark .btn-primary:hover {
760
+ background: #e4e4e7 !important;
761
+ border-color: #e4e4e7 !important;
762
+ }
763
+ .dark .btn-primary:disabled {
764
+ background: #27272a !important;
765
+ border-color: #27272a !important;
766
+ color: #71717a !important;
767
+ }
768
+
769
+ .dark .btn-ghost {
770
+ background: #18181b !important;
771
+ color: var(--text-secondary) !important;
772
+ border-color: var(--border-medium) !important;
773
+ }
774
+ .dark .btn-ghost:hover {
775
+ background: #27272a !important;
776
+ border-color: var(--border-strong) !important;
777
+ color: var(--text-primary) !important;
778
+ }
779
+
780
+ /* Background overrides for active elements */
781
+ .dark .bg-zinc-950 {
782
+ background-color: #ffffff !important;
783
+ color: #09090b !important;
784
+ }
785
+ .dark .border-zinc-950 {
786
+ border-color: #ffffff !important;
787
+ }
788
+ .dark .bg-zinc-900 {
789
+ background-color: #27272a !important;
790
+ }
791
+
792
+ /* Specific overrides for non-badge amber alerts in dark mode */
793
+ .dark .bg-amber-50\/50 {
794
+ background-color: rgba(245, 158, 11, 0.1) !important;
795
+ }
796
+ .dark .border-amber-200\/50 {
797
+ border-color: rgba(245, 158, 11, 0.2) !important;
798
+ }
799
+ .dark .text-amber-900 {
800
+ color: #fbbf24 !important;
801
+ }
802
+ .dark .text-amber-700 {
803
+ color: #f59e0b !important;
804
+ }
805
+
806
+ /* Accent icon/text color overrides */
807
+ .dark .text-emerald-600 {
808
+ color: #34d399 !important;
809
+ }
810
+ .dark .text-blue-600 {
811
+ color: #60a5fa !important;
812
+ }
813
+ .dark .text-rose-600 {
814
+ color: #f87171 !important;
815
+ }
816
+ .dark .text-amber-600 {
817
+ color: #fbbf24 !important;
818
+ }
819
+ .dark .border-rose-100 {
820
+ border-color: rgba(244, 63, 94, 0.2) !important;
821
+ }
822
+ .dark .hover\:bg-rose-50:hover {
823
+ background-color: rgba(244, 63, 94, 0.1) !important;
824
+ }
825
+ .dark .border-zinc-300 {
826
+ border-color: #52525b !important;
827
+ }
828
+
829
+ /* Dark mode flowing wave background */
830
+ .dark .ambient-bg,
831
+ .dark .mesh-bg {
832
+ background-color: #09090b !important;
833
+ background-image:
834
+ radial-gradient(circle at 50% 50%, rgba(6, 182, 212, 0.12) 0%, rgba(37, 99, 235, 0.06) 40%, transparent 70%),
835
+ radial-gradient(circle at 50% 50%, transparent 30%, #09090b 85%),
836
+ linear-gradient(rgba(34, 211, 238, 0.02) 1px, transparent 1px),
837
+ linear-gradient(90deg, rgba(34, 211, 238, 0.02) 1px, transparent 1px),
838
+ url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 600' width='100%' height='100%'><defs><filter id='glow' x='-50%' y='-50%' width='200%' height='200%'><feGaussianBlur in='SourceGraphic' stdDeviation='3' result='blur1'/><feGaussianBlur in='SourceGraphic' stdDeviation='10' result='blur2'/><feMerge><feMergeNode in='blur2'/><feMergeNode in='blur1'/><feMergeNode in='SourceGraphic'/></feMerge></filter><linearGradient id='grad-cyan' x1='0' y1='0' x2='0' y2='1'><stop offset='0%' stop-color='%2306b6d4' stop-opacity='0.16'/><stop offset='100%' stop-color='%2306b6d4' stop-opacity='0'/></linearGradient><linearGradient id='grad-blue' x1='0' y1='0' x2='0' y2='1'><stop offset='0%' stop-color='%232563eb' stop-opacity='0.12'/><stop offset='100%' stop-color='%232563eb' stop-opacity='0'/></linearGradient></defs><style>.wave-1{animation:flow-1 30s linear infinite, bob-1 10s ease-in-out infinite alternate}.wave-2{animation:flow-2 22s linear infinite, bob-2 7s ease-in-out infinite alternate}.wave-3{animation:flow-3 18s linear infinite, bob-3 8s ease-in-out infinite alternate}.wave-4{animation:flow-4 14s linear infinite, bob-4 5s ease-in-out infinite alternate}@keyframes flow-1{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(-1440px,0,0)}}@keyframes flow-2{0%{transform:translate3d(-1440px,0,0)}100%{transform:translate3d(0,0,0)}}@keyframes flow-3{0%{transform:translate3d(0,0,0)}100%{transform:translate3d(-1440px,0,0)}}@keyframes flow-4{0%{transform:translate3d(-1440px,0,0)}100%{transform:translate3d(0,0,0)}}@keyframes bob-1{0%{transform:translateY(-12px)}100%{transform:translateY(12px)}}@keyframes bob-2{0%{transform:translateY(9px)}100%{transform:translateY(-9px)}}@keyframes bob-3{0%{transform:translateY(-7px)}100%{transform:translateY(7px)}}@keyframes bob-4{0%{transform:translateY(6px)}100%{transform:translateY(-6px)}}</style><path class='wave-1' fill='url(%23grad-blue)' d='M-1440,300 C-1080,220 -720,380 -360,300 C0,220 360,380 720,300 C1080,220 1440,380 1800,300 C2160,220 2520,380 2880,300 L2880,600 L-1440,600 Z'/><path class='wave-2' fill='url(%23grad-cyan)' d='M-1440,320 C-1080,440 -720,200 -360,320 C0,440 360,200 720,320 C1080,440 1440,200 1800,320 C2160,450 2520,190 2880,320 L2880,600 L-1440,600 Z'/><path class='wave-3' stroke='%2322d3ee' stroke-width='2' fill='none' filter='url(%23glow)' opacity='0.75' d='M-1440,300 C-1080,240 -720,360 -360,300 C0,240 360,360 720,300 C1080,240 1440,360 1800,300 C2160,240 2520,360 2880,300'/><path class='wave-4' stroke='%233b82f6' stroke-width='1.25' fill='none' opacity='0.65' d='M-1440,315 C-1080,365 -720,265 -360,315 C0,365 360,265 720,315 C1080,365 1440,265 1800,315 C2160,365 2520,265 2880,315'/><path class='wave-3' stroke='%23ffffff' stroke-width='0.75' fill='none' opacity='0.85' filter='url(%23glow)' d='M-1440,295 C-1080,230 -720,370 -360,295 C0,230 360,370 720,295 C1080,230 1440,370 1800,295 C2160,230 2520,370 2880,295'/><path class='wave-1' stroke='%230891b2' stroke-width='1' fill='none' opacity='0.55' filter='url(%23glow)' d='M-1440,290 C-1080,190 -720,390 -360,290 C0,190 360,390 720,290 C1080,190 1440,390 1800,290 C2160,190 2520,390 2880,290'/></svg>") !important;
839
+ background-size: cover, cover, 80px 80px, 80px 80px, cover !important;
840
+ background-position: center !important;
841
+ background-repeat: no-repeat, no-repeat, repeat, repeat, no-repeat !important;
842
+ }
843
+
844
+ /* Kiosk header and footer translucent glass overlays */
845
+ .kiosk-header {
846
+ background-color: rgba(255, 255, 255, 0.45) !important;
847
+ backdrop-filter: blur(16px);
848
+ -webkit-backdrop-filter: blur(16px);
849
+ }
850
+ .dark .kiosk-header {
851
+ background-color: rgba(9, 9, 11, 0.45) !important;
852
+ backdrop-filter: blur(16px);
853
+ -webkit-backdrop-filter: blur(16px);
854
+ }
855
+
856
+ .kiosk-footer {
857
+ background-color: rgba(255, 255, 255, 0.45) !important;
858
+ backdrop-filter: blur(16px);
859
+ -webkit-backdrop-filter: blur(16px);
860
+ }
861
+ .dark .kiosk-footer {
862
+ background-color: rgba(9, 9, 11, 0.45) !important;
863
+ backdrop-filter: blur(16px);
864
+ -webkit-backdrop-filter: blur(16px);
865
+ }
866
+
867
+
868
+
869
+
frontend/app/kiosk/page.tsx CHANGED
@@ -6,20 +6,42 @@ import {
6
  Volume2, VolumeX, Clock as ClockIcon, Play, Wifi, Fingerprint, Shield
7
  } from "lucide-react";
8
  import { getBackendUrl } from "@/app/utils/api";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  export default function KioskPage() {
 
11
  const [kioskActive, setKioskActive] = useState(false);
12
  const [currentTime, setCurrentTime] = useState("");
13
  const [currentDate, setCurrentDate] = useState("");
14
  const [matchTime, setMatchTime] = useState("");
15
  const [scanning, setScanning] = useState(false);
16
- const [scanStatus, setScanStatus] = useState<"idle" | "success" | "spoof" | "unknown" | "maintenance" | "no_employees">("idle");
17
  const [scanResult, setScanResult] = useState<any>(null);
18
  const [scanFeedback, setScanFeedback] = useState<string | null>(null);
19
  const [cameraLabel] = useState("Main Entrance");
20
  const [voiceEnabled, setVoiceEnabled] = useState(true);
21
  const [isFullscreen, setIsFullscreen] = useState(false);
22
  const [profileImageError, setProfileImageError] = useState(false);
 
 
23
 
24
  const videoRef = useRef<HTMLVideoElement>(null);
25
  const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -31,7 +53,7 @@ export default function KioskPage() {
31
  useEffect(() => {
32
  const tick = () => {
33
  const now = new Date();
34
- setCurrentTime(now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }));
35
  setCurrentDate(now.toLocaleDateString([], { weekday: "long", year: "numeric", month: "long", day: "numeric" }));
36
  };
37
  tick();
@@ -39,6 +61,31 @@ export default function KioskPage() {
39
  return () => clearInterval(t);
40
  }, []);
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  const toggleFullscreen = () => {
43
  if (!document.fullscreenElement) {
44
  document.documentElement.requestFullscreen().catch(() => {});
@@ -68,7 +115,7 @@ export default function KioskPage() {
68
  intervalRef.current = setInterval(captureAndScan, 1000);
69
  } catch (err) {
70
  console.error(err);
71
- alert("Unable to access camera. Please check browser permissions and ensure no other application is using it.");
72
  }
73
  };
74
 
@@ -108,6 +155,7 @@ export default function KioskPage() {
108
  ctx.setTransform(1, 0, 0, 1, 0, 0);
109
 
110
  const base64 = canvas.toDataURL("image/jpeg", 0.82);
 
111
  setScanning(true);
112
  try {
113
  // Connect to the backend running at the configured URL
@@ -136,6 +184,27 @@ export default function KioskPage() {
136
  }
137
  };
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  const handleResult = (data: any) => {
140
  if (data.status === "success") {
141
  setProfileImageError(false);
@@ -148,6 +217,18 @@ export default function KioskPage() {
148
  console.error("Autoplay voice greeting failed:", err);
149
  });
150
  }
 
 
 
 
 
 
 
 
 
 
 
 
151
  } else if (data.status === "spoof_detected") {
152
  setScanStatus("spoof"); setScanResult(data);
153
  triggerCooldown(3000);
@@ -175,44 +256,38 @@ export default function KioskPage() {
175
  return (
176
  <div className="min-h-screen bg-[var(--bg-base)] text-[var(--text-primary)] flex flex-col relative select-none overflow-hidden font-sans">
177
  {/* Background Grid Mesh */}
178
- <div className="absolute inset-0 mesh-bg opacity-30 pointer-events-none" />
179
  <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[350px] bg-slate-500/5 blur-[120px] pointer-events-none rounded-full" />
180
 
181
  {/* ─── Top HUD Bar ─── */}
182
- <header className="relative z-30 h-16 border-b border-[var(--border-medium)] px-8 flex items-center justify-between bg-white/95 backdrop-blur-xl shadow-sm">
183
  {/* Brand info */}
184
- <div className="flex items-center gap-3">
185
- <div className="relative inline-flex items-center justify-center w-8.5 h-8.5 rounded-xl bg-slate-950 border border-slate-800/80 shadow-[0_0_10px_rgba(6,182,212,0.15)] overflow-hidden shrink-0">
186
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)]" />
187
- <div className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-emerald-500 border border-white z-20" />
188
- <svg viewBox="0 0 100 100" className="w-5.5 h-5.5 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
189
- <circle cx="50" cy="50" r="45" stroke="#1e293b" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
190
- <circle cx="50" cy="50" r="40" stroke="#0891b2" strokeWidth="1.5" strokeDasharray="30 15" className="animate-rotate-ring-reverse" style={{ opacity: 0.6 }} />
191
- <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#475569" strokeWidth="2.5" />
192
  <g className="animate-eye-lid">
193
- <circle cx="50" cy="50" r="22" fill="#0f172a" stroke="#0891b2" strokeWidth="2" />
194
  <circle cx="50" cy="50" r="7" fill="#22d3ee" className="animate-pupil" />
195
  </g>
196
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="2" className="animate-laser" />
197
  </svg>
198
  </div>
199
  <div>
200
- <h1 className="font-bold text-[13.5px] tracking-tight text-slate-800 leading-none">NetraID Kiosk</h1>
201
- <div className="flex items-center gap-1.5 mt-1">
202
- <Wifi className="w-2.5 h-2.5 text-emerald-500" />
203
- <p className="text-[9.5px] text-slate-500 font-mono uppercase tracking-wider">{cameraLabel}</p>
204
- </div>
205
  </div>
206
  </div>
207
 
208
  {/* Clock */}
209
- <div className="text-center absolute left-1/2 -translate-x-1/2">
210
- <p className="text-lg font-bold font-mono tracking-tight text-slate-800 leading-none tabular-nums">
211
  {currentTime || "00:00:00"}
212
  </p>
213
- <p className="text-[9.5px] text-slate-500 mt-1 uppercase tracking-wider font-mono">
214
- {currentDate}
215
- </p>
216
  </div>
217
 
218
  {/* Controls */}
@@ -220,10 +295,10 @@ export default function KioskPage() {
220
  <button
221
  onClick={() => setVoiceEnabled(!voiceEnabled)}
222
  title={voiceEnabled ? "Mute audio assistance" : "Enable audio assistance"}
223
- className={`p-2 rounded-xl border transition-all cursor-pointer ${
224
  voiceEnabled
225
  ? "bg-zinc-950 border-zinc-950 text-white"
226
- : "bg-slate-100 border-slate-200 text-slate-400"
227
  }`}
228
  >
229
  {voiceEnabled ? <Volume2 className="w-4 h-4" /> : <VolumeX className="w-4 h-4" />}
@@ -231,36 +306,41 @@ export default function KioskPage() {
231
  <button
232
  onClick={toggleFullscreen}
233
  title="Fullscreen toggle"
234
- className="p-2 rounded-xl bg-slate-100 border border-slate-200 text-slate-500 hover:text-slate-800 transition-all cursor-pointer"
235
  >
236
  {isFullscreen ? <Minimize className="w-4 h-4" /> : <Maximize className="w-4 h-4" />}
237
  </button>
238
  </div>
239
  </header>
240
 
241
- {/* ─── Main Area ─── */}
242
  <main className="flex-1 flex flex-col items-center justify-center p-6 relative z-10">
243
  <canvas ref={canvasRef} className="hidden" />
244
 
245
- {/* Video / Camera frame (Always mounted to prevent null refs) */}
246
- <div className="w-full max-w-3xl space-y-4 relative">
247
 
248
- {/* Pre-start overlay */}
249
  {!kioskActive && (
250
- <div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-[var(--bg-base)] text-center p-8 space-y-7 animate-fadeInUp">
251
- <div className="relative">
 
 
 
 
 
 
252
  {/* Subtle outer breathing ring */}
253
- <div className="absolute inset-[-12px] rounded-full bg-zinc-100 border border-zinc-200/60 animate-pulse animate-fade-in" />
254
 
255
  {/* Cybernetic blinking/glowing robot eye logo */}
256
- <div className="relative inline-flex items-center justify-center w-22 h-22 rounded-full bg-slate-950 border border-slate-800/80 shadow-[0_0_20px_rgba(6,182,212,0.25)] overflow-hidden shrink-0">
257
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.25)_0%,transparent_70%)]" />
258
- <svg viewBox="0 0 100 100" className="w-12 h-12 relative z-10" fill="none" xmlns="http://www.w3.org/2000/svg">
259
- <circle cx="50" cy="50" r="45" stroke="#1e293b" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
260
- <circle cx="50" cy="50" r="40" stroke="#0891b2" strokeWidth="1.5" strokeDasharray="30 15" className="animate-rotate-ring-reverse" style={{ opacity: 0.6 }} />
261
- <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#475569" strokeWidth="2.5" />
262
  <g className="animate-eye-lid">
263
- <circle cx="50" cy="50" r="22" fill="#0f172a" stroke="#0891b2" strokeWidth="2" />
264
  <circle cx="50" cy="50" r="7" fill="#22d3ee" className="animate-pupil" />
265
  </g>
266
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="2" className="animate-laser" />
@@ -268,25 +348,25 @@ export default function KioskPage() {
268
  </div>
269
  </div>
270
 
271
- <div className="space-y-2 max-w-sm">
272
- <h2 className="text-2xl font-black text-[var(--text-primary)] tracking-tight">Kiosk Scanner Offline</h2>
273
- <p className="text-sm text-[var(--text-secondary)] font-medium leading-relaxed">
274
- Initialize the secure terminal to start the live camera feed and begin biometric attendance recording.
275
  </p>
276
  </div>
277
 
278
  <button
279
  onClick={startKiosk}
280
- className="btn-primary h-11 px-8 text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2.5 shadow-md shadow-zinc-950/10 cursor-pointer"
281
  >
282
- <Play className="w-3.5 h-3.5 fill-current" />
283
- Start Scanner Terminal
284
  </button>
285
  </div>
286
  )}
287
 
288
  {/* Camera Frame Frame */}
289
- <div className="relative aspect-video rounded-3xl overflow-hidden bg-black border border-[var(--border-strong)] shadow-lg shadow-slate-200">
290
 
291
  {/* Native Video player (Centrally mounted) */}
292
  <video
@@ -300,14 +380,14 @@ export default function KioskPage() {
300
  <>
301
  <div className="scanner-laser" />
302
  <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
303
- <div className="relative w-60 h-60">
304
- <div className="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 border-zinc-950 rounded-tl-2xl" />
305
- <div className="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 border-zinc-950 rounded-tr-2xl" />
306
- <div className="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 border-zinc-950 rounded-bl-2xl" />
307
- <div className="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 border-zinc-950 rounded-br-2xl" />
308
 
309
- <div className="absolute inset-0 flex items-center justify-center">
310
- <span className="text-[10px] text-zinc-950 font-mono font-bold tracking-wider uppercase bg-white/95 px-4 py-2 rounded-full border border-zinc-200/80 shadow-sm">
311
  {scanFeedback || (scanning ? "Processing..." : "Center Face")}
312
  </span>
313
  </div>
@@ -318,61 +398,55 @@ export default function KioskPage() {
318
 
319
  {/* SUCCESS screen */}
320
  {kioskActive && scanStatus === "success" && (
321
- <div className="absolute inset-0 bg-white flex flex-col items-center justify-center p-8 text-center animate-fadeInUp">
322
- <div className="space-y-6 max-w-sm w-full animate-fade-in">
323
- {/* Initials Circle */}
324
- <div className="relative mx-auto w-24 h-24">
325
- {/* Ring animation */}
326
- <div className="absolute inset-[-6px] rounded-full border border-emerald-500/30 animate-ping" />
327
  {scanResult?.employee?.employee_id && !profileImageError ? (
328
  <img
329
  src={`${getBackendUrl().replace("/api/v1", "")}/uploads/${scanResult.employee.employee_id}/front.jpg`}
330
  alt={scanResult.employee.name}
331
- className="w-24 h-24 rounded-full object-cover border border-zinc-200 shadow-lg shadow-zinc-950/20"
332
  onError={() => setProfileImageError(true)}
333
  />
334
  ) : (
335
- <div className="w-24 h-24 rounded-full bg-slate-950 border border-slate-800 flex items-center justify-center text-white font-mono text-3xl font-extrabold shadow-lg shadow-zinc-950/20">
336
  {scanResult?.employee?.name
337
  ? scanResult.employee.name.split(" ").map((n: string) => n[0]).join("").substring(0, 2).toUpperCase()
338
  : "PK"}
339
- </div>
340
  )}
341
  </div>
342
 
343
- {/* Name and Location info */}
344
- <div className="space-y-2">
345
- <h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">
346
  {scanResult?.employee?.name || "Employee"}
347
  </h2>
348
-
349
- <div className="flex items-center justify-center gap-1.5 text-xs text-slate-500 font-mono uppercase font-semibold">
350
- <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
351
- <span>{cameraLabel}</span>
352
- </div>
353
-
354
- <p className="text-sm text-slate-700 font-semibold">
355
- {scanResult?.employee?.designation || "SDE-1"} ({scanResult?.employee?.employee_id || "int01"})
356
  </p>
357
  </div>
358
 
359
- {/* Divider */}
360
- <div className="w-12 h-px bg-slate-200 mx-auto" />
361
-
362
- {/* Real-time Clock & Matched Status */}
363
- <div className="space-y-4">
364
- <p className="text-2xl font-black font-mono text-slate-900 tracking-tight tabular-nums">
365
- {matchTime || "00:00:00 AM"}
366
  </p>
367
 
368
- <div className="flex flex-col items-center gap-1.5 mt-1">
369
- <span className="inline-flex items-center gap-1.5 px-4.5 py-1.5 rounded-full bg-emerald-50 border border-emerald-200 text-emerald-700 font-mono text-xs font-bold uppercase tracking-wider shadow-sm">
370
- <UserCheck className="w-3.5 h-3.5" />
371
  Matched
372
  </span>
 
 
 
 
 
 
373
  {scanResult?.confidence && (
374
- <p className="text-[10px] text-slate-400 font-mono">
375
- Match: <span className="font-bold text-slate-600">{(scanResult.confidence * 100).toFixed(1)}%</span> · Liveness: <span className="font-bold text-emerald-600">{(scanResult.liveness_score * 100).toFixed(1)}% Real</span>
376
  </p>
377
  )}
378
  </div>
@@ -381,83 +455,153 @@ export default function KioskPage() {
381
  </div>
382
  )}
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  {/* SPOOF screen */}
385
  {kioskActive && scanStatus === "spoof" && (
386
- <div className="absolute inset-0 bg-red-50/98 flex flex-col items-center justify-center p-8 text-center animate-fadeInUp">
387
- <div className="relative mb-5">
388
- <div className="w-18 h-18 rounded-full bg-rose-500/10 border border-rose-500 flex items-center justify-center">
389
- <ShieldAlert className="w-8 h-8 text-rose-600" />
390
  </div>
 
 
 
 
 
391
  </div>
392
- <h2 className="text-2xl font-bold text-slate-800 tracking-tight">Security Check Failed</h2>
393
- <p className="text-sm text-rose-600 mt-1 font-semibold">{scanResult?.message}</p>
394
- <p className="text-[10px] text-slate-400 font-mono mt-4">
395
- Liveness Prob: {scanResult?.liveness_score?.toFixed(3)} · Lockout applied
396
- </p>
397
  </div>
398
  )}
399
 
400
  {/* UNKNOWN screen */}
401
  {kioskActive && scanStatus === "unknown" && (
402
- <div className="absolute inset-0 bg-amber-50/98 flex flex-col items-center justify-center p-8 text-center animate-fadeInUp">
403
- <div className="relative mb-5">
404
- <div className="w-18 h-18 rounded-full bg-amber-500/10 border border-amber-500 flex items-center justify-center">
405
- <HelpCircle className="w-8 h-8 text-amber-600" />
406
  </div>
 
 
407
  </div>
408
- <h2 className="text-xl font-bold text-slate-800 tracking-tight">Unknown Identity</h2>
409
- <p className="text-xs text-amber-700 mt-1.5 font-medium">{scanResult?.message || "Face not registered on company database."}</p>
410
- <p className="text-[10px] text-slate-400 font-mono mt-3">
411
- Match Score: {scanResult?.confidence?.toFixed(3)}
412
- </p>
413
  </div>
414
  )}
415
 
416
  {/* MAINTENANCE screen */}
417
  {kioskActive && scanStatus === "maintenance" && (
418
- <div className="absolute inset-0 bg-zinc-50/98 flex flex-col items-center justify-center p-8 text-center animate-fadeInUp">
419
- <div className="relative mb-5">
420
- <div className="w-18 h-18 rounded-full bg-zinc-950/10 border border-zinc-950 flex items-center justify-center animate-pulse">
421
- <ShieldAlert className="w-8 h-8 text-zinc-900" />
422
  </div>
423
- </div>
424
- <h2 className="text-2xl font-bold text-slate-800 tracking-tight">System Under Maintenance</h2>
425
- <p className="text-xs text-zinc-650 mt-1.5 font-semibold max-w-xs leading-relaxed">
426
- {scanResult?.message || "Biometric logs and active attendance scans are temporarily suspended."}
427
- </p>
428
- <div className="mt-4">
429
- <span className="inline-flex items-center gap-1.5 px-4.5 py-1.5 rounded-full bg-zinc-100 border border-zinc-200 text-zinc-800 font-mono text-xs font-bold uppercase tracking-wider shadow-sm">
430
- Offline Standby
431
- </span>
432
  </div>
433
  </div>
434
  )}
435
 
436
  {/* NO_EMPLOYEES screen */}
437
  {kioskActive && scanStatus === "no_employees" && (
438
- <div className="absolute inset-0 bg-amber-50/98 flex flex-col items-center justify-center p-8 text-center animate-fadeInUp">
439
- <div className="relative mb-5">
440
- <div className="w-18 h-18 rounded-full bg-amber-500/10 border border-amber-500 flex items-center justify-center">
441
- <UserCheck className="w-8 h-8 text-amber-600" />
442
  </div>
 
 
 
 
443
  </div>
444
- <h2 className="text-2xl font-bold text-slate-800 tracking-tight">No Registered Employees</h2>
445
- <p className="text-sm text-amber-700 mt-2.5 font-semibold max-w-xs leading-relaxed">
446
- {scanResult?.message || "Please add the employee"}
447
- </p>
448
  </div>
449
  )}
450
 
451
  {/* Frame Info HUD Overlay */}
452
  {kioskActive && (
453
  <div className="absolute bottom-4 left-4 right-4 flex items-center justify-between pointer-events-none z-10">
454
- <div className="flex items-center gap-1.5 text-[9px] font-mono font-bold text-slate-500 bg-white/90 border border-slate-200 shadow-sm px-3 py-1.5 rounded-xl">
455
  <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
456
  ONLINE · {cameraLabel.toUpperCase()}
457
  </div>
458
  <button
459
  onClick={(e) => { e.stopPropagation(); stopKiosk(); }}
460
- className="pointer-events-auto text-[10px] font-bold text-rose-600 bg-white border border-rose-200 hover:bg-rose-50 shadow-sm px-3.5 py-1.5 rounded-xl transition-all cursor-pointer"
461
  >
462
  Disable Camera
463
  </button>
@@ -467,17 +611,17 @@ export default function KioskPage() {
467
 
468
  {/* Active bottom status bar indicators */}
469
  {kioskActive && (
470
- <div className="flex items-center justify-center gap-6 text-[10px] font-mono text-slate-500 font-semibold">
471
  <div className="flex items-center gap-1.5">
472
- <div className={`w-1.5 h-1.5 rounded-full ${scanning ? "bg-zinc-950 animate-pulse" : "bg-slate-400"}`} />
473
- <span>{scanning ? "SCANNING..." : "ACTIVE STANDBY"}</span>
474
  </div>
475
- <div className="flex items-center gap-1.5">
476
- <Shield className="w-3.5 h-3.5 text-zinc-700" />
477
- <span>Anti-Spoofing Protocol</span>
478
  </div>
479
- <div className="flex items-center gap-1.5">
480
- <Camera className="w-3.5 h-3.5 text-zinc-700" />
481
  <span>Auto-indexing</span>
482
  </div>
483
  </div>
@@ -486,9 +630,9 @@ export default function KioskPage() {
486
  </main>
487
 
488
  {/* ─── Footer ─── */}
489
- <footer className="relative z-10 h-10 border-t border-[var(--border-medium)] flex items-center justify-center bg-white shadow-inner">
490
- <p className="text-[9.5px] font-mono font-bold text-slate-400 tracking-wider">
491
- NETRAID SECURE TERMINAL GATEWAY v1.0.0 · MOCKED OFFLINE ENGINE
492
  </p>
493
  </footer>
494
  </div>
 
6
  Volume2, VolumeX, Clock as ClockIcon, Play, Wifi, Fingerprint, Shield
7
  } from "lucide-react";
8
  import { getBackendUrl } from "@/app/utils/api";
9
+ import { useToast } from "@/app/utils/toast";
10
+
11
+ function formatTime12h(timeStr: string | null | undefined): string {
12
+ if (!timeStr) return "";
13
+ if (timeStr.includes("AM") || timeStr.includes("PM")) return timeStr;
14
+
15
+ const parts = timeStr.split(":");
16
+ if (parts.length < 2) return timeStr;
17
+
18
+ const hr = parseInt(parts[0], 10);
19
+ const mn = parseInt(parts[1], 10);
20
+ const sc = parts[2] ? parseInt(parts[2], 10) : 0;
21
+
22
+ const suffix = hr >= 12 ? "PM" : "AM";
23
+ const hour12 = hr % 12 || 12;
24
+ const pad = (num: number) => String(num).padStart(2, "0");
25
+
26
+ return `${pad(hour12)}:${pad(mn)}:${pad(sc)} ${suffix}`;
27
+ }
28
 
29
  export default function KioskPage() {
30
+ const { toast } = useToast();
31
  const [kioskActive, setKioskActive] = useState(false);
32
  const [currentTime, setCurrentTime] = useState("");
33
  const [currentDate, setCurrentDate] = useState("");
34
  const [matchTime, setMatchTime] = useState("");
35
  const [scanning, setScanning] = useState(false);
36
+ const [scanStatus, setScanStatus] = useState<"idle" | "success" | "spoof" | "unknown" | "maintenance" | "no_employees" | "ask_checkout" | "locked">("idle");
37
  const [scanResult, setScanResult] = useState<any>(null);
38
  const [scanFeedback, setScanFeedback] = useState<string | null>(null);
39
  const [cameraLabel] = useState("Main Entrance");
40
  const [voiceEnabled, setVoiceEnabled] = useState(true);
41
  const [isFullscreen, setIsFullscreen] = useState(false);
42
  const [profileImageError, setProfileImageError] = useState(false);
43
+ const [lastImage, setLastImage] = useState<string | null>(null);
44
+ const [engineMode, setEngineMode] = useState<string>("LOADING ENGINE...");
45
 
46
  const videoRef = useRef<HTMLVideoElement>(null);
47
  const canvasRef = useRef<HTMLCanvasElement>(null);
 
53
  useEffect(() => {
54
  const tick = () => {
55
  const now = new Date();
56
+ setCurrentTime(now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true }));
57
  setCurrentDate(now.toLocaleDateString([], { weekday: "long", year: "numeric", month: "long", day: "numeric" }));
58
  };
59
  tick();
 
61
  return () => clearInterval(t);
62
  }, []);
63
 
64
+ // Fetch engine state dynamically on mount
65
+ useEffect(() => {
66
+ const checkEngine = async () => {
67
+ try {
68
+ const url = `${getBackendUrl().replace("/api/v1", "")}/health`;
69
+ const res = await fetch(url);
70
+ if (res.ok) {
71
+ const data = await res.json();
72
+ if (data.mock_mode) {
73
+ setEngineMode("MOCKED OFFLINE ENGINE");
74
+ } else {
75
+ setEngineMode("REAL BIOMETRIC ENGINE");
76
+ }
77
+ } else {
78
+ setEngineMode("REAL BIOMETRIC ENGINE");
79
+ }
80
+ } catch (err) {
81
+ setEngineMode("REAL BIOMETRIC ENGINE");
82
+ }
83
+ };
84
+ checkEngine();
85
+ const interval = setInterval(checkEngine, 10000);
86
+ return () => clearInterval(interval);
87
+ }, []);
88
+
89
  const toggleFullscreen = () => {
90
  if (!document.fullscreenElement) {
91
  document.documentElement.requestFullscreen().catch(() => {});
 
115
  intervalRef.current = setInterval(captureAndScan, 1000);
116
  } catch (err) {
117
  console.error(err);
118
+ toast.error("Unable to access camera. Please check browser permissions and ensure no other application is using it.");
119
  }
120
  };
121
 
 
155
  ctx.setTransform(1, 0, 0, 1, 0, 0);
156
 
157
  const base64 = canvas.toDataURL("image/jpeg", 0.82);
158
+ setLastImage(base64);
159
  setScanning(true);
160
  try {
161
  // Connect to the backend running at the configured URL
 
184
  }
185
  };
186
 
187
+ const confirmCheckout = async () => {
188
+ if (!lastImage) return;
189
+ setScanning(true);
190
+ try {
191
+ const url = `${getBackendUrl()}/kiosk/scan`;
192
+ const res = await fetch(url, {
193
+ method: "POST",
194
+ headers: { "Content-Type": "application/json" },
195
+ body: JSON.stringify({ image: lastImage, camera: cameraLabel, confirm_checkout: true })
196
+ });
197
+ if (!res.ok) throw new Error("Checkout confirmation failed");
198
+ const data = await res.json();
199
+ handleResult(data);
200
+ } catch (err) {
201
+ console.error("Checkout confirmation error:", err);
202
+ setScanFeedback("Confirmation error");
203
+ } finally {
204
+ setScanning(false);
205
+ }
206
+ };
207
+
208
  const handleResult = (data: any) => {
209
  if (data.status === "success") {
210
  setProfileImageError(false);
 
217
  console.error("Autoplay voice greeting failed:", err);
218
  });
219
  }
220
+ } else if (data.status === "ask_checkout") {
221
+ setProfileImageError(false);
222
+ setScanStatus("ask_checkout"); setScanResult(data);
223
+ if (voiceEnabled && data.tts_url) {
224
+ const backendBaseUrl = getBackendUrl().replace("/api/v1", "");
225
+ new Audio(`${backendBaseUrl}${data.tts_url}`).play().catch((err) => {
226
+ console.error("Autoplay voice greeting failed:", err);
227
+ });
228
+ }
229
+ } else if (data.status === "locked") {
230
+ setScanStatus("locked"); setScanResult(data);
231
+ triggerCooldown(4000);
232
  } else if (data.status === "spoof_detected") {
233
  setScanStatus("spoof"); setScanResult(data);
234
  triggerCooldown(3000);
 
256
  return (
257
  <div className="min-h-screen bg-[var(--bg-base)] text-[var(--text-primary)] flex flex-col relative select-none overflow-hidden font-sans">
258
  {/* Background Grid Mesh */}
259
+ <div className="absolute inset-0 mesh-bg pointer-events-none" />
260
  <div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[350px] bg-slate-500/5 blur-[120px] pointer-events-none rounded-full" />
261
 
262
  {/* ─── Top HUD Bar ─── */}
263
+ <header className="relative z-30 h-14 border-b border-[var(--border-medium)] px-6 flex items-center justify-between kiosk-header">
264
  {/* Brand info */}
265
+ <div className="flex items-center gap-2.5 animate-fadeInUp">
266
+ <div className="relative inline-flex items-center justify-center w-9 h-9 rounded-lg bg-slate-900 border border-slate-700/80 shadow-[0_0_12px_rgba(6,182,212,0.2)] overflow-hidden shrink-0">
267
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)] animate-pulse" />
268
+ <div className="absolute -bottom-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-emerald-500 border border-white z-20" />
269
+ <svg viewBox="0 0 100 100" className="w-6 h-6 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
270
+ <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
271
+ <circle cx="50" cy="50" r="40" stroke="#06b6d4" strokeWidth="1.5" strokeDasharray="30 15" className="animate-rotate-ring-reverse" style={{ opacity: 0.8 }} />
272
+ <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
273
  <g className="animate-eye-lid">
274
+ <circle cx="50" cy="50" r="22" fill="#1e293b" stroke="#06b6d4" strokeWidth="2" />
275
  <circle cx="50" cy="50" r="7" fill="#22d3ee" className="animate-pupil" />
276
  </g>
277
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="2" className="animate-laser" />
278
  </svg>
279
  </div>
280
  <div>
281
+ <h1 className="font-bold text-[15px] tracking-tight text-[var(--text-primary)] leading-none">NetraID Kiosk</h1>
282
+ <p className="text-[9px] text-[var(--text-muted)] font-mono mt-0.5 uppercase tracking-wider">{cameraLabel}</p>
 
 
 
283
  </div>
284
  </div>
285
 
286
  {/* Clock */}
287
+ <div className="text-center absolute left-1/2 -translate-y-1/2">
288
+ <p className="text-base font-bold font-mono tracking-tight text-[var(--text-primary)] leading-none tabular-nums">
289
  {currentTime || "00:00:00"}
290
  </p>
 
 
 
291
  </div>
292
 
293
  {/* Controls */}
 
295
  <button
296
  onClick={() => setVoiceEnabled(!voiceEnabled)}
297
  title={voiceEnabled ? "Mute audio assistance" : "Enable audio assistance"}
298
+ className={`p-1.5 rounded-lg border transition-all cursor-pointer ${
299
  voiceEnabled
300
  ? "bg-zinc-950 border-zinc-950 text-white"
301
+ : "bg-slate-55 border-slate-200 text-slate-400"
302
  }`}
303
  >
304
  {voiceEnabled ? <Volume2 className="w-4 h-4" /> : <VolumeX className="w-4 h-4" />}
 
306
  <button
307
  onClick={toggleFullscreen}
308
  title="Fullscreen toggle"
309
+ className="p-1.5 rounded-lg bg-slate-55 border border-slate-200 text-slate-500 hover:text-slate-800 transition-all cursor-pointer"
310
  >
311
  {isFullscreen ? <Minimize className="w-4 h-4" /> : <Maximize className="w-4 h-4" />}
312
  </button>
313
  </div>
314
  </header>
315
 
 
316
  <main className="flex-1 flex flex-col items-center justify-center p-6 relative z-10">
317
  <canvas ref={canvasRef} className="hidden" />
318
 
319
+ {/* Video / Camera frame container */}
320
+ <div className="w-full max-w-3xl relative">
321
 
322
+ {/* Pre-start offline glass card */}
323
  {!kioskActive && (
324
+ <div className="w-full max-w-2xl mx-auto glass-overlay border border-[var(--border-medium)] rounded-3xl text-center p-12 aspect-video flex flex-col items-center justify-center space-y-6 animate-fadeInUp shadow-[0_8px_32px_rgba(0,0,0,0.03)] relative overflow-hidden">
325
+ <div className="relative flex flex-col items-center">
326
+ {/* Status Badge */}
327
+ <div className="mb-4 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-rose-500/10 border border-rose-500/20 text-rose-600 font-mono text-[9px] font-bold uppercase tracking-wider">
328
+ <span className="w-1.5 h-1.5 rounded-full bg-rose-500 animate-pulse" />
329
+ Terminal Offline
330
+ </div>
331
+
332
  {/* Subtle outer breathing ring */}
333
+ <div className="absolute inset-[-6px] rounded-full bg-cyan-500/5 border border-cyan-500/10 animate-pulse" />
334
 
335
  {/* Cybernetic blinking/glowing robot eye logo */}
336
+ <div className="relative inline-flex items-center justify-center w-20 h-20 rounded-full bg-[var(--bg-surface)] border border-cyan-500/30 dark:border-cyan-500/40 shadow-[0_0_20px_rgba(6,182,212,0.15)] overflow-hidden shrink-0">
337
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.1)_0%,transparent_70%)]" />
338
+ <svg viewBox="0 0 100 100" className="w-11 h-11 relative z-10" fill="none" xmlns="http://www.w3.org/2000/svg">
339
+ <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
340
+ <circle cx="50" cy="50" r="40" stroke="#06b6d4" strokeWidth="1.5" strokeDasharray="30 15" className="animate-rotate-ring-reverse" style={{ opacity: 0.8 }} />
341
+ <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
342
  <g className="animate-eye-lid">
343
+ <circle cx="50" cy="50" r="22" fill="#1e293b" stroke="#06b6d4" strokeWidth="2" />
344
  <circle cx="50" cy="50" r="7" fill="#22d3ee" className="animate-pupil" />
345
  </g>
346
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="2" className="animate-laser" />
 
348
  </div>
349
  </div>
350
 
351
+ <div className="space-y-1 max-w-sm mx-auto">
352
+ <h2 className="text-xl font-extrabold text-[var(--text-primary)] tracking-tight">Kiosk Offline</h2>
353
+ <p className="text-xs text-[var(--text-muted)] leading-relaxed">
354
+ Start the camera terminal to begin face biometric logging.
355
  </p>
356
  </div>
357
 
358
  <button
359
  onClick={startKiosk}
360
+ className="btn-primary h-10 px-8 text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2 cursor-pointer shadow-md hover:scale-[1.02] active:scale-[0.98] transition-all"
361
  >
362
+ <Play className="w-4 h-4 fill-current" />
363
+ Start Scanner
364
  </button>
365
  </div>
366
  )}
367
 
368
  {/* Camera Frame Frame */}
369
+ <div className={`relative aspect-video rounded-2xl overflow-hidden bg-black border border-zinc-100 shadow-sm ${kioskActive ? "block" : "hidden"}`}>
370
 
371
  {/* Native Video player (Centrally mounted) */}
372
  <video
 
380
  <>
381
  <div className="scanner-laser" />
382
  <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
383
+ <div className="relative w-48 h-48">
384
+ <div className="absolute top-0 left-0 w-6 h-6 border-t-2 border-l-2 border-zinc-950 rounded-tl-xl" />
385
+ <div className="absolute top-0 right-0 w-6 h-6 border-t-2 border-r-2 border-zinc-950 rounded-tr-xl" />
386
+ <div className="absolute bottom-0 left-0 w-6 h-6 border-b-2 border-l-2 border-zinc-950 rounded-bl-xl" />
387
+ <div className="absolute bottom-0 right-0 w-6 h-6 border-b-2 border-r-2 border-zinc-950 rounded-br-xl" />
388
 
389
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
390
+ <span className="text-[9px] text-[var(--text-primary)] font-mono font-bold tracking-wider uppercase bg-[var(--bg-surface)] px-3 py-1.5 rounded border border-[var(--border-medium)] shadow-2xs">
391
  {scanFeedback || (scanning ? "Processing..." : "Center Face")}
392
  </span>
393
  </div>
 
398
 
399
  {/* SUCCESS screen */}
400
  {kioskActive && scanStatus === "success" && (
401
+ <div className="absolute inset-0 glass-overlay flex flex-col items-center justify-center p-6 text-center animate-fadeInUp">
402
+ <div className="space-y-4 max-w-xs w-full animate-fade-in">
403
+ {/* Profile Image / Initials */}
404
+ <div className="mx-auto w-20 h-20 rounded-full overflow-hidden border border-zinc-150 shadow-sm bg-zinc-55 flex items-center justify-center">
 
 
405
  {scanResult?.employee?.employee_id && !profileImageError ? (
406
  <img
407
  src={`${getBackendUrl().replace("/api/v1", "")}/uploads/${scanResult.employee.employee_id}/front.jpg`}
408
  alt={scanResult.employee.name}
409
+ className="w-full h-full object-cover"
410
  onError={() => setProfileImageError(true)}
411
  />
412
  ) : (
413
+ <span className="text-zinc-700 font-bold text-2xl">
414
  {scanResult?.employee?.name
415
  ? scanResult.employee.name.split(" ").map((n: string) => n[0]).join("").substring(0, 2).toUpperCase()
416
  : "PK"}
417
+ </span>
418
  )}
419
  </div>
420
 
421
+ {/* Name and Designation */}
422
+ <div className="space-y-0.5">
423
+ <h2 className="text-lg font-bold text-zinc-900 tracking-tight">
424
  {scanResult?.employee?.name || "Employee"}
425
  </h2>
426
+ <p className="text-[11px] text-zinc-400 font-medium">
427
+ {scanResult?.employee?.designation || "Employee"} ({scanResult?.employee?.employee_id})
 
 
 
 
 
 
428
  </p>
429
  </div>
430
 
431
+ {/* Clock & Status */}
432
+ <div className="space-y-2.5 pt-2">
433
+ <p className="text-xl font-bold font-mono text-[var(--text-primary)] tracking-tight tabular-nums">
434
+ {matchTime}
 
 
 
435
  </p>
436
 
437
+ <div className="flex flex-col items-center gap-1">
438
+ <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-emerald-700 font-mono text-[9px] font-bold uppercase tracking-wider">
 
439
  Matched
440
  </span>
441
+ {scanResult?.attendance?.working_hours > 0 && (
442
+ <div className="mt-1 px-3 py-1 bg-zinc-50 border border-zinc-200 rounded-lg">
443
+ <p className="text-[9px] text-zinc-400 font-bold uppercase tracking-wider">Time Worked Today</p>
444
+ <p className="text-xs font-bold text-zinc-755 font-mono mt-0.5">{scanResult.attendance.working_hours.toFixed(2)} hours</p>
445
+ </div>
446
+ )}
447
  {scanResult?.confidence && (
448
+ <p className="text-[8.5px] text-zinc-400 font-mono mt-1">
449
+ Confidence: {(scanResult.confidence * 100).toFixed(0)}% · {(scanResult.liveness_score * 100).toFixed(0)}% Real
450
  </p>
451
  )}
452
  </div>
 
455
  </div>
456
  )}
457
 
458
+ {/* ASK_CHECKOUT screen */}
459
+ {kioskActive && scanStatus === "ask_checkout" && (
460
+ <div className="absolute inset-0 glass-overlay flex flex-col items-center justify-center p-6 text-center animate-fadeInUp z-30">
461
+ <div className="space-y-4 max-w-sm w-full animate-fade-in">
462
+ <div className="mx-auto w-20 h-20 rounded-full overflow-hidden border border-zinc-150 shadow-sm bg-zinc-55 flex items-center justify-center">
463
+ {scanResult?.employee?.employee_id && !profileImageError ? (
464
+ <img
465
+ src={`${getBackendUrl().replace("/api/v1", "")}/uploads/${scanResult.employee.employee_id}/front.jpg`}
466
+ alt={scanResult.employee.name}
467
+ className="w-full h-full object-cover"
468
+ onError={() => setProfileImageError(true)}
469
+ />
470
+ ) : (
471
+ <span className="text-zinc-700 font-bold text-2xl">
472
+ {scanResult?.employee?.name
473
+ ? scanResult.employee.name.split(" ").map((n: string) => n[0]).join("").substring(0, 2).toUpperCase()
474
+ : "PK"}
475
+ </span>
476
+ )}
477
+ </div>
478
+
479
+ <div className="space-y-1">
480
+ <h2 className="text-lg font-bold text-zinc-900 tracking-tight">
481
+ {scanResult?.employee?.name || "Employee"}
482
+ </h2>
483
+ <p className="text-[11.5px] text-zinc-500 font-medium">
484
+ Already checked in at <span className="font-mono font-bold text-zinc-750">{formatTime12h(scanResult?.attendance?.check_in)}</span>
485
+ </p>
486
+ </div>
487
+
488
+ <div className="bg-zinc-50 border border-zinc-200 rounded-xl p-2.5 max-w-[200px] mx-auto">
489
+ <p className="text-[9px] text-zinc-450 font-bold uppercase tracking-wider">Working Hours So Far</p>
490
+ <p className="text-sm font-extrabold text-zinc-850 font-mono mt-0.5">
491
+ {scanResult?.working_hours_so_far?.toFixed(2)} hours
492
+ </p>
493
+ </div>
494
+
495
+ <div className="space-y-2.5 pt-1.5">
496
+ <p className="text-[12px] font-bold text-[var(--text-primary)]">Do you want to Check Out?</p>
497
+ <div className="flex items-center justify-center gap-2.5">
498
+ <button
499
+ onClick={confirmCheckout}
500
+ disabled={scanning}
501
+ className="px-5 py-2 rounded-xl bg-zinc-950 hover:bg-zinc-800 text-white text-[12px] font-bold shadow-sm cursor-pointer transition-all disabled:opacity-50"
502
+ >
503
+ Yes, Check Out
504
+ </button>
505
+ <button
506
+ onClick={() => {
507
+ setScanStatus("idle");
508
+ setScanResult(null);
509
+ }}
510
+ disabled={scanning}
511
+ className="px-5 py-2 rounded-xl bg-zinc-100 hover:bg-zinc-200 text-zinc-700 text-[12px] font-bold border border-zinc-200 cursor-pointer transition-all disabled:opacity-50"
512
+ >
513
+ No
514
+ </button>
515
+ </div>
516
+ </div>
517
+ </div>
518
+ </div>
519
+ )}
520
+
521
+ {/* LOCKED screen */}
522
+ {kioskActive && scanStatus === "locked" && (
523
+ <div className="absolute inset-0 glass-overlay flex flex-col items-center justify-center p-6 text-center animate-fadeInUp z-30">
524
+ <div className="space-y-3.5 max-w-xs animate-fade-in">
525
+ <div className="w-12 h-12 rounded-full bg-rose-500/10 border border-rose-500/20 flex items-center justify-center mx-auto text-rose-600">
526
+ <ShieldAlert className="w-5 h-5" />
527
+ </div>
528
+ <h2 className="text-base font-bold text-zinc-900 tracking-tight">Attendance Locked</h2>
529
+ <p className="text-xs text-zinc-500 leading-relaxed font-medium">
530
+ {scanResult?.message || "Your attendance is locked for today. Emergency re-entry must be approved by an Admin."}
531
+ </p>
532
+ </div>
533
+ </div>
534
+ )}
535
+
536
  {/* SPOOF screen */}
537
  {kioskActive && scanStatus === "spoof" && (
538
+ <div className="absolute inset-0 glass-overlay flex flex-col items-center justify-center p-6 text-center animate-fadeInUp">
539
+ <div className="space-y-3 max-w-xs animate-fade-in">
540
+ <div className="w-12 h-12 rounded-full bg-rose-500/10 border border-rose-500/20 flex items-center justify-center mx-auto text-rose-600">
541
+ <ShieldAlert className="w-5 h-5" />
542
  </div>
543
+ <h2 className="text-base font-bold text-zinc-900 tracking-tight">Verification Denied</h2>
544
+ <p className="text-xs text-rose-600 font-medium leading-relaxed">{scanResult?.message}</p>
545
+ <p className="text-[8.5px] text-zinc-450 font-mono">
546
+ Liveness score: {scanResult?.liveness_score?.toFixed(3)}
547
+ </p>
548
  </div>
 
 
 
 
 
549
  </div>
550
  )}
551
 
552
  {/* UNKNOWN screen */}
553
  {kioskActive && scanStatus === "unknown" && (
554
+ <div className="absolute inset-0 glass-overlay flex flex-col items-center justify-center p-6 text-center animate-fadeInUp">
555
+ <div className="space-y-3 max-w-xs animate-fade-in">
556
+ <div className="w-12 h-12 rounded-full bg-amber-500/10 border border-amber-500/20 flex items-center justify-center mx-auto text-amber-600">
557
+ <HelpCircle className="w-5 h-5" />
558
  </div>
559
+ <h2 className="text-base font-bold text-zinc-900 tracking-tight">Not Recognized</h2>
560
+ <p className="text-xs text-zinc-500 leading-relaxed">{scanResult?.message || "Face not registered on system database."}</p>
561
  </div>
 
 
 
 
 
562
  </div>
563
  )}
564
 
565
  {/* MAINTENANCE screen */}
566
  {kioskActive && scanStatus === "maintenance" && (
567
+ <div className="absolute inset-0 glass-overlay flex flex-col items-center justify-center p-6 text-center animate-fadeInUp">
568
+ <div className="space-y-3 max-w-xs animate-fade-in">
569
+ <div className="w-12 h-12 rounded-full bg-zinc-100 border border-zinc-200/50 flex items-center justify-center mx-auto text-zinc-800">
570
+ <ShieldAlert className="w-5 h-5" />
571
  </div>
572
+ <h2 className="text-base font-bold text-zinc-900 tracking-tight">Kiosk Offline</h2>
573
+ <p className="text-xs text-zinc-500 leading-relaxed">
574
+ {scanResult?.message || "Biometric scans are temporarily suspended."}
575
+ </p>
 
 
 
 
 
576
  </div>
577
  </div>
578
  )}
579
 
580
  {/* NO_EMPLOYEES screen */}
581
  {kioskActive && scanStatus === "no_employees" && (
582
+ <div className="absolute inset-0 glass-overlay flex flex-col items-center justify-center p-6 text-center animate-fadeInUp">
583
+ <div className="space-y-3 max-w-xs animate-fade-in">
584
+ <div className="w-12 h-12 rounded-full bg-amber-500/10 border border-amber-500/20 flex items-center justify-center mx-auto text-amber-600">
585
+ <UserCheck className="w-5 h-5" />
586
  </div>
587
+ <h2 className="text-base font-bold text-zinc-900 tracking-tight">Setup Required</h2>
588
+ <p className="text-xs text-zinc-555 leading-relaxed">
589
+ {scanResult?.message || "Please add employees to the system first."}
590
+ </p>
591
  </div>
 
 
 
 
592
  </div>
593
  )}
594
 
595
  {/* Frame Info HUD Overlay */}
596
  {kioskActive && (
597
  <div className="absolute bottom-4 left-4 right-4 flex items-center justify-between pointer-events-none z-10">
598
+ <div className="flex items-center gap-1.5 text-[9px] font-mono font-bold text-[var(--text-muted)] bg-[var(--bg-surface)] border border-[var(--border-medium)] shadow-2xs px-3 py-1.5 rounded-lg">
599
  <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
600
  ONLINE · {cameraLabel.toUpperCase()}
601
  </div>
602
  <button
603
  onClick={(e) => { e.stopPropagation(); stopKiosk(); }}
604
+ className="pointer-events-auto text-[9.5px] font-semibold text-rose-500 bg-[var(--bg-surface)] border border-rose-500/20 hover:bg-rose-500/10 shadow-2xs px-3 py-1 rounded-lg transition-all cursor-pointer"
605
  >
606
  Disable Camera
607
  </button>
 
611
 
612
  {/* Active bottom status bar indicators */}
613
  {kioskActive && (
614
+ <div className="flex items-center justify-center gap-5 text-[9px] font-mono text-slate-400 font-bold uppercase tracking-wider">
615
  <div className="flex items-center gap-1.5">
616
+ <div className={`w-1.5 h-1.5 rounded-full ${scanning ? "bg-zinc-950 animate-pulse" : "bg-zinc-350"}`} />
617
+ <span>{scanning ? "Scanning" : "Standby"}</span>
618
  </div>
619
+ <span className="text-zinc-200">|</span>
620
+ <div className="flex items-center gap-1">
621
+ <span>Anti-Spoof Active</span>
622
  </div>
623
+ <span className="text-zinc-200">|</span>
624
+ <div className="flex items-center gap-1">
625
  <span>Auto-indexing</span>
626
  </div>
627
  </div>
 
630
  </main>
631
 
632
  {/* ─── Footer ─── */}
633
+ <footer className="relative z-10 h-10 border-t border-[var(--border-medium)] flex items-center justify-center kiosk-footer">
634
+ <p className="text-[9.5px] font-mono font-bold text-[var(--text-muted)] tracking-wider">
635
+ NETRAID SECURE TERMINAL GATEWAY v1.0.0 · {engineMode}
636
  </p>
637
  </footer>
638
  </div>
frontend/app/page.tsx CHANGED
@@ -47,22 +47,18 @@ export default function LoginPage() {
47
  return (
48
  <div className="min-h-screen relative bg-[var(--bg-base)] flex items-center justify-center overflow-hidden px-4">
49
  {/* Grid Mesh background */}
50
- <div className="absolute inset-0 mesh-bg opacity-30 pointer-events-none" />
51
-
52
- {/* Ambient background glows */}
53
- <div className="absolute top-0 left-1/4 w-96 h-96 bg-slate-100/50 rounded-full blur-[100px] pointer-events-none" />
54
- <div className="absolute bottom-0 right-1/4 w-96 h-96 bg-zinc-100/50 rounded-full blur-[100px] pointer-events-none" />
55
 
56
  <div className="w-full max-w-sm relative z-10 animate-fadeInUp">
57
 
58
  {/* Logo block */}
59
- <div className="text-center mb-8">
60
- <div className="relative inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-slate-950 border border-slate-800/80 shadow-[0_0_20px_rgba(6,182,212,0.25)] mb-4 overflow-hidden group">
61
  {/* Ambient glowing background inside the container */}
62
  <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)] animate-pulse" />
63
 
64
  {/* Robotic eye SVG */}
65
- <svg viewBox="0 0 100 100" className="w-10 h-10 relative z-10" fill="none" xmlns="http://www.w3.org/2000/svg">
66
  {/* Outer technical rotating rings */}
67
  <circle cx="50" cy="50" r="45" stroke="#1e293b" strokeWidth="1.5" strokeDasharray="6 12" className="animate-rotate-ring" />
68
  <circle cx="50" cy="50" r="40" stroke="#0891b2" strokeWidth="1" strokeDasharray="40 10 15 5" className="animate-rotate-ring-reverse" style={{ opacity: 0.6 }} />
@@ -88,7 +84,6 @@ export default function LoginPage() {
88
 
89
  {/* Futuristic crosshairs / HUD markers */}
90
  <path d="M50 5 L50 12 M50 88 L50 95 M5 50 L12 50 M88 50 L95 50" stroke="#475569" strokeWidth="1.5" />
91
- <path d="M30 30 L35 35 M65 65 L70 70 M65 30 L60 35 M35 65 L30 70" stroke="#0891b2" strokeWidth="1" opacity="0.5" />
92
 
93
  {/* Scanning laser beam overlay */}
94
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="1.5" className="animate-laser" filter="url(#glow-logo)" />
@@ -102,41 +97,38 @@ export default function LoginPage() {
102
  </defs>
103
  </svg>
104
  </div>
105
- <h1 className="text-2xl font-bold text-slate-900 tracking-tight">
106
  NetraID
107
  </h1>
108
- <p className="text-slate-500 text-[12.5px] mt-1">
109
- AI Biometric Attendance Platform
110
  </p>
111
  </div>
112
 
113
  {/* Login card */}
114
- <div className="bg-white border border-slate-200 rounded-2xl p-8 shadow-lg shadow-slate-100">
115
- <div className="mb-6">
116
- <h2 className="text-base font-bold text-slate-800">
117
- Sign in to dashboard
118
  </h2>
119
- <p className="text-xs text-slate-500 mt-1">
120
- Use your administrator credentials to continue
121
- </p>
122
  </div>
123
 
124
  {/* Error state */}
125
  {error && (
126
- <div className="flex items-start gap-2.5 bg-rose-500/5 border border-rose-500/15 text-rose-600 p-3 rounded-xl mb-5 text-xs animate-fadeInUp">
127
- <ShieldAlert className="w-4 h-4 shrink-0 mt-0.5" />
128
  <span className="leading-relaxed">{error}</span>
129
  </div>
130
  )}
131
 
132
- <form onSubmit={handleSubmit} className="space-y-4">
133
  {/* Email */}
134
  <div className="space-y-1.5">
135
- <label className="block text-[10.5px] font-bold text-slate-500 uppercase tracking-wider">
136
  Email Address
137
  </label>
138
  <div className="relative">
139
- <Mail className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
140
  <input
141
  id="login-email"
142
  type="email"
@@ -144,7 +136,7 @@ export default function LoginPage() {
144
  placeholder="admin@netraid.ai"
145
  value={email}
146
  onChange={(e) => setEmail(e.target.value)}
147
- className="input-field pl-icon h-11 bg-white border-slate-200 text-slate-900 rounded-xl focus:border-slate-800"
148
  autoComplete="email"
149
  suppressHydrationWarning
150
  />
@@ -153,11 +145,11 @@ export default function LoginPage() {
153
 
154
  {/* Password */}
155
  <div className="space-y-1.5">
156
- <label className="block text-[10.5px] font-bold text-slate-500 uppercase tracking-wider">
157
  Password
158
  </label>
159
  <div className="relative">
160
- <Lock className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" />
161
  <input
162
  id="login-password"
163
  type={showPass ? "text" : "password"}
@@ -165,17 +157,17 @@ export default function LoginPage() {
165
  placeholder="••••••••"
166
  value={password}
167
  onChange={(e) => setPassword(e.target.value)}
168
- className="input-field pl-icon pr-icon h-11 bg-white border-slate-200 text-slate-900 rounded-xl focus:border-slate-800"
169
  autoComplete="current-password"
170
  suppressHydrationWarning
171
  />
172
  <button
173
  type="button"
174
  onClick={() => setShowPass(!showPass)}
175
- className="absolute right-3.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
176
  suppressHydrationWarning
177
  >
178
- {showPass ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
179
  </button>
180
  </div>
181
  </div>
@@ -185,18 +177,18 @@ export default function LoginPage() {
185
  id="login-submit"
186
  type="submit"
187
  disabled={loading}
188
- className="btn-primary w-full h-11 flex items-center justify-center gap-2 mt-2 rounded-xl bg-slate-900 text-white border-slate-900 hover:bg-slate-800 cursor-pointer shadow-sm"
189
  suppressHydrationWarning
190
  >
191
  {loading ? (
192
  <>
193
- <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
194
  <span>Authenticating...</span>
195
  </>
196
  ) : (
197
  <>
198
  <span>Sign In</span>
199
- <ArrowRight className="w-4 h-4" />
200
  </>
201
  )}
202
  </button>
@@ -204,21 +196,21 @@ export default function LoginPage() {
204
  </div>
205
 
206
  {/* Kiosk link */}
207
- <div className="mt-5 text-center">
208
  <a
209
  href="/kiosk"
210
  target="_blank"
211
  rel="noopener noreferrer"
212
- className="inline-flex items-center gap-2 text-[12px] text-slate-500 hover:text-slate-800 transition-colors"
213
  >
214
- <ExternalLink className="w-3.5 h-3.5" />
215
  <span>Open Kiosk Terminal</span>
216
  </a>
217
  </div>
218
 
219
  {/* Footer */}
220
- <p className="text-center text-[10px] text-slate-400 mt-6 font-mono">
221
- NetraID v1.0.0 · Open Source · MIT License
222
  </p>
223
  </div>
224
  </div>
 
47
  return (
48
  <div className="min-h-screen relative bg-[var(--bg-base)] flex items-center justify-center overflow-hidden px-4">
49
  {/* Grid Mesh background */}
50
+ <div className="absolute inset-0 mesh-bg pointer-events-none" />
 
 
 
 
51
 
52
  <div className="w-full max-w-sm relative z-10 animate-fadeInUp">
53
 
54
  {/* Logo block */}
55
+ <div className="text-center mb-6">
56
+ <div className="relative inline-flex items-center justify-center w-18 h-18 rounded-2xl bg-slate-950 border border-slate-800/80 shadow-[0_0_20px_rgba(6,182,212,0.25)] mb-4 overflow-hidden group">
57
  {/* Ambient glowing background inside the container */}
58
  <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)] animate-pulse" />
59
 
60
  {/* Robotic eye SVG */}
61
+ <svg viewBox="0 0 100 100" className="w-11 h-11 relative z-10" fill="none" xmlns="http://www.w3.org/2000/svg">
62
  {/* Outer technical rotating rings */}
63
  <circle cx="50" cy="50" r="45" stroke="#1e293b" strokeWidth="1.5" strokeDasharray="6 12" className="animate-rotate-ring" />
64
  <circle cx="50" cy="50" r="40" stroke="#0891b2" strokeWidth="1" strokeDasharray="40 10 15 5" className="animate-rotate-ring-reverse" style={{ opacity: 0.6 }} />
 
84
 
85
  {/* Futuristic crosshairs / HUD markers */}
86
  <path d="M50 5 L50 12 M50 88 L50 95 M5 50 L12 50 M88 50 L95 50" stroke="#475569" strokeWidth="1.5" />
 
87
 
88
  {/* Scanning laser beam overlay */}
89
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="1.5" className="animate-laser" filter="url(#glow-logo)" />
 
97
  </defs>
98
  </svg>
99
  </div>
100
+ <h1 className="text-xl font-extrabold text-slate-900 tracking-tight">
101
  NetraID
102
  </h1>
103
+ <p className="text-slate-400 text-[11px] mt-0.5">
104
+ AI Biometric Attendance
105
  </p>
106
  </div>
107
 
108
  {/* Login card */}
109
+ <div className="glass-overlay rounded-2xl p-6 shadow-[0_4px_30px_rgba(0,0,0,0.03)]">
110
+ <div className="mb-5 text-center">
111
+ <h2 className="text-xs font-bold uppercase tracking-wider text-slate-400">
112
+ Admin Credentials
113
  </h2>
 
 
 
114
  </div>
115
 
116
  {/* Error state */}
117
  {error && (
118
+ <div className="flex items-start gap-2 bg-rose-500/5 border border-rose-500/15 text-rose-600 p-2.5 rounded-lg mb-4 text-[11.5px] animate-fadeInUp">
119
+ <ShieldAlert className="w-3.5 h-3.5 shrink-0 mt-0.5" />
120
  <span className="leading-relaxed">{error}</span>
121
  </div>
122
  )}
123
 
124
+ <form onSubmit={handleSubmit} className="space-y-3.5">
125
  {/* Email */}
126
  <div className="space-y-1.5">
127
+ <label className="block text-[9.5px] font-bold text-slate-400 uppercase tracking-wider">
128
  Email Address
129
  </label>
130
  <div className="relative">
131
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400 pointer-events-none" />
132
  <input
133
  id="login-email"
134
  type="email"
 
136
  placeholder="admin@netraid.ai"
137
  value={email}
138
  onChange={(e) => setEmail(e.target.value)}
139
+ className="input-field pl-icon"
140
  autoComplete="email"
141
  suppressHydrationWarning
142
  />
 
145
 
146
  {/* Password */}
147
  <div className="space-y-1.5">
148
+ <label className="block text-[9.5px] font-bold text-slate-400 uppercase tracking-wider">
149
  Password
150
  </label>
151
  <div className="relative">
152
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-slate-400 pointer-events-none" />
153
  <input
154
  id="login-password"
155
  type={showPass ? "text" : "password"}
 
157
  placeholder="••••••••"
158
  value={password}
159
  onChange={(e) => setPassword(e.target.value)}
160
+ className="input-field pl-icon pr-icon"
161
  autoComplete="current-password"
162
  suppressHydrationWarning
163
  />
164
  <button
165
  type="button"
166
  onClick={() => setShowPass(!showPass)}
167
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
168
  suppressHydrationWarning
169
  >
170
+ {showPass ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
171
  </button>
172
  </div>
173
  </div>
 
177
  id="login-submit"
178
  type="submit"
179
  disabled={loading}
180
+ className="btn-primary w-full flex items-center justify-center gap-1.5 mt-2 h-9"
181
  suppressHydrationWarning
182
  >
183
  {loading ? (
184
  <>
185
+ <div className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
186
  <span>Authenticating...</span>
187
  </>
188
  ) : (
189
  <>
190
  <span>Sign In</span>
191
+ <ArrowRight className="w-3.5 h-3.5" />
192
  </>
193
  )}
194
  </button>
 
196
  </div>
197
 
198
  {/* Kiosk link */}
199
+ <div className="mt-4 text-center">
200
  <a
201
  href="/kiosk"
202
  target="_blank"
203
  rel="noopener noreferrer"
204
+ className="inline-flex items-center gap-1.5 text-[11px] text-slate-400 hover:text-slate-800 transition-colors"
205
  >
206
+ <ExternalLink className="w-3 h-3" />
207
  <span>Open Kiosk Terminal</span>
208
  </a>
209
  </div>
210
 
211
  {/* Footer */}
212
+ <p className="text-center text-[9px] text-slate-400 mt-6 font-mono">
213
+ NetraID v1.0.0
214
  </p>
215
  </div>
216
  </div>
frontend/app/providers.tsx CHANGED
@@ -1,7 +1,8 @@
1
  "use client";
2
 
3
- import React, { useState } from "react";
4
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 
5
 
6
  export default function Providers({ children }: { children: React.ReactNode }) {
7
  const [queryClient] = useState(
@@ -17,9 +18,23 @@ export default function Providers({ children }: { children: React.ReactNode }) {
17
  })
18
  );
19
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  return (
21
  <QueryClientProvider client={queryClient}>
22
- {children}
 
 
23
  </QueryClientProvider>
24
  );
25
  }
 
1
  "use client";
2
 
3
+ import React, { useState, useEffect } from "react";
4
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { ToastProvider } from "./utils/toast";
6
 
7
  export default function Providers({ children }: { children: React.ReactNode }) {
8
  const [queryClient] = useState(
 
18
  })
19
  );
20
 
21
+ useEffect(() => {
22
+ const savedTheme = localStorage.getItem("theme");
23
+ const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
24
+ const initialTheme = savedTheme || (systemPrefersDark ? "dark" : "light");
25
+
26
+ if (initialTheme === "dark") {
27
+ document.documentElement.classList.add("dark");
28
+ } else {
29
+ document.documentElement.classList.remove("dark");
30
+ }
31
+ }, []);
32
+
33
  return (
34
  <QueryClientProvider client={queryClient}>
35
+ <ToastProvider>
36
+ {children}
37
+ </ToastProvider>
38
  </QueryClientProvider>
39
  );
40
  }
frontend/app/reports/page.tsx CHANGED
@@ -4,6 +4,7 @@ import React, { useState } from "react";
4
  import { useQuery } from "@tanstack/react-query";
5
  import SidebarLayout from "@/components/SidebarLayout";
6
  import { fetchApi, getLocalDateString } from "@/app/utils/api";
 
7
  import {
8
  FileText, Calendar, Download, Loader2, CheckCircle2,
9
  FileSpreadsheet, FileDown, CalendarDays, User, Layers, ChevronDown, Check
@@ -23,6 +24,7 @@ const FORMATS = [
23
  ];
24
 
25
  export default function ReportsPage() {
 
26
  const [reportType, setReportType] = useState("daily");
27
  const [format, setFormat] = useState("xlsx");
28
  const [startDate, setStartDate] = useState(() => {
@@ -55,7 +57,7 @@ export default function ReportsPage() {
55
  setSuccess(true);
56
  setTimeout(() => setSuccess(false), 5000);
57
  } catch (err: any) {
58
- alert(err.message || "Failed to generate report.");
59
  } finally {
60
  setExporting(false);
61
  }
@@ -70,9 +72,6 @@ export default function ReportsPage() {
70
  {/* Header */}
71
  <div className="pb-5 border-b border-white/5">
72
  <h1 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">Reports & Export</h1>
73
- <p className="text-xs text-slate-500 mt-0.5">
74
- Compile and generate compliance-ready corporate records in PDF, Excel, or CSV format.
75
- </p>
76
  </div>
77
 
78
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
@@ -84,7 +83,6 @@ export default function ReportsPage() {
84
  </div>
85
  <div>
86
  <h2 className="text-xs font-bold text-[var(--text-primary)] uppercase tracking-wider">Export Parameters</h2>
87
- <p className="text-[10px] text-slate-500 mt-0.5">Specify report criteria and format</p>
88
  </div>
89
  </div>
90
 
 
4
  import { useQuery } from "@tanstack/react-query";
5
  import SidebarLayout from "@/components/SidebarLayout";
6
  import { fetchApi, getLocalDateString } from "@/app/utils/api";
7
+ import { useToast } from "@/app/utils/toast";
8
  import {
9
  FileText, Calendar, Download, Loader2, CheckCircle2,
10
  FileSpreadsheet, FileDown, CalendarDays, User, Layers, ChevronDown, Check
 
24
  ];
25
 
26
  export default function ReportsPage() {
27
+ const { toast } = useToast();
28
  const [reportType, setReportType] = useState("daily");
29
  const [format, setFormat] = useState("xlsx");
30
  const [startDate, setStartDate] = useState(() => {
 
57
  setSuccess(true);
58
  setTimeout(() => setSuccess(false), 5000);
59
  } catch (err: any) {
60
+ toast.error(err.message || "Failed to generate report.");
61
  } finally {
62
  setExporting(false);
63
  }
 
72
  {/* Header */}
73
  <div className="pb-5 border-b border-white/5">
74
  <h1 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">Reports & Export</h1>
 
 
 
75
  </div>
76
 
77
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
 
83
  </div>
84
  <div>
85
  <h2 className="text-xs font-bold text-[var(--text-primary)] uppercase tracking-wider">Export Parameters</h2>
 
86
  </div>
87
  </div>
88
 
frontend/app/settings/page.tsx CHANGED
@@ -4,6 +4,7 @@ import React, { useState } from "react";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
  import SidebarLayout from "@/components/SidebarLayout";
6
  import { fetchApi } from "@/app/utils/api";
 
7
  import {
8
  Check, Loader2, CheckCircle2, AlertCircle, Sliders, Info,
9
  Fingerprint, Clock, Volume2
@@ -13,6 +14,7 @@ type SettingsTab = "biometrics" | "shift" | "voice" | "advanced";
13
 
14
  export default function SettingsPage() {
15
  const queryClient = useQueryClient();
 
16
  const [activeTab, setActiveTab] = useState<SettingsTab>("biometrics");
17
  const [updatingKey, setUpdatingKey] = useState<string | null>(null);
18
  const [successKey, setSuccessKey] = useState<string | null>(null);
@@ -40,7 +42,7 @@ export default function SettingsPage() {
40
  },
41
  onError: (err: any, vars) => {
42
  setUpdatingKey(null);
43
- alert(err.message || `Failed to save ${vars.key}`);
44
  }
45
  });
46
 
@@ -209,9 +211,6 @@ export default function SettingsPage() {
209
  {/* Header */}
210
  <div className="pb-5 border-b border-slate-200">
211
  <h1 className="text-xl font-bold text-slate-900 tracking-tight">System Configuration</h1>
212
- <p className="text-xs text-slate-500 mt-0.5">
213
- Optimize biometric accuracy rates, manage shift schedule rules, and toggle voice assistant audio.
214
- </p>
215
  </div>
216
 
217
  {/* Outer container */}
@@ -252,9 +251,6 @@ export default function SettingsPage() {
252
  <h2 className="text-sm font-bold text-slate-900">
253
  {getTabLabel(activeTab)}
254
  </h2>
255
- <p className="text-[11px] text-slate-400 mt-0.5">
256
- {getTabDesc(activeTab)}
257
- </p>
258
  </div>
259
 
260
  {isLoading ? (
 
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
  import SidebarLayout from "@/components/SidebarLayout";
6
  import { fetchApi } from "@/app/utils/api";
7
+ import { useToast } from "@/app/utils/toast";
8
  import {
9
  Check, Loader2, CheckCircle2, AlertCircle, Sliders, Info,
10
  Fingerprint, Clock, Volume2
 
14
 
15
  export default function SettingsPage() {
16
  const queryClient = useQueryClient();
17
+ const { toast } = useToast();
18
  const [activeTab, setActiveTab] = useState<SettingsTab>("biometrics");
19
  const [updatingKey, setUpdatingKey] = useState<string | null>(null);
20
  const [successKey, setSuccessKey] = useState<string | null>(null);
 
42
  },
43
  onError: (err: any, vars) => {
44
  setUpdatingKey(null);
45
+ toast.error(err.message || `Failed to save ${vars.key}`);
46
  }
47
  });
48
 
 
211
  {/* Header */}
212
  <div className="pb-5 border-b border-slate-200">
213
  <h1 className="text-xl font-bold text-slate-900 tracking-tight">System Configuration</h1>
 
 
 
214
  </div>
215
 
216
  {/* Outer container */}
 
251
  <h2 className="text-sm font-bold text-slate-900">
252
  {getTabLabel(activeTab)}
253
  </h2>
 
 
 
254
  </div>
255
 
256
  {isLoading ? (
frontend/app/utils/api.ts CHANGED
@@ -124,9 +124,10 @@ export async function fetchApi(endpoint: string, options: RequestInit = {}): Pro
124
 
125
  export function parseDateTime(dateStr: string | null | undefined): Date | null {
126
  if (!dateStr) return null;
127
- // If no offset/timezone indicator, append 'Z' to treat as UTC from database
 
128
  const hasTimezone = dateStr.endsWith("Z") || dateStr.includes("+") || /-\d{2}:\d{2}$/.test(dateStr);
129
- const formattedStr = hasTimezone ? dateStr : `${dateStr}Z`;
130
  return new Date(formattedStr);
131
  }
132
 
 
124
 
125
  export function parseDateTime(dateStr: string | null | undefined): Date | null {
126
  if (!dateStr) return null;
127
+ // If the date string has a timezone, parse it as-is.
128
+ // Otherwise, treat it as a local ISO string (do NOT append 'Z', as backend stores local time).
129
  const hasTimezone = dateStr.endsWith("Z") || dateStr.includes("+") || /-\d{2}:\d{2}$/.test(dateStr);
130
+ const formattedStr = hasTimezone ? dateStr : dateStr.replace(" ", "T");
131
  return new Date(formattedStr);
132
  }
133
 
frontend/app/utils/toast.tsx ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { createContext, useContext, useState, useCallback } from "react";
4
+ import { AlertCircle, CheckCircle2, Info, X } from "lucide-react";
5
+
6
+ type ToastType = "success" | "error" | "info" | "warning";
7
+
8
+ interface Toast {
9
+ id: string;
10
+ message: string;
11
+ type: ToastType;
12
+ }
13
+
14
+ interface ToastContextType {
15
+ toast: {
16
+ success: (message: string) => void;
17
+ error: (message: string) => void;
18
+ info: (message: string) => void;
19
+ warning: (message: string) => void;
20
+ };
21
+ }
22
+
23
+ const ToastContext = createContext<ToastContextType | undefined>(undefined);
24
+
25
+ export function useToast() {
26
+ const context = useContext(ToastContext);
27
+ if (!context) {
28
+ throw new Error("useToast must be used within a ToastProvider");
29
+ }
30
+ return context;
31
+ }
32
+
33
+ export function ToastProvider({ children }: { children: React.ReactNode }) {
34
+ const [toasts, setToasts] = useState<Toast[]>([]);
35
+
36
+ const removeToast = useCallback((id: string) => {
37
+ setToasts((prev) => prev.filter((t) => t.id !== id));
38
+ }, []);
39
+
40
+ const addToast = useCallback((message: string, type: ToastType) => {
41
+ const id = Math.random().toString(36).substring(2, 9);
42
+ setToasts((prev) => [...prev, { id, message, type }]);
43
+
44
+ // Auto remove after 4.5 seconds
45
+ setTimeout(() => {
46
+ removeToast(id);
47
+ }, 4500);
48
+ }, [removeToast]);
49
+
50
+ const toast = {
51
+ success: (msg: string) => addToast(msg, "success"),
52
+ error: (msg: string) => addToast(msg, "error"),
53
+ info: (msg: string) => addToast(msg, "info"),
54
+ warning: (msg: string) => addToast(msg, "warning"),
55
+ };
56
+
57
+ return (
58
+ <ToastContext.Provider value={{ toast }}>
59
+ {children}
60
+ <style>{`
61
+ @keyframes slideInRight {
62
+ from {
63
+ transform: translateX(105%);
64
+ opacity: 0;
65
+ }
66
+ to {
67
+ transform: translateX(0);
68
+ opacity: 1;
69
+ }
70
+ }
71
+ .animate-slide-in-right {
72
+ animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
73
+ }
74
+ `}</style>
75
+
76
+ {/* Toast Portal Container */}
77
+ <div className="fixed top-5 right-5 z-[9999] flex flex-col gap-2.5 max-w-sm w-[calc(100vw-40px)] pointer-events-none">
78
+ {toasts.map((t) => (
79
+ <div
80
+ key={t.id}
81
+ className={`pointer-events-auto flex items-start gap-3 p-3.5 rounded-2xl border shadow-lg transition-all duration-300 animate-slide-in-right ${
82
+ t.type === "success"
83
+ ? "bg-emerald-50/90 border-emerald-200 text-emerald-900 dark:bg-emerald-950/90 dark:border-emerald-900/40 dark:text-emerald-200"
84
+ : t.type === "error"
85
+ ? "bg-rose-50/90 border-rose-200 text-rose-900 dark:bg-rose-950/90 dark:border-rose-900/40 dark:text-rose-200"
86
+ : t.type === "warning"
87
+ ? "bg-amber-50/90 border-amber-200 text-amber-900 dark:bg-amber-950/90 dark:border-amber-900/40 dark:text-amber-200"
88
+ : "bg-blue-50/90 border-blue-200 text-blue-900 dark:bg-blue-950/90 dark:border-blue-900/40 dark:text-blue-200"
89
+ }`}
90
+ style={{ backdropFilter: "blur(12px)" }}
91
+ >
92
+ {t.type === "success" && <CheckCircle2 className="w-4.5 h-4.5 text-emerald-500 shrink-0 mt-0.5" />}
93
+ {t.type === "error" && <AlertCircle className="w-4.5 h-4.5 text-rose-500 shrink-0 mt-0.5" />}
94
+ {t.type === "warning" && <AlertCircle className="w-4.5 h-4.5 text-amber-500 shrink-0 mt-0.5" />}
95
+ {t.type === "info" && <Info className="w-4.5 h-4.5 text-blue-500 shrink-0 mt-0.5" />}
96
+
97
+ <p className="text-[12px] font-semibold flex-1 leading-relaxed">{t.message}</p>
98
+
99
+ <button
100
+ onClick={() => removeToast(t.id)}
101
+ className="p-1 hover:bg-black/5 dark:hover:bg-white/10 rounded-lg text-slate-400 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-200 transition-colors cursor-pointer"
102
+ >
103
+ <X className="w-3.5 h-3.5" />
104
+ </button>
105
+ </div>
106
+ ))}
107
+ </div>
108
+ </ToastContext.Provider>
109
+ );
110
+ }
frontend/components/SidebarLayout.tsx CHANGED
@@ -15,14 +15,16 @@ import {
15
  X,
16
  ChevronRight,
17
  Scan,
18
- History
 
 
19
  } from "lucide-react";
20
  import { getAccessToken, getUserProfile, clearTokens } from "@/app/utils/api";
21
 
22
  const navItems = [
23
  { name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
24
- { name: "Employees", href: "/employees", icon: Users },
25
  { name: "Attendance", href: "/attendance", icon: Clock },
 
26
  { name: "Reports", href: "/reports", icon: FileSpreadsheet },
27
  { name: "Audit Logs", href: "/audit", icon: History },
28
  { name: "Settings", href: "/settings", icon: Settings },
@@ -85,14 +87,38 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
85
  const [user, setUser] = useState<any>(null);
86
  const [authorized, setAuthorized] = useState(false);
87
 
88
- // Load sidebar state from localStorage on client side
 
 
89
  useEffect(() => {
90
  const saved = localStorage.getItem("sidebar_collapsed");
91
  if (saved === "true") {
92
  setIsCollapsed(true);
93
  }
 
 
 
 
 
 
 
 
 
 
 
94
  }, []);
95
 
 
 
 
 
 
 
 
 
 
 
 
96
  const toggleCollapse = () => {
97
  const nextVal = !isCollapsed;
98
  setIsCollapsed(nextVal);
@@ -148,10 +174,10 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
148
  <div className={`flex items-center ${isCollapsed ? "justify-center" : "justify-between"} px-1 py-3 mb-6`}>
149
  {!isCollapsed && (
150
  <div className="flex items-center gap-3">
151
- <div className="relative inline-flex items-center justify-center w-9 h-9 rounded-xl bg-slate-900 border border-slate-700/80 shadow-[0_0_15px_rgba(6,182,212,0.25)] overflow-hidden shrink-0">
152
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.2)_0%,transparent_70%)]" />
153
- <div className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-emerald-500 border border-white z-20" />
154
- <svg viewBox="0 0 100 100" className="w-5.5 h-5.5 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
155
  <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
156
  <circle cx="50" cy="50" r="40" stroke="#06b6d4" strokeWidth="1.5" strokeDasharray="30 15" className="animate-rotate-ring-reverse" style={{ opacity: 0.8 }} />
157
  <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
@@ -163,21 +189,18 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
163
  </svg>
164
  </div>
165
  <div>
166
- <h1 className="font-bold text-[15px] tracking-tight text-[var(--text-primary)] leading-none">
167
  NetraID
168
  </h1>
169
- <p className="text-[10px] text-[var(--text-muted)] font-mono tracking-widest uppercase mt-0.5">
170
- Face Auth
171
- </p>
172
  </div>
173
  </div>
174
  )}
175
 
176
  {isCollapsed && (
177
- <div className="relative inline-flex items-center justify-center w-9 h-9 rounded-xl bg-slate-900 border border-slate-700/80 shadow-[0_0_15px_rgba(6,182,212,0.25)] overflow-hidden shrink-0">
178
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.2)_0%,transparent_70%)]" />
179
- <div className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-emerald-500 border border-white z-20" />
180
- <svg viewBox="0 0 100 100" className="w-5.5 h-5.5 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
181
  <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
182
  <circle cx="50" cy="50" r="40" stroke="#06b6d4" strokeWidth="1.5" strokeDasharray="30 15" className="animate-rotate-ring-reverse" style={{ opacity: 0.8 }} />
183
  <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
@@ -196,7 +219,7 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
196
  className="p-1.5 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--border-subtle)] transition-all cursor-pointer"
197
  title="Collapse sidebar"
198
  >
199
- <Menu className="w-4.5 h-4.5" />
200
  </button>
201
  )}
202
  </div>
@@ -208,7 +231,7 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
208
  className="p-1.5 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--border-subtle)] transition-all cursor-pointer"
209
  title="Expand sidebar"
210
  >
211
- <Menu className="w-4.5 h-4.5" />
212
  </button>
213
  </div>
214
  )}
@@ -249,6 +272,22 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
249
  </>
250
  )}
251
  </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  </nav>
253
 
254
  {/* User Profile Footer */}
@@ -298,11 +337,11 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
298
 
299
  {/* ─── Mobile Top Bar ─── */}
300
  <div className="md:hidden fixed top-0 left-0 right-0 h-14 z-50 flex items-center justify-between px-5 border-b border-[var(--border-subtle)] bg-[var(--bg-surface)]/95 backdrop-blur-xl">
301
- <div className="flex items-center gap-2.5">
302
- <div className="relative inline-flex items-center justify-center w-8 h-8 rounded-lg bg-slate-900 border border-slate-700/80 shadow-[0_0_15px_rgba(6,182,212,0.25)] overflow-hidden shrink-0">
303
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.2)_0%,transparent_70%)]" />
304
  <div className="absolute -bottom-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-emerald-500 border border-white z-20" />
305
- <svg viewBox="0 0 100 100" className="w-5 h-5 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
306
  <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
307
  <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
308
  <g className="animate-eye-lid">
@@ -312,14 +351,24 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
312
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="2" className="animate-laser" />
313
  </svg>
314
  </div>
315
- <span className="font-bold text-sm text-[var(--text-primary)]">NetraID</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  </div>
317
- <button
318
- onClick={() => setSidebarOpen(!sidebarOpen)}
319
- className="p-2 rounded-lg hover:bg-[var(--border-subtle)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all cursor-pointer"
320
- >
321
- {sidebarOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
322
- </button>
323
  </div>
324
 
325
  {/* ─── Mobile Drawer ─── */}
@@ -333,10 +382,10 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
333
  onClick={(e) => e.stopPropagation()}
334
  >
335
  <div className="flex items-center gap-3 px-1 py-3 mb-6 mt-14">
336
- <div className="relative inline-flex items-center justify-center w-8 h-8 rounded-lg bg-slate-900 border border-slate-700/80 shadow-[0_0_15px_rgba(6,182,212,0.25)] overflow-hidden shrink-0">
337
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.2)_0%,transparent_70%)]" />
338
  <div className="absolute -bottom-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-emerald-500 border border-white z-20" />
339
- <svg viewBox="0 0 100 100" className="w-5 h-5 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
340
  <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
341
  <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
342
  <g className="animate-eye-lid">
@@ -346,7 +395,7 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
346
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="2" className="animate-laser" />
347
  </svg>
348
  </div>
349
- <h1 className="font-bold text-sm text-[var(--text-primary)]">NetraID</h1>
350
  </div>
351
 
352
  <nav className="flex-1 space-y-0.5">
@@ -389,6 +438,19 @@ export default function SidebarLayout({ children }: { children: React.ReactNode
389
 
390
  {/* ─── Main Content ─── */}
391
  <main className="flex-1 min-h-screen overflow-y-auto relative z-10 pt-14 md:pt-0">
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  <div className="max-w-7xl mx-auto px-5 py-6 md:px-8 md:py-8">
393
  {children}
394
  </div>
 
15
  X,
16
  ChevronRight,
17
  Scan,
18
+ History,
19
+ Sun,
20
+ Moon
21
  } from "lucide-react";
22
  import { getAccessToken, getUserProfile, clearTokens } from "@/app/utils/api";
23
 
24
  const navItems = [
25
  { name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
 
26
  { name: "Attendance", href: "/attendance", icon: Clock },
27
+ { name: "Employees", href: "/employees", icon: Users },
28
  { name: "Reports", href: "/reports", icon: FileSpreadsheet },
29
  { name: "Audit Logs", href: "/audit", icon: History },
30
  { name: "Settings", href: "/settings", icon: Settings },
 
87
  const [user, setUser] = useState<any>(null);
88
  const [authorized, setAuthorized] = useState(false);
89
 
90
+ const [theme, setTheme] = useState<"light" | "dark">("light");
91
+
92
+ // Load theme and sidebar state from localStorage on client side
93
  useEffect(() => {
94
  const saved = localStorage.getItem("sidebar_collapsed");
95
  if (saved === "true") {
96
  setIsCollapsed(true);
97
  }
98
+
99
+ const savedTheme = localStorage.getItem("theme");
100
+ const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
101
+ const initialTheme = (savedTheme as "light" | "dark") || (systemPrefersDark ? "dark" : "light");
102
+
103
+ setTheme(initialTheme);
104
+ if (initialTheme === "dark") {
105
+ document.documentElement.classList.add("dark");
106
+ } else {
107
+ document.documentElement.classList.remove("dark");
108
+ }
109
  }, []);
110
 
111
+ const toggleTheme = () => {
112
+ const nextTheme = theme === "dark" ? "light" : "dark";
113
+ setTheme(nextTheme);
114
+ localStorage.setItem("theme", nextTheme);
115
+ if (nextTheme === "dark") {
116
+ document.documentElement.classList.add("dark");
117
+ } else {
118
+ document.documentElement.classList.remove("dark");
119
+ }
120
+ };
121
+
122
  const toggleCollapse = () => {
123
  const nextVal = !isCollapsed;
124
  setIsCollapsed(nextVal);
 
174
  <div className={`flex items-center ${isCollapsed ? "justify-center" : "justify-between"} px-1 py-3 mb-6`}>
175
  {!isCollapsed && (
176
  <div className="flex items-center gap-3">
177
+ <div className="relative inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-900 border border-slate-700/80 shadow-[0_0_15px_rgba(6,182,212,0.25)] overflow-hidden shrink-0">
178
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)] animate-pulse" />
179
+ <div className="absolute -bottom-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-emerald-500 border border-white z-20" />
180
+ <svg viewBox="0 0 100 100" className="w-6 h-6 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
181
  <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
182
  <circle cx="50" cy="50" r="40" stroke="#06b6d4" strokeWidth="1.5" strokeDasharray="30 15" className="animate-rotate-ring-reverse" style={{ opacity: 0.8 }} />
183
  <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
 
189
  </svg>
190
  </div>
191
  <div>
192
+ <h1 className="font-extrabold text-[17px] tracking-tight text-[var(--text-primary)] leading-none">
193
  NetraID
194
  </h1>
 
 
 
195
  </div>
196
  </div>
197
  )}
198
 
199
  {isCollapsed && (
200
+ <div className="relative inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-900 border border-slate-700/80 shadow-[0_0_15px_rgba(6,182,212,0.25)] overflow-hidden shrink-0">
201
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)] animate-pulse" />
202
+ <div className="absolute -bottom-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-emerald-500 border border-white z-20" />
203
+ <svg viewBox="0 0 100 100" className="w-6 h-6 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
204
  <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
205
  <circle cx="50" cy="50" r="40" stroke="#06b6d4" strokeWidth="1.5" strokeDasharray="30 15" className="animate-rotate-ring-reverse" style={{ opacity: 0.8 }} />
206
  <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
 
219
  className="p-1.5 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--border-subtle)] transition-all cursor-pointer"
220
  title="Collapse sidebar"
221
  >
222
+ <Menu className="w-4 h-4" />
223
  </button>
224
  )}
225
  </div>
 
231
  className="p-1.5 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--border-subtle)] transition-all cursor-pointer"
232
  title="Expand sidebar"
233
  >
234
+ <Menu className="w-4 h-4" />
235
  </button>
236
  </div>
237
  )}
 
272
  </>
273
  )}
274
  </a>
275
+
276
+ {/* Admin Log Out */}
277
+ <button
278
+ onClick={handleLogout}
279
+ data-tooltip={isCollapsed ? "Admin Log Out" : undefined}
280
+ className={`group flex items-center ${isCollapsed ? "justify-center px-2" : "gap-3 px-3"} py-2.5 rounded-xl text-[var(--text-secondary)] hover:text-rose-600 hover:bg-rose-500/10 dark:hover:bg-rose-500/15 transition-all duration-200 w-full text-left cursor-pointer`}
281
+ >
282
+ <div className="w-8 h-8 rounded-lg bg-[var(--border-subtle)] flex items-center justify-center group-hover:bg-rose-600 transition-all">
283
+ <LogOut className="w-4 h-4 text-[var(--text-secondary)] group-hover:text-white" />
284
+ </div>
285
+ {!isCollapsed && (
286
+ <div className="flex-1">
287
+ <p className="text-sm font-medium">Admin Log Out</p>
288
+ </div>
289
+ )}
290
+ </button>
291
  </nav>
292
 
293
  {/* User Profile Footer */}
 
337
 
338
  {/* ─── Mobile Top Bar ─── */}
339
  <div className="md:hidden fixed top-0 left-0 right-0 h-14 z-50 flex items-center justify-between px-5 border-b border-[var(--border-subtle)] bg-[var(--bg-surface)]/95 backdrop-blur-xl">
340
+ <div className="flex items-center gap-3">
341
+ <div className="relative inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-900 border border-slate-700/80 shadow-[0_0_15px_rgba(6,182,212,0.25)] overflow-hidden shrink-0">
342
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)] animate-pulse" />
343
  <div className="absolute -bottom-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-emerald-500 border border-white z-20" />
344
+ <svg viewBox="0 0 100 100" className="w-6 h-6 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
345
  <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
346
  <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
347
  <g className="animate-eye-lid">
 
351
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="2" className="animate-laser" />
352
  </svg>
353
  </div>
354
+ <span className="font-extrabold text-[17px] text-[var(--text-primary)] tracking-tight">NetraID</span>
355
+ </div>
356
+ <div className="flex items-center gap-2">
357
+ {/* Theme Toggle Mobile */}
358
+ <button
359
+ onClick={toggleTheme}
360
+ className="p-2 rounded-lg hover:bg-[var(--border-subtle)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all cursor-pointer"
361
+ title="Toggle theme"
362
+ >
363
+ {theme === "dark" ? <Sun className="w-5 h-5 text-amber-500" /> : <Moon className="w-5 h-5" />}
364
+ </button>
365
+ <button
366
+ onClick={() => setSidebarOpen(!sidebarOpen)}
367
+ className="p-2 rounded-lg hover:bg-[var(--border-subtle)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all cursor-pointer"
368
+ >
369
+ {sidebarOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
370
+ </button>
371
  </div>
 
 
 
 
 
 
372
  </div>
373
 
374
  {/* ─── Mobile Drawer ─── */}
 
382
  onClick={(e) => e.stopPropagation()}
383
  >
384
  <div className="flex items-center gap-3 px-1 py-3 mb-6 mt-14">
385
+ <div className="relative inline-flex items-center justify-center w-10 h-10 rounded-xl bg-slate-900 border border-slate-700/80 shadow-[0_0_15px_rgba(6,182,212,0.25)] overflow-hidden shrink-0">
386
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(6,182,212,0.15)_0%,transparent_70%)] animate-pulse" />
387
  <div className="absolute -bottom-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-emerald-500 border border-white z-20" />
388
+ <svg viewBox="0 0 100 100" className="w-6 h-6 relative z-10 animate-fade-in" fill="none" xmlns="http://www.w3.org/2000/svg">
389
  <circle cx="50" cy="50" r="45" stroke="#334155" strokeWidth="2" strokeDasharray="8 12" className="animate-rotate-ring" />
390
  <path d="M15 50 C 30 25, 70 25, 85 50 C 70 75, 30 75, 15 50 Z" stroke="#64748b" strokeWidth="2.5" />
391
  <g className="animate-eye-lid">
 
395
  <line x1="15" y1="50" x2="85" y2="50" stroke="#22d3ee" strokeWidth="2" className="animate-laser" />
396
  </svg>
397
  </div>
398
+ <h1 className="font-extrabold text-[17px] text-[var(--text-primary)] tracking-tight">NetraID</h1>
399
  </div>
400
 
401
  <nav className="flex-1 space-y-0.5">
 
438
 
439
  {/* ─── Main Content ─── */}
440
  <main className="flex-1 min-h-screen overflow-y-auto relative z-10 pt-14 md:pt-0">
441
+ {/* Desktop Top Navbar */}
442
+ <header className="hidden md:flex h-14 border-b border-[var(--border-subtle)] px-8 items-center justify-end bg-[var(--bg-surface)]/95 backdrop-blur-xl sticky top-0 z-30">
443
+ <div className="flex items-center gap-2">
444
+ <button
445
+ onClick={toggleTheme}
446
+ className="p-2 rounded-lg hover:bg-[var(--border-subtle)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all cursor-pointer"
447
+ title="Toggle theme"
448
+ >
449
+ {theme === "dark" ? <Sun className="w-5 h-5 text-amber-500" /> : <Moon className="w-5 h-5" />}
450
+ </button>
451
+ </div>
452
+ </header>
453
+
454
  <div className="max-w-7xl mx-auto px-5 py-6 md:px-8 md:py-8">
455
  {children}
456
  </div>
frontend/tsconfig.tsbuildinfo ADDED
@@ -0,0 +1 @@
 
 
1
+ {"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.iterator.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.esnext.error.d.ts","./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/next/dist/shared/lib/amp.d.ts","./node_modules/next/amp.d.ts","./node_modules/next/dist/server/get-page-files.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","../../../../node_modules/buffer/index.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/react/canary.d.ts","./node_modules/@types/react/experimental.d.ts","./node_modules/@types/react-dom/index.d.ts","./node_modules/@types/react-dom/canary.d.ts","./node_modules/@types/react-dom/experimental.d.ts","./node_modules/next/dist/lib/fallback.d.ts","./node_modules/next/dist/compiled/webpack/webpack.d.ts","./node_modules/next/dist/server/config.d.ts","./node_modules/next/dist/lib/load-custom-routes.d.ts","./node_modules/next/dist/shared/lib/image-config.d.ts","./node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","./node_modules/next/dist/server/body-streams.d.ts","./node_modules/next/dist/server/lib/revalidate.d.ts","./node_modules/next/dist/lib/setup-exception-listeners.d.ts","./node_modules/next/dist/lib/worker.d.ts","./node_modules/next/dist/lib/constants.d.ts","./node_modules/next/dist/client/components/app-router-headers.d.ts","./node_modules/next/dist/build/rendering-mode.d.ts","./node_modules/next/dist/server/require-hook.d.ts","./node_modules/next/dist/server/lib/experimental/ppr.d.ts","./node_modules/next/dist/build/webpack/plugins/app-build-manifest-plugin.d.ts","./node_modules/next/dist/lib/page-types.d.ts","./node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","./node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","./node_modules/next/dist/build/analysis/get-page-static-info.d.ts","./node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","./node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","./node_modules/next/dist/server/route-kind.d.ts","./node_modules/next/dist/server/route-definitions/route-definition.d.ts","./node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","./node_modules/next/dist/server/lib/cache-handlers/types.d.ts","./node_modules/next/dist/server/response-cache/types.d.ts","./node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","./node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","./node_modules/next/dist/server/render-result.d.ts","./node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","./node_modules/next/dist/server/route-modules/route-module.d.ts","./node_modules/next/dist/shared/lib/deep-readonly.d.ts","./node_modules/next/dist/server/load-components.d.ts","./node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","./node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","./node_modules/next/dist/client/flight-data-helpers.d.ts","./node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","./node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","./node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","./node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","./node_modules/next/dist/shared/lib/mitt.d.ts","./node_modules/next/dist/client/with-router.d.ts","./node_modules/next/dist/client/router.d.ts","./node_modules/next/dist/client/route-loader.d.ts","./node_modules/next/dist/client/page-loader.d.ts","./node_modules/next/dist/shared/lib/bloom-filter.d.ts","./node_modules/next/dist/shared/lib/router/router.d.ts","./node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","./node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","./node_modules/next/dist/build/templates/pages.d.ts","./node_modules/next/dist/server/route-modules/pages/module.d.ts","./node_modules/next/dist/server/render.d.ts","./node_modules/next/dist/server/response-cache/index.d.ts","./node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","./node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","./node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","./node_modules/next/dist/server/instrumentation/types.d.ts","./node_modules/next/dist/server/route-matchers/route-matcher.d.ts","./node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","./node_modules/next/dist/server/lib/i18n-provider.d.ts","./node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","./node_modules/next/dist/server/normalizers/normalizer.d.ts","./node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/suffix.d.ts","./node_modules/next/dist/server/normalizers/request/rsc.d.ts","./node_modules/next/dist/server/normalizers/request/prefetch-rsc.d.ts","./node_modules/next/dist/server/normalizers/request/next-data.d.ts","./node_modules/next/dist/server/after/builtin-request-context.d.ts","./node_modules/next/dist/server/base-server.d.ts","./node_modules/next/dist/server/web/next-url.d.ts","./node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","./node_modules/next/dist/server/web/spec-extension/cookies.d.ts","./node_modules/next/dist/server/web/spec-extension/request.d.ts","./node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","./node_modules/next/dist/server/web/spec-extension/response.d.ts","./node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","./node_modules/next/dist/server/web/types.d.ts","./node_modules/next/dist/server/web/adapter.d.ts","./node_modules/next/dist/server/use-cache/cache-life.d.ts","./node_modules/next/dist/server/app-render/types.d.ts","./node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","./node_modules/next/dist/shared/lib/constants.d.ts","./node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","./node_modules/next/dist/build/page-extensions-type.d.ts","./node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","./node_modules/next/dist/server/lib/app-dir-module.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","./node_modules/next/dist/server/request/fallback-params.d.ts","./node_modules/next/dist/server/app-render/clean-async-snapshot-instance.d.ts","./node_modules/next/dist/server/app-render/clean-async-snapshot.external.d.ts","./node_modules/next/dist/server/app-render/app-render.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","./node_modules/@types/react/jsx-runtime.d.ts","./node_modules/next/dist/client/components/error-boundary.d.ts","./node_modules/next/dist/client/components/layout-router.d.ts","./node_modules/next/dist/client/components/render-from-template-context.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","./node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","./node_modules/next/dist/server/app-render/cache-signal.d.ts","./node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","./node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","./node_modules/next/dist/client/components/client-page.d.ts","./node_modules/next/dist/client/components/client-segment.d.ts","./node_modules/next/dist/server/request/search-params.d.ts","./node_modules/next/dist/client/components/hooks-server-context.d.ts","./node_modules/next/dist/client/components/not-found-boundary.d.ts","./node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","./node_modules/next/dist/lib/metadata/types/extra-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","./node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","./node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","./node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","./node_modules/next/dist/lib/metadata/types/resolvers.d.ts","./node_modules/next/dist/lib/metadata/metadata.d.ts","./node_modules/next/dist/lib/metadata/metadata-boundary.d.ts","./node_modules/next/dist/server/app-render/rsc/preloads.d.ts","./node_modules/next/dist/server/app-render/rsc/postpone.d.ts","./node_modules/next/dist/server/app-render/rsc/taint.d.ts","./node_modules/next/dist/server/app-render/collect-segment-data.d.ts","./node_modules/next/dist/server/app-render/entry-base.d.ts","./node_modules/next/dist/build/templates/app-page.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.d.ts","./node_modules/next/dist/server/node-polyfill-crypto.d.ts","./node_modules/next/dist/server/node-environment-baseline.d.ts","./node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","./node_modules/next/dist/server/node-environment-extensions/random.d.ts","./node_modules/next/dist/server/node-environment-extensions/date.d.ts","./node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","./node_modules/next/dist/server/node-environment.d.ts","./node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","./node_modules/next/dist/server/async-storage/work-store.d.ts","./node_modules/next/dist/server/web/http.d.ts","./node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","./node_modules/next/dist/client/components/redirect-status-code.d.ts","./node_modules/next/dist/client/components/redirect.d.ts","./node_modules/next/dist/build/templates/app-route.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","./node_modules/next/dist/build/segment-config/app/app-segments.d.ts","./node_modules/next/dist/build/utils.d.ts","./node_modules/next/dist/build/turborepo-access-trace/types.d.ts","./node_modules/next/dist/build/turborepo-access-trace/result.d.ts","./node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","./node_modules/next/dist/build/turborepo-access-trace/index.d.ts","./node_modules/next/dist/export/types.d.ts","./node_modules/next/dist/export/worker.d.ts","./node_modules/next/dist/build/worker.d.ts","./node_modules/next/dist/build/index.d.ts","./node_modules/next/dist/server/lib/incremental-cache/index.d.ts","./node_modules/next/dist/server/after/after.d.ts","./node_modules/next/dist/server/after/after-context.d.ts","./node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","./node_modules/next/dist/server/request/params.d.ts","./node_modules/next/dist/server/route-matches/route-match.d.ts","./node_modules/next/dist/server/request-meta.d.ts","./node_modules/next/dist/cli/next-test.d.ts","./node_modules/next/dist/server/config-shared.d.ts","./node_modules/next/dist/server/base-http/index.d.ts","./node_modules/next/dist/server/api-utils/index.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","./node_modules/next/dist/server/base-http/node.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","./node_modules/next/dist/server/image-optimizer.d.ts","./node_modules/next/dist/server/next-server.d.ts","./node_modules/next/dist/lib/coalesced-function.d.ts","./node_modules/next/dist/server/lib/router-utils/types.d.ts","./node_modules/next/dist/trace/types.d.ts","./node_modules/next/dist/trace/trace.d.ts","./node_modules/next/dist/trace/shared.d.ts","./node_modules/next/dist/trace/index.d.ts","./node_modules/next/dist/build/load-jsconfig.d.ts","./node_modules/next/dist/build/webpack-config.d.ts","./node_modules/next/dist/build/swc/generated-native.d.ts","./node_modules/next/dist/build/swc/types.d.ts","./node_modules/next/dist/server/dev/parse-version-info.d.ts","./node_modules/next/dist/client/components/react-dev-overlay/types.d.ts","./node_modules/next/dist/server/dev/hot-reloader-types.d.ts","./node_modules/next/dist/telemetry/storage.d.ts","./node_modules/next/dist/server/lib/types.d.ts","./node_modules/next/dist/server/lib/render-server.d.ts","./node_modules/next/dist/server/lib/router-server.d.ts","./node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","./node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","./node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","./node_modules/next/dist/server/lib/lru-cache.d.ts","./node_modules/next/dist/server/lib/dev-bundler-service.d.ts","./node_modules/next/dist/server/dev/static-paths-worker.d.ts","./node_modules/next/dist/server/dev/next-dev-server.d.ts","./node_modules/next/dist/server/next.d.ts","./node_modules/next/dist/types.d.ts","./node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","./node_modules/@next/env/dist/index.d.ts","./node_modules/next/dist/shared/lib/utils.d.ts","./node_modules/next/dist/pages/_app.d.ts","./node_modules/next/app.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","./node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","./node_modules/next/dist/server/use-cache/cache-tag.d.ts","./node_modules/next/cache.d.ts","./node_modules/next/dist/shared/lib/runtime-config.external.d.ts","./node_modules/next/config.d.ts","./node_modules/next/dist/pages/_document.d.ts","./node_modules/next/document.d.ts","./node_modules/next/dist/shared/lib/dynamic.d.ts","./node_modules/next/dynamic.d.ts","./node_modules/next/dist/pages/_error.d.ts","./node_modules/next/error.d.ts","./node_modules/next/dist/shared/lib/head.d.ts","./node_modules/next/head.d.ts","./node_modules/next/dist/server/request/cookies.d.ts","./node_modules/next/dist/server/request/headers.d.ts","./node_modules/next/dist/server/request/draft-mode.d.ts","./node_modules/next/headers.d.ts","./node_modules/next/dist/shared/lib/get-img-props.d.ts","./node_modules/next/dist/client/image-component.d.ts","./node_modules/next/dist/shared/lib/image-external.d.ts","./node_modules/next/image.d.ts","./node_modules/next/dist/client/link.d.ts","./node_modules/next/link.d.ts","./node_modules/next/dist/client/components/not-found.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.d.ts","./node_modules/next/dist/client/components/navigation.react-server.d.ts","./node_modules/next/dist/client/components/navigation.d.ts","./node_modules/next/navigation.d.ts","./node_modules/next/router.d.ts","./node_modules/next/dist/client/script.d.ts","./node_modules/next/script.d.ts","./node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","./node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","./node_modules/next/dist/server/web/spec-extension/image-response.d.ts","./node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/types.d.ts","./node_modules/next/dist/server/after/index.d.ts","./node_modules/next/dist/server/request/connection.d.ts","./node_modules/next/server.d.ts","./node_modules/next/types/global.d.ts","./node_modules/next/types/compiled.d.ts","./node_modules/next/types.d.ts","./node_modules/next/index.d.ts","./node_modules/next/image-types/global.d.ts","./next-env.d.ts","./app/utils/api.ts","./node_modules/next/dist/compiled/@next/font/dist/types.d.ts","./node_modules/next/dist/compiled/@next/font/dist/google/index.d.ts","./node_modules/next/font/google/index.d.ts","./node_modules/@tanstack/query-core/build/legacy/_tsup-dts-rollup.d.ts","./node_modules/@tanstack/query-core/build/legacy/index.d.ts","./node_modules/@tanstack/react-query/build/legacy/_tsup-dts-rollup.d.ts","./node_modules/@tanstack/react-query/build/legacy/index.d.ts","./node_modules/lucide-react/dist/lucide-react.d.ts","./app/utils/toast.tsx","./app/providers.tsx","./app/layout.tsx","./app/page.tsx","./components/sidebarlayout.tsx","./app/attendance/page.tsx","./app/audit/page.tsx","./node_modules/echarts/types/dist/echarts.d.ts","./node_modules/echarts/index.d.ts","./node_modules/echarts-for-react/lib/types.d.ts","./node_modules/echarts-for-react/lib/core.d.ts","./node_modules/echarts-for-react/lib/index.d.ts","./app/dashboard/page.tsx","./app/employees/page.tsx","./app/enroll/[id]/page.tsx","./app/kiosk/page.tsx","./app/reports/page.tsx","./app/settings/page.tsx","./.next/types/cache-life.d.ts","./.next/types/app/layout.ts","./.next/types/app/page.ts","./.next/types/app/attendance/page.ts","./.next/types/app/audit/page.ts","./.next/types/app/dashboard/page.ts","./.next/types/app/employees/page.ts","./.next/types/app/enroll/[id]/page.ts","./.next/types/app/kiosk/page.ts","./.next/types/app/reports/page.ts","./.next/types/app/settings/page.ts","../../../../node_modules/@types/connect/index.d.ts","../../../../node_modules/@types/body-parser/index.d.ts","../../../../node_modules/@types/bonjour/index.d.ts","../../../../node_modules/@types/send/index.d.ts","../../../../node_modules/@types/qs/index.d.ts","../../../../node_modules/@types/range-parser/index.d.ts","../../../../node_modules/@types/express-serve-static-core/index.d.ts","../../../../node_modules/@types/connect-history-api-fallback/index.d.ts","../../../../node_modules/@types/estree/index.d.ts","../../../../node_modules/@types/json-schema/index.d.ts","../../../../node_modules/@types/eslint/use-at-your-own-risk.d.ts","../../../../node_modules/@types/eslint/index.d.ts","../../../../node_modules/@types/eslint-scope/index.d.ts","../../../../node_modules/@types/http-errors/index.d.ts","../../../../node_modules/@types/mime/index.d.ts","../../../../node_modules/@types/serve-static/node_modules/@types/send/index.d.ts","../../../../node_modules/@types/serve-static/index.d.ts","../../../../node_modules/@types/express/index.d.ts","../../../../node_modules/@types/http-proxy/index.d.ts","../../../../node_modules/@types/node-forge/index.d.ts","../../../../node_modules/@types/retry/index.d.ts","../../../../node_modules/@types/serve-index/index.d.ts","../../../../node_modules/@types/sockjs/index.d.ts","../../../../node_modules/@types/ws/index.d.ts"],"fileIdsList":[[99,146,160,194,501],[99,146,152,194],[99,146,187,194,507],[99,146,160,194],[99,146,509,512],[99,146,509,510,511],[99,146,512],[99,146],[99,146,157,160,194,504,505,506],[99,146,502,505,507,517],[99,146,157,160,162,165,176,187,194],[99,146,194],[99,146,158,176,194],[99,146,158,518],[99,146,160,194,514,516],[99,146,158,176,194,515],[99,146,157,160,162,165,176,184,187,193,194],[99,146,328,477],[99,146,328,478],[99,146,328,484],[99,146,328,485],[99,146,328,486],[99,146,328,487],[99,146,328,474],[99,146,328,475],[99,146,328,488],[99,146,328,489],[99,146,415,416,417,418],[85,99,146,463,470,471,472,476],[85,99,146,463,470,471,476],[85,99,146,439,463,470,471,476,483],[85,99,146,439,463,470,471,472,476],[85,99,146,444,463,470,471,476],[85,99,146,463,471,472],[99,146,460,466,473],[85,99,146,444,463,471],[85,99,146,470,472],[85,99,146,471],[85,99,146,439,444,463,471],[99,146,460,461],[99,146,467],[85,99,146,304,468],[99,146,469],[99,143,146],[99,145,146],[146],[99,146,151,179],[99,146,147,152,157,165,176,187],[99,146,147,148,157,165],[94,95,96,99,146],[99,146,149,188],[99,146,150,151,158,166],[99,146,151,176,184],[99,146,152,154,157,165],[99,145,146,153],[99,146,154,155],[99,146,156,157],[99,145,146,157],[99,146,157,158,159,176,187],[99,146,157,158,159,172,176,179],[99,146,154,157,160,165,176,187],[99,146,157,158,160,161,165,176,184,187],[99,146,160,162,176,184,187],[97,98,99,100,101,102,103,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193],[99,146,157,163],[99,146,164,187,192],[99,146,154,157,165,176],[99,146,166],[99,146,167],[99,145,146,168],[99,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193],[99,146,170],[99,146,171],[99,146,157,172,173],[99,146,172,174,188,190],[99,146,157,176,177,179],[99,146,178,179],[99,146,176,177],[99,146,179],[99,146,180],[99,143,146,176,181],[99,146,157,182,183],[99,146,182,183],[99,146,151,165,176,184],[99,146,185],[99,146,165,186],[99,146,160,171,187],[99,146,151,188],[99,146,176,189],[99,146,164,190],[99,146,191],[99,141,146],[99,141,146,157,159,168,176,179,187,190,192],[99,146,176,193],[85,89,99,146,195,196,197,199,409,453],[85,89,99,146,195,196,197,198,409,453],[85,89,99,146,195,196,198,199,409,453],[85,89,99,146,196,197,198,199,409,453],[85,89,99,146,195,197,198,199,409,453],[83,84,99,146],[85,99,146],[85,99,146,480,481],[99,146,481,482],[85,99,146,480],[99,146,479],[91,99,146],[99,146,413],[99,146,420],[99,146,203,216,217,218,220,375],[99,146,203,207,209,210,211,212,364,375,377],[99,146,375],[99,146,217,233,303,355,371],[99,146,203],[99,146,393],[99,146,375,377,392],[99,146,296,303,336,458],[99,146,314,320,355,370],[99,146,258],[99,146,359],[99,146,358,359,360],[99,146,358],[93,99,146,160,200,203,210,213,214,215,217,221,289,294,338,346,356,366,375,409],[99,146,203,219,247,292,375,389,390,458],[99,146,219,458],[99,146,292,293,294,375,458],[99,146,458],[99,146,203,219,220,458],[99,146,213,357,363],[99,146,171,304,371],[99,146,304,371],[85,99,146,304],[85,99,146,290,304,305],[99,146,238,256,371,442],[99,146,352,440,441],[99,146,351],[99,146,235,236,290],[99,146,237,238,290],[99,146,290],[85,99,146,204,434],[85,99,146,187],[85,99,146,219,245],[85,99,146,219],[99,146,243,248],[85,99,146,244,412],[99,146,464],[85,89,99,146,160,194,195,196,197,198,199,409,451,452],[99,146,158,160,207,233,261,279,290,361,375,376,458],[99,146,346,362],[99,146,409],[99,146,202],[99,146,171,296,301,329,370,371],[99,146,322,323,324,325,326,327],[99,146,324],[99,146,328],[85,99,146,244,304,412],[85,99,146,304,410,412],[85,99,146,304,412],[99,146,279,367],[99,146,367],[99,146,160,376,412],[99,146,316],[99,145,146,315],[99,146,229,230,232,262,290,297,298,300,338,370,373,376],[99,146,299],[99,146,230],[99,146,314,370],[99,146,305,306,307,314,316,317,318,319,320,321,330,331,332,333,334,335,370,371,458],[99,146,312],[99,146,160,171,207,228,230,232,233,234,238,266,279,288,289,338,366,375,376,377,409,458],[99,146,370],[99,145,146,217,232,289,298,320,366,368,369,376],[99,146,314],[99,145,146,228,262,282,308,309,310,311,312,313],[99,146,160,282,283,308,376,377],[99,146,217,279,289,290,298,366,370,376],[99,146,160,375,377],[99,146,160,176,373,376,377],[99,146,160,171,187,200,207,219,229,230,232,233,234,239,261,262,263,265,266,269,270,272,275,276,277,278,290,365,366,371,373,375,376,377],[99,146,160,176],[99,146,203,204,205,207,214,373,374,409,412,458],[99,146,160,176,187,223,391,393,394,395,458],[99,146,171,187,200,223,233,262,263,270,279,287,290,366,371,373,378,379,383,389,405,406],[99,146,213,214,289,346,357,366,375],[99,146,160,187,204,262,373,375],[99,146,295],[99,146,160,398,403,404],[99,146,373,375],[99,146,207,232,262,365,412],[99,146,385,389,405,408],[99,146,160,213,346,389,398,399,408],[99,146,203,239,365,375,401],[99,146,160,219,239,375,384,385,396,397,400,402],[93,99,146,230,231,232,409,412],[99,146,160,171,187,207,213,221,229,233,234,262,263,265,266,279,287,290,346,365,366,371,372,373,378,379,381,382,412],[99,146,160,213,373,383,403,407],[99,146,341,342,343,344,345],[99,146,269,271],[99,146,273],[99,146,271],[99,146,273,274],[99,146,160,207,228,376],[85,99,146,160,171,202,204,207,229,230,232,233,234,260,373,377,409,412],[99,146,160,171,187,206,211,262,372,376],[99,146,308],[99,146,309],[99,146,310],[99,146,222,226],[99,146,160,207,222,229],[99,146,225,226],[99,146,227],[99,146,222,223],[99,146,222,240],[99,146,222],[99,146,268,269,372],[99,146,267],[99,146,223,371,372],[99,146,264,372],[99,146,223,371],[99,146,338],[99,146,224,229,231,262,290,296,298,301,302,337,373,376],[99,146,238,249,252,253,254,255,256],[99,146,354],[99,146,217,231,232,283,290,314,316,320,347,348,349,350,352,353,356,365,370,375],[99,146,238],[99,146,260],[99,146,160,229,231,241,257,259,261,373,409,412],[99,146,238,249,250,251,252,253,254,255,256,410],[99,146,223],[99,146,283,284,287,366],[99,146,160,269,375],[99,146,160],[99,146,282,314],[99,146,281],[99,146,278,283],[99,146,280,282,375],[99,146,160,206,283,284,285,286,375,376],[85,99,146,235,237,290],[99,146,291],[85,99,146,204],[85,99,146,371],[85,93,99,146,232,234,409,412],[99,146,204,434,435],[85,99,146,248],[85,99,146,171,187,202,242,244,246,247,412],[99,146,219,371,376],[99,146,371,380],[85,99,146,158,160,171,202,248,292,409,410,411],[85,99,146,195,196,197,198,199,409,453],[85,86,87,88,89,99,146],[99,146,151],[99,146,386,387,388],[99,146,386],[85,89,99,146,160,162,171,194,195,196,197,198,199,200,202,266,328,377,408,412,453],[99,146,422],[99,146,424],[99,146,426],[99,146,465],[99,146,428],[99,146,430,431,432],[99,146,436],[90,92,99,146,414,419,421,423,425,427,429,433,437,439,444,445,447,456,457,458,459],[99,146,438],[99,146,443],[99,146,244],[99,146,446],[99,145,146,283,284,285,287,319,371,448,449,450,453,454,455],[99,113,117,146,187],[99,113,146,176,187],[99,108,146],[99,110,113,146,184,187],[99,146,165,184],[99,108,146,194],[99,110,113,146,165,187],[99,105,106,109,112,146,157,176,187],[99,113,120,146],[99,105,111,146],[99,113,134,135,146],[99,109,113,146,179,187,194],[99,134,146,194],[99,107,108,146,194],[99,113,146],[99,107,108,109,110,111,112,113,114,115,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,135,136,137,138,139,140,146],[99,113,128,146],[99,113,120,121,146],[99,111,113,121,122,146],[99,112,146],[99,105,108,113,146],[99,113,117,121,122,146],[99,117,146],[99,111,113,116,146,187],[99,105,110,113,120,146],[99,146,176],[99,108,113,134,146,192,194]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"dc782ff85b2cb10075ecffc158af7bfb27ff97bf8491c917efea0c3d622d5ac4","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","impliedFormat":1},{"version":"631eff75b0e35d1b1b31081d55209abc43e16b49426546ab5a9b40bdd40b1f60","impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"ba481bca06f37d3f2c137ce343c7d5937029b2468f8e26111f3c9d9963d6568d","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d9ef24f9a22a88e3e9b3b3d8c40ab1ddb0853f1bfbd5c843c37800138437b61","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e9c23ba78aabc2e0a27033f18737a6df754067731e69dc5f52823957d60a4b6","impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2677634fe27e87348825bb041651e22d50a613e2fdf6a4a3ade971d71bac37e","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"8c0bcd6c6b67b4b503c11e91a1fb91522ed585900eab2ab1f61bba7d7caa9d6f","impliedFormat":1},{"version":"8cd19276b6590b3ebbeeb030ac271871b9ed0afc3074ac88a94ed2449174b776","affectsGlobalScope":true,"impliedFormat":1},{"version":"696eb8d28f5949b87d894b26dc97318ef944c794a9a4e4f62360cd1d1958014b","impliedFormat":1},{"version":"3f8fa3061bd7402970b399300880d55257953ee6d3cd408722cb9ac20126460c","impliedFormat":1},{"version":"35ec8b6760fd7138bbf5809b84551e31028fb2ba7b6dc91d95d098bf212ca8b4","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"68bd56c92c2bd7d2339457eb84d63e7de3bd56a69b25f3576e1568d21a162398","affectsGlobalScope":true,"impliedFormat":1},{"version":"3e93b123f7c2944969d291b35fed2af79a6e9e27fdd5faa99748a51c07c02d28","impliedFormat":1},{"version":"9d19808c8c291a9010a6c788e8532a2da70f811adb431c97520803e0ec649991","impliedFormat":1},{"version":"87aad3dd9752067dc875cfaa466fc44246451c0c560b820796bdd528e29bef40","impliedFormat":1},{"version":"4aacb0dd020eeaef65426153686cc639a78ec2885dc72ad220be1d25f1a439df","impliedFormat":1},{"version":"f0bd7e6d931657b59605c44112eaf8b980ba7f957a5051ed21cb93d978cf2f45","impliedFormat":1},{"version":"8db0ae9cb14d9955b14c214f34dae1b9ef2baee2fe4ce794a4cd3ac2531e3255","affectsGlobalScope":true,"impliedFormat":1},{"version":"15fc6f7512c86810273af28f224251a5a879e4261b4d4c7e532abfbfc3983134","impliedFormat":1},{"version":"58adba1a8ab2d10b54dc1dced4e41f4e7c9772cbbac40939c0dc8ce2cdb1d442","impliedFormat":1},{"version":"641942a78f9063caa5d6b777c99304b7d1dc7328076038c6d94d8a0b81fc95c1","impliedFormat":1},{"version":"70e15d5c5c14e26442b2f15484b126a982290c5f4713834a6e572fd4274f9119","impliedFormat":1},{"version":"855cd5f7eb396f5f1ab1bc0f8580339bff77b68a770f84c6b254e319bbfd1ac7","impliedFormat":1},{"version":"5650cf3dace09e7c25d384e3e6b818b938f68f4e8de96f52d9c5a1b3db068e86","impliedFormat":1},{"version":"1354ca5c38bd3fd3836a68e0f7c9f91f172582ba30ab15bb8c075891b91502b7","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e20d899c28ca26a2a7afc98beaa69e63ff7fba0a8bc47b4e3bf3ede5e09e424","impliedFormat":1},{"version":"2d2fcaab481b31a5882065c7951255703ddbe1c0e507af56ea42d79ac3911201","impliedFormat":1},{"version":"a192fe8ec33f75edbc8d8f3ed79f768dfae11ff5735e7fe52bfa69956e46d78d","impliedFormat":1},{"version":"ca867399f7db82df981d6915bcbb2d81131d7d1ef683bc782b59f71dda59bc85","affectsGlobalScope":true,"impliedFormat":1},{"version":"372413016d17d804e1d139418aca0c68e47a83fb6669490857f4b318de8cccb3","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e043a1bc8fbf2a255bccf9bf27e0f1caf916c3b0518ea34aa72357c0afd42ec","impliedFormat":1},{"version":"b4f70ec656a11d570e1a9edce07d118cd58d9760239e2ece99306ee9dfe61d02","impliedFormat":1},{"version":"3bc2f1e2c95c04048212c569ed38e338873f6a8593930cf5a7ef24ffb38fc3b6","impliedFormat":1},{"version":"6e70e9570e98aae2b825b533aa6292b6abd542e8d9f6e9475e88e1d7ba17c866","impliedFormat":1},{"version":"f9d9d753d430ed050dc1bf2667a1bab711ccbb1c1507183d794cc195a5b085cc","impliedFormat":1},{"version":"9eece5e586312581ccd106d4853e861aaaa1a39f8e3ea672b8c3847eedd12f6e","impliedFormat":1},{"version":"085f552d005479e2e6a7311cdbbe5d8c55c497b4d19274285df161ee9684cd9c","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"007faacc9268357caa21d24169f3f3f2497af3e9241308df2d89f6e6d9bb3f2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"74cf591a0f63db318651e0e04cb55f8791385f86e987a67fd4d2eaab8191f730","impliedFormat":1},{"version":"5eab9b3dc9b34f185417342436ec3f106898da5f4801992d8ff38ab3aff346b5","impliedFormat":1},{"version":"12ed4559eba17cd977aa0db658d25c4047067444b51acfdcbf38470630642b23","affectsGlobalScope":true,"impliedFormat":1},{"version":"f3ffabc95802521e1e4bcba4c88d8615176dc6e09111d920c7a213bdda6e1d65","impliedFormat":1},{"version":"809821b8a065e3234a55b3a9d7846231ed18d66dd749f2494c66288d890daf7f","impliedFormat":1},{"version":"ae56f65caf3be91108707bd8dfbccc2a57a91feb5daabf7165a06a945545ed26","impliedFormat":1},{"version":"a136d5de521da20f31631a0a96bf712370779d1c05b7015d7019a9b2a0446ca9","impliedFormat":1},{"version":"c3b41e74b9a84b88b1dca61ec39eee25c0dbc8e7d519ba11bb070918cfacf656","affectsGlobalScope":true,"impliedFormat":1},{"version":"4737a9dc24d0e68b734e6cfbcea0c15a2cfafeb493485e27905f7856988c6b29","affectsGlobalScope":true,"impliedFormat":1},{"version":"36d8d3e7506b631c9582c251a2c0b8a28855af3f76719b12b534c6edf952748d","impliedFormat":1},{"version":"1ca69210cc42729e7ca97d3a9ad48f2e9cb0042bada4075b588ae5387debd318","impliedFormat":1},{"version":"f5ebe66baaf7c552cfa59d75f2bfba679f329204847db3cec385acda245e574e","impliedFormat":1},{"version":"ed59add13139f84da271cafd32e2171876b0a0af2f798d0c663e8eeb867732cf","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7c5e2ea4a9749097c347454805e933844ed207b6eefec6b7cfd418b5f5f7b28","impliedFormat":1},{"version":"b1810689b76fd473bd12cc9ee219f8e62f54a7d08019a235d07424afbf074d25","impliedFormat":1},{"version":"2beff543f6e9a9701df88daeee3cdd70a34b4a1c11cb4c734472195a5cb2af54","impliedFormat":1},{"version":"2e07abf27aa06353d46f4448c0bbac73431f6065eef7113128a5cd804d0c384d","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"3da0083607976261730c44908eab1b6262f727747ef3230a65ecd0153d9e8639","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"dd721e5707f241e4ef4ab36570d9e2a79f66aad63a339e3cbdbac7d9164d2431","impliedFormat":1},{"version":"24f8562308dd8ba6013120557fa7b44950b619610b2c6cb8784c79f11e3c4f90","impliedFormat":1},{"version":"3849a7f92d0e11b785f6ae7bedb25d9aad8d1234b3f1cf530a4e7404be26dd0a","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"57d6ac03382e30e9213641ff4f18cf9402bb246b77c13c8e848c0b1ca2b7ef92","impliedFormat":1},{"version":"f040772329d757ecd38479991101ef7bc9bf8d8f4dd8ee5d96fe00aa264f2a2b","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"57e47d02e88abef89d214cdf52b478104dc17997015746e288cbb580beaef266","impliedFormat":1},{"version":"e621da3d09c38e896cc2d432cbfec5af3f08739a3e64fb52f8723df6764edab9","impliedFormat":1},{"version":"376c21ad92ca004531807ea4498f90a740fd04598b45a19335a865408180eddd","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"a9af0e608929aaf9ce96bd7a7b99c9360636c31d73670e4af09a09950df97841","impliedFormat":1},{"version":"48d37b90a04e753a925228f50304d02c4f95d57bf682f8bb688621c3cd9d32ec","impliedFormat":1},{"version":"361e2b13c6765d7f85bb7600b48fde782b90c7c41105b7dab1f6e7871071ba20","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"b6db56e4903e9c32e533b78ac85522de734b3d3a8541bf24d256058d464bf04b","impliedFormat":1},{"version":"24daa0366f837d22c94a5c0bad5bf1fd0f6b29e1fae92dc47c3072c3fdb2fbd5","impliedFormat":1},{"version":"b68c4ed987ef5693d3dccd85222d60769463aca404f2ffca1c4c42781dce388e","impliedFormat":1},{"version":"cfb5b5d514eb4ad0ee25f313b197f3baa493eee31f27613facd71efb68206720","impliedFormat":1},{"version":"65f43099ded6073336e697512d9b80f2d4fec3182b7b2316abf712e84104db00","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"dffe876972134f7ab6b7b9d0906317adb189716b922f55877190836d75d637ff","impliedFormat":1},{"version":"7840deea5b9a76aa60bc7283136a3ace1ed6ae08dfe4bac108e4322d6929f333","impliedFormat":1},{"version":"a5104342bf6b503b51eac266cecac1a441bdf10e4ad743b86cafa9fa5e4b9522","impliedFormat":1},{"version":"07aad78d0fb49288f29d4a852352a47ac8d45d16a333f0040dec849ba10b34f0","impliedFormat":1},{"version":"449eb009b315e1393839d57bfc9036b5ae54b23b95e2ae8f9309993b095dc99a","impliedFormat":1},{"version":"4c7845c76670c65f41a0116ea5f360b2e107f7b69b7a93972b60739282de3e7f","impliedFormat":1},{"version":"ef61792acbfa8c27c9bd113f02731e66229f7d3a169e3c1993b508134f1a58e0","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"973d2650149b7ec576d1a8195c8e9272f19c4a8efb31efe6ddc4ff98f0b9332d","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"fb6029bd56096befddfe0b98eaf23c2f794872610f8fa40dd63618a8d261ec6c","impliedFormat":1},{"version":"fe4860fa03b676d124ac61c8e7c405a83c67e1b10fc30f48c08b64aa1680098f","impliedFormat":1},{"version":"61d8276131ed263cb5323fbfdd1f1a6dd1920f30aedce0274aadcd2bfdc9a5ad","impliedFormat":1},{"version":"80cda0a68679f52326d99646814a8e98fec3051fd7fbed784fc9cd44fbc6fefa","impliedFormat":1},{"version":"a23185bc5ef590c287c28a91baf280367b50ae4ea40327366ad01f6f4a8edbc5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"8b8f00491431fe82f060dfe8c7f2180a9fb239f3d851527db909b83230e75882","affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a020158a317c07774393974d26723af551e569f1ba4d6524e8e245f10e11b976","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"15b36126e0089bfef173ab61329e8286ce74af5e809d8a72edcafd0cc049057f","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d07cbc787a997d83f7bde3877fec5fb5b12ce8c1b7047eb792996ed9726b4dde","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"8bba776476c48b0e319d243f353190f24096057acede3c2f620fee17ff885dba","impliedFormat":1},{"version":"a3abe92070fbd33714bd837806030b39cfb1f8283a98c7c1f55fffeea388809e","impliedFormat":1},{"version":"ceb6696b98a72f2dae802260c5b0940ea338de65edd372ff9e13ab0a410c3a88","impliedFormat":1},{"version":"2cd914e04d403bdc7263074c63168335d44ce9367e8a74f6896c77d4d26a1038","impliedFormat":1},{"version":"ac60bbee0d4235643cc52b57768b22de8c257c12bd8c2039860540cab1fa1d82","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"208c9af9429dd3c76f5927b971263174aaa4bc7621ddec63f163640cbd3c473c","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"3bc8605900fd1668f6d93ce8e14386478b6caa6fda41be633ee0fe4d0c716e62","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"45490817629431853543adcb91c0673c25af52a456479588b6486daba34f68bb","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"40170617a96a979bb8d137240f39ecf62333e7d52b9ccf18d7a3c105051b087c","impliedFormat":1},{"version":"4aef12ed1bd4615a9cf6d251755753906e9db06804e88a1859bfd22453490453","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"fc3e1c87b39e5ba1142f27ec089d1966da168c04a859a4f6aab64dceae162c2b","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"f11d0dcaa4a1cba6d6513b04ceb31a262f223f56e18b289c0ba3133b4d3cd9a6","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"ca7ab3c16a196b851407e5dd43617c7c4d229115f4c38d5504b9210ed5c60837","impliedFormat":1},{"version":"56013416784a6b754f3855f8f2bf6ce132320679b8a435389aca0361bce4df6b","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"c6c801af2193e5935f66c003ee029fc939d9888b64f8ea7842229d512bf25b4e","impliedFormat":1},{"version":"002eae065e6960458bda3cf695e578b0d1e2785523476f8a9170b103c709cd4f","impliedFormat":1},{"version":"b8590c5d0a36dd9dad69399d765b511b41a6583e9521b95894010c45c7a5e962","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"8f30fb3ea68f4cc1c766f9aebb0682c3e955411477ea90bb4b81f35b2f48c5fd","impliedFormat":1},{"version":"ef33b8f373b674d6bf04d579a6f332e6fb2b66756ff653df41a78f966fd8d696","impliedFormat":1},{"version":"643672ce383e1c58ea665a92c5481f8441edbd3e91db36e535abccbc9035adeb","impliedFormat":1},{"version":"6dd9bcf10678b889842d467706836a0ab42e6c58711e33918ed127073807ee65","impliedFormat":1},{"version":"8fa022ea514ce0ea78ac9b7092a9f97f08ead20c839c779891019e110fce8307","impliedFormat":1},{"version":"c93235337600b786fd7d0ff9c71a00f37ca65c4d63e5d695fc75153be2690f09","impliedFormat":1},{"version":"713289d81d39985140b4641c47a9daf4617a13519e04a3b711405296d5c72a92","impliedFormat":1},{"version":"ce791f6ea807560f08065d1af6014581eeb54a05abd73294777a281b6dfd73c2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"b838d4c72740eb0afd284bf7575b74c624b105eff2e8c7b4aeead57e7ac320ff","impliedFormat":1},{"version":"c0a666b005521f52e2db0b685d659d7ee9b0b60bc0d347dfc5e826c7957bdb83","impliedFormat":1},{"version":"0a626484617019fcfbfc3c1bc1f9e84e2913f1adb73692aa9075817404fb41a1","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"4360ad4de54de2d5c642c4375d5eab0e7fe94ebe8adca907e6c186bbef75a54d","impliedFormat":1},{"version":"c338dff3233675f87a3869417aaea8b8bf590505106d38907dc1d0144f6402ef","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"9c9cae45dc94c2192c7d25f80649414fa13c425d0399a2c7cb2b979e4e50af42","impliedFormat":1},{"version":"068f063c2420b20f8845afadb38a14c640aed6bb01063df224edb24af92b4550","impliedFormat":1},{"version":"4bf183d06c039f0880141389ea403b17f4464455015fd5e91987a8c63301ba95","impliedFormat":1},{"version":"e56d6f3b3e00fdbe11c3a9da83b4693ca0814752da5610e90f4bc9c9a17cacd7","impliedFormat":1},{"version":"cf185cc4a9a6d397f416dd28cca95c227b29f0f27b160060a95c0e5e36cda865","impliedFormat":1},{"version":"6f6d8b734699387b60fcc8300efd98d967f4c255ace55f088a1b93d2c1f31ac6","impliedFormat":1},{"version":"e17cd049a1448de4944800399daa4a64c5db8657cc9be7ef46be66e2a2cd0e7c","impliedFormat":1},{"version":"d05fb434f4ba073aed74b6c62eff1723c835de2a963dbb091e000a2decb5a691","impliedFormat":1},{"version":"10e6166be454ddb8c81000019ce1069b476b478c316e7c25965a91904ec5c1e3","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"ad534b18336a35244d8838029974f6367d54fd96733a570062bcec065db52d2d","impliedFormat":1},{"version":"f8acecec1114f11690956e007d920044799aefeb3cece9e7f4b1f8a1d542b2c9","impliedFormat":1},{"version":"178071ccd043967a58c5d1a032db0ddf9bd139e7920766b537d9783e88eb615e","impliedFormat":1},{"version":"9b07d156d1db6d2e27cb0180470e16a7956258ebc86d2f757b554f81c1fed075","impliedFormat":1},{"version":"48d7da8c8d53a8601c9747297aab87408d35b5ddee2d2c8168a7dc3c83347c5e","impliedFormat":1},{"version":"0ce1b2237c1c3df49748d61568160d780d7b26693bd9feb3acb0744a152cd86d","impliedFormat":1},{"version":"e489985388e2c71d3542612685b4a7db326922b57ac880f299da7026a4e8a117","impliedFormat":1},{"version":"18e99839b1ec5ce200181657caa2b3ed830a693f3dea6a5a33c577e838576834","impliedFormat":1},{"version":"d973b85fc71be3e8733c670324631df1a5aa5b0d300b63b509724485e13edb01","impliedFormat":1},{"version":"5b2b575ac31335a49e3610820dda421eba4b50e385954647ebc0a8d462e8d0f7","impliedFormat":1},{"version":"9e21f8e2c0cfea713a4a372f284b60089c0841eb90bf3610539d89dbcd12d65a","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"d2bae957a5b34f39743d7814c0a35ba0c6a89fe5207001f4498477da17570f4b","impliedFormat":1},{"version":"5b2b799936191252416aa19455f19eafb7eb3fc8a996764bcf7d35937b6bd712","impliedFormat":1},{"version":"8eea4cc42d04d26bcbcaf209366956e9f7abaf56b0601c101016bb773730c5fe","impliedFormat":1},{"version":"f5319e38724c54dff74ee734950926a745c203dcce00bb0343cb08fbb2f6b546","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"12b8dfed70961bea1861e5d39e433580e71323abb5d33da6605182ec569db584","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"7e560f533aaf88cf9d3b427dcf6c112dd3f2ee26d610e2587583b6c354c753db","impliedFormat":1},{"version":"71e0082342008e4dfb43202df85ea0986ef8e003c921a1e49999d0234a3019da","impliedFormat":1},{"version":"27ab780875bcbb65e09da7496f2ca36288b0c541abaa75c311450a077d54ec15","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"745197115b3a563e70f8b6565425370bbfdf8c194c1a61dbc8048daa2b22df15","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"c7d5d3b5aac1e1b4f7cb6f64351aff02b3a2e98feda9bc8e5e40f35639cad9f2","impliedFormat":1},{"version":"794998dc1c5a19ce77a75086fe829fb9c92f2fd07b5631c7d5e0d04fd9bc540c","impliedFormat":1},{"version":"bd28a12377e68e1fedd4c3d36cbcc2d229f681ba459eff00bba58970b20fc167","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"0427df5c06fafc5fe126d14b9becd24160a288deff40e838bfbd92a35f8d0d00","impliedFormat":1},{"version":"f8ab24dcd2177ca88334f12d7ab07e14587cef5132437ddea18b5074f9860b22","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"6cd4b0986c638d92f7204d1407b1cb3e0a79d7a2d23b0f141c1a0829540ce7ef","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"d58265e159fc3cb30aa8878ba5e986a314b1759c824ff66d777b9fe42117231a","impliedFormat":1},{"version":"ff8fccaae640b0bb364340216dcc7423e55b6bb182ca2334837fee38636ad32e","impliedFormat":1},{"version":"3d4d58fe8bc7d5f6977cb33ddccf0e210ff75fb5e9d8b69ec4dafa1e64fc25fb","impliedFormat":1},{"version":"14b65941c926f5dd00e9fcc235cc471830042d43c41722fcb34589c54b610ed1","impliedFormat":1},{"version":"22bda3002a475e16a060062ca36bc666443f58af4aacf152ae0aaa00dd9ee2cc","impliedFormat":1},{"version":"36eab071c38859aa13b794e28014f34fb4e17659c82aeda8d841f77e727bff27","impliedFormat":1},{"version":"c590195790d7fa35b4abed577a605d283b8336b9e01fa9bf4ae4be49855940f9","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"026a43d8239b8f12d2fc4fa5a7acbc2ad06dd989d8c71286d791d9f57ca22b78","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"6020d077de5ca3d10712991b7739cea2d41897912f25e079c11af386327eba03","impliedFormat":1},{"version":"12f0fb50e28b9d48fe5b7580580efe7cc0bd38e4b8c02d21c175aa9a4fd839b0","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"7cd657e359eac7829db5f02c856993e8945ffccc71999cdfb4ab3bf801a1bbc6","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"29c2aa0712786a4a504fce3acd50928f086027276f7490965cb467d2ce638bae","impliedFormat":1},{"version":"f14e63395b54caecc486f00a39953ab00b7e4d428a4e2c38325154b08eb5dcc2","impliedFormat":1},{"version":"a0c132dc6ac49d78637c91c43d0808b32ea29d4f6f9b7eb01a142f06a6ab9a73","impliedFormat":1},{"version":"c3d2b0fcdcd32187917a6ea467510fb913c8e9ae199ee41a4af4a94c577c3bb4","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"29164fb428c851bc35b632761daad3ae075993a0bf9c43e9e3bc6468b32d9aa5","impliedFormat":1},{"version":"3c01539405051bffccacffd617254c8d0f665cdce00ec568c6f66ccb712b734f","impliedFormat":1},{"version":"ebd69e950c88b32530930299e4f5d06a3995b9424cb2c89b92f563e6300d79b3","impliedFormat":1},{"version":"70bea51bd3d87afe270228d4388c94d7ae1f0c6b43189c37406ba8b6acfba8df","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"96e1caae9b78cde35c62fee46c1ec9fa5f12c16bc1e2ab08d48e5921e29a6958","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"cf97b7e36e26871296e849751af2ee1c9727c9cc817b473cd9697d5bfc4fa4f3","impliedFormat":1},{"version":"e678acbb7d55cacfe74edcf9223cc32e8c600e14643941d03a0bf07905197b51","impliedFormat":1},{"version":"01aa917531e116485beca44a14970834687b857757159769c16b228eb1e49c5f","impliedFormat":1},{"version":"397f568f996f8ffcf12d9156342552b0da42f6571eadba6bce61c99e1651977d","impliedFormat":1},{"version":"78d8c61c0641960db72a65bc60e58c30e5b5bd46e621aad924bb7a421826673f","impliedFormat":1},{"version":"a52674bc98da7979607e0f44d4c015c59c1b1d264c83fc50ec79ff2cfea06723","impliedFormat":1},{"version":"57512aaaefbf4db2e8c7d47ee3762fa12c9521b324c93ea384d37b1b56aa7277","impliedFormat":1},{"version":"6aaa60c75563da35e4632a696b392851f64acacdb8b2b10656ebcf303f7e3569","impliedFormat":1},{"version":"6c7cd3294c6645be448eba2851c92c2931b7ddf84306805c5c502ea0ce345690","impliedFormat":1},{"version":"f26b8c8e4adf86c299a29d93c133a31aa60624b63664f28f1c917509e8cccbfb","impliedFormat":1},{"version":"82fd4516331cc701cedd54f3be2d31197dcf45f6cee6942b704b1f8ef061a2c6","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbedf8280e47eeb541717b79e24d9e5a10abc568480375466a3674d016b46704","impliedFormat":1},{"version":"81f95ded33d1980a5220502cc363311f3ef5558e8ab5557c6949b6265802259d","impliedFormat":1},{"version":"714d8ebb298c7acc9bd1f34bd479c57d12b73371078a0c5a1883a68b8f1b9389","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"3b0b1d352b8d2e47f1c4df4fb0678702aee071155b12ef0185fce9eb4fa4af1e","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"197047506e1db2b9a2986b07bd16873805f4244d8c8a3f03f9444cee4b2a5b9d","impliedFormat":1},{"version":"b7fff2d004c5879cae335db8f954eb1d61242d9f2d28515e67902032723caeab","impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","impliedFormat":1},{"version":"bb0cd7862b72f5eba39909c9889d566e198fcaddf7207c16737d0c2246112678","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"6812502cc640de74782ce9121592ae3765deb1c5c8e795b179736b308dd65e90","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"2b664c3cc544d0e35276e1fb2d4989f7d4b4027ffc64da34ec83a6ccf2e5c528","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"bad68fd0401eb90fe7da408565c8aee9c7a7021c2577aec92fa1382e8876071a","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"fec01479923e169fb52bd4f668dbeef1d7a7ea6e6d491e15617b46f2cacfa37d","impliedFormat":1},{"version":"8a8fb3097ba52f0ae6530ec6ab34e43e316506eb1d9aa29420a4b1e92a81442d","impliedFormat":1},{"version":"44e09c831fefb6fe59b8e65ad8f68a7ecc0e708d152cfcbe7ba6d6080c31c61e","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"b10bc147143031b250dc36815fd835543f67278245bf2d0a46dca765f215124e","impliedFormat":1},{"version":"87affad8e2243635d3a191fa72ef896842748d812e973b7510a55c6200b3c2a4","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"33777bfdf7c070a52fb5ad47e89c937ea833bef733478c5fd7f1164f9186e0e3","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"b685df59addf95ac6b09c594cc1e83b3d4a5b6f9f5eb06609720fee484a7b7ee","impliedFormat":1},{"version":"3c7b3aecd652169787b3c512d8f274a3511c475f84dcd6cead164e40cad64480","impliedFormat":1},{"version":"99b23c2c1986f5b5100b938f65336e49eca8679c532f641890a715d97aeff808","impliedFormat":1},{"version":"315d14addabfc08bcda173a9c2c79c70e831b6c2b38d7f1bb0ea3b58b59c15f1","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"b95a6f019095dd1d48fd04965b50dfd63e5743a6e75478343c46d2582a5132bf","impliedFormat":99},{"version":"c2008605e78208cfa9cd70bd29856b72dda7ad89df5dc895920f8e10bcb9cd0a","impliedFormat":99},{"version":"b97cb5616d2ab82a98ec9ada7b9e9cabb1f5da880ec50ea2b8dc5baa4cbf3c16","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"ec866055bdff922e6b319e537386dccbd768e1750ad91b095593444942dedf11","affectsGlobalScope":true,"impliedFormat":1},{"version":"272a7e7dbe05e8aaba1662ef1a16bbd57975cc352648b24e7a61b7798f3a0ad7","affectsGlobalScope":true,"impliedFormat":1},{"version":"a1219ee18b9282b4c6a31f1f0bcc9255b425e99363268ba6752a932cf76662f0","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"1d63055b690a582006435ddd3aa9c03aac16a696fac77ce2ed808f3e5a06efab","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},"e462a655754db9df18b4a657454a7b6a88717ffded4e89403b2b3a47c6603fc3","b11e98774818115d4884dadc533983ae2c7cf191cd942cdf8a53a89c71669246",{"version":"fe93c474ab38ac02e30e3af073412b4f92b740152cf3a751fdaee8cbea982341","impliedFormat":1},{"version":"ad878aa7fdc49e82e47749a6cc284a4903fc2cf62fe8ef189dac13734a4e44fa","impliedFormat":1},{"version":"1e00b8bf9e3766c958218cd6144ffe08418286f89ff44ba5a2cc830c03dd22c7","impliedFormat":1},{"version":"73c078fcbc0fa04ba70b1c3e5a3dea6a980d8765079cdbfb40f903eb8daa4319","impliedFormat":99},{"version":"5297e84d3de08bbe3c00f964d1c74f89cf101d59a4826b335654f44ff41529a8","impliedFormat":99},{"version":"355b33af59287683501f76cbf7d6a141544c5ff1ae5f5c0701a3f89cc38e5238","impliedFormat":99},{"version":"280a996092ab956e80dc7bb7497d472ca5c1be23a9c52ac771f5c750ede462b9","impliedFormat":99},{"version":"76f7360f36a5265a80db1106c3f96f7155a90834a842572de47f87cb02b770bd","impliedFormat":1},{"version":"967a7b3760644baa436d7e894662bffa3f74b11e54de26e98bcf75f78a9d6df8","signature":"9f6c26e8741c14626547785e954f8a521677c6daa181a50730255b5eba213461"},{"version":"2f8cba8848fdfc4fdfe240d8ee7986c9a192e09726e409f7ff0b5821952e4462","signature":"cd81285ebfb0c63cd175d4e46fb6327c68fb1b623e0cd9e531cbafd2af31b104"},"4eb61bb13ef554bda7d5aac3d664957ea516883b8e3b5f265d779baef1f0e562",{"version":"bf30ee28866f6fa28ae4ff2e8179503fd9dc3d438c4e72ffea39f4516a154580","signature":"d0ea9fb700a7b8df441d1b70b53cef5a6246ad338f581f57f5a7295ed3faa684"},{"version":"b9118308d52b5efd93a889fe8957bc094ffc99276e42ebe4071a36e417d81711","signature":"d92ca184dc154e0c54633d748044a4c40428e4ac9c84a6404c1850d8427978a0"},{"version":"0a7c5b0653484c9b5397c91fe6a95889ce3690cf6727aaa53fe07816e9524537","signature":"dd24c8e77286762b0fe0cb1ac1fbcf8b56c20f270fcdc5065fbc5d951de5a229"},"ef4b4e3d5e8eac7b8d583018a95cd2e357a8bac24ec02034c7b641e661928fa6",{"version":"6392353adcff7db02a3f5dcacb5637b791dbbcb76125aac3075da2519af9785a","impliedFormat":99},{"version":"1f3952b74b8c766a2e602a0ba2db19d3d872d00bab4e01746c6b7229c585086c","impliedFormat":99},{"version":"d02858f618f372be74ac6fc803b19fe0e1a1468da517813f450861f7f0ea8097","impliedFormat":1},{"version":"cd4031a2eae33de12cf8af8f97fd20c2d10047e7de9038f4d71e76b730bee127","impliedFormat":1},{"version":"6f41ed944dca7ffcb3d85a71434cd91167cc91ca24967909a79fc6e10f3411bb","impliedFormat":1},{"version":"f8c7fb94f0845aadb40111660df8c1e19a5f0c0245a9283fb225669df3962f57","signature":"9e9184640db072253117c68486ff81928b373fa206e9c9fd015f75929d72caab"},{"version":"a47007011570eda48c8b5b0331b43d9a070cca13a3730cb1a5cdbc5b126b1de5","signature":"95b9f088185647848112b28f9c0d42363de4dcdbd7c7415710c7cc243a1cc05c"},{"version":"13bc7f98a2d71375a2612802003df1e23e034073e36ce55915cb03aa227dce8f","signature":"1f01e90ffc9f0ef9673d8144fba39e9bfc7be8c363e7ded50196bf762b7a512f"},{"version":"8a1201ee1c58ba098495eed6f65d5ee7580ecf26cfc39ca7cb9fddf3b230aac2","signature":"a79011835bbabea49e2c94a318faef60aac67715519b72c6c44d39163230fa1f"},{"version":"3bb78bdddad4ae2e5022aafafa3a5b618fd367def266ee975809a6f8f715af2a","signature":"caa891d8c0a72d95687a7e0722eb8703956e21ca2d5d995c0dfe39b0934c5786"},{"version":"d27c7cd73550e9484e488eba3d951583dc91bf8ece1402a45f0e1d3c0c63c530","signature":"4222f8043b033b82ff30a49281b22f7e15b7701a91dfa38fdb962822bb9cf613"},"4e5346437598bc926a3ce75ed44ebbadd98b2722ad6ce99568cb1b151db5b2ab","8e1e1eae82d463d852b40e927d773e63965f59dc72526ccf4b26187ea51bc040","cfe152d37347c865e4aa52e4dec9bf8f4dae080bd0a2e34bc33a8287dd56df99","ea4ebe18f586edc5096c55a34c6715e8f2f8a49e85fea61e2fcfb654014646fe","7cab244b2b1b8b894708ffaf811a1f6bb1893b95e49dc94b9cf320639b37eb01","a29ed94e5cc9e93ed7ab7056e6d2ffa520be1453f0826f62564c88fa39fb8ef7","38f13037cd2d678081dd0e6bf936a3c0e2c0681bf8a1717bea95946b8c101a5d",{"version":"a0b3bcdf19b537bd04cdc0708fcfbf32375d7e6be51d81a11c3bf1319f36a8e4","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},"ed61940c34b3083345b4d1d1cbf54dfbe8c7d2aaccee0b17221b87bc07b5b662","a24e6a4c2fd1150ff5083ca8a8c3666184461c6553782598eb4b44832e87a92d","37c33aee2e46a532aa6e4c537a3afd9e8de53de26d2078bc7b8d59b7756244af",{"version":"104c67f0da1bdf0d94865419247e20eded83ce7f9911a1aa75fc675c077ca66e","impliedFormat":1},{"version":"cc0d0b339f31ce0ab3b7a5b714d8e578ce698f1e13d7f8c60bfb766baeb1d35c","impliedFormat":1},{"version":"f9e22729fa06ed20f8b1fe60670b7c74933fdfd44d869ddfb1919c15a5cf12fb","impliedFormat":1},{"version":"d34aa8df2d0b18fb56b1d772ff9b3c7aea7256cf0d692f969be6e1d27b74d660","impliedFormat":1},{"version":"f4db16820c99b6db923ab18af5fecb02331d785c4c2a8a88373a0cfc08256589","impliedFormat":1},{"version":"2f5747b1508ccf83fad0c251ba1e5da2f5a30b78b09ffa1cfaf633045160afed","impliedFormat":1},{"version":"90407bbaa24977b8a6a90861148ac98d8652afe69992a90d823f29e9807fe2d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"689be50b735f145624c6f391042155ae2ff6b90a93bac11ca5712bc866f6010c","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","impliedFormat":1},{"version":"a4a39b5714adfcadd3bbea6698ca2e942606d833bde62ad5fb6ec55f5e438ff8","impliedFormat":1},{"version":"bbc1d029093135d7d9bfa4b38cbf8761db505026cc458b5e9c8b74f4000e5e75","impliedFormat":1},{"version":"1f68ab0e055994eb337b67aa87d2a15e0200951e9664959b3866ee6f6b11a0fe","impliedFormat":1},{"version":"b71c603a539078a5e3a039b20f2b0a0d1708967530cf97dec8850a9ca45baa2b","impliedFormat":1},{"version":"d3f2d715f57df3f04bf7b16dde01dec10366f64fce44503c92b8f78f614c1769","impliedFormat":1},{"version":"cb90077223cc1365fa21ef0911a1f9b8f2f878943523d97350dc557973ca3823","impliedFormat":1},{"version":"18f1541b81b80d806120a3489af683edfb811deb91aeca19735d9bb2613e6311","impliedFormat":1},{"version":"232f118ae64ab84dcd26ddb60eaed5a6e44302d36249abf05e9e3fc2cbb701a2","impliedFormat":1},{"version":"26b7d0cd4b41ab557ef9e3bfeec42dcf24252843633e3d29f38d2c0b13aaa528","impliedFormat":1},{"version":"7fadb2778688ebf3fd5b8d04f63d5bf27a43a3e420bc80732d3c6239067d1a4b","impliedFormat":1},{"version":"510616459e6edd01acbce333fb256e06bdffdad43ca233a9090164bf8bb83912","impliedFormat":1},{"version":"ddef25f825320de051dcb0e62ffce621b41c67712b5b4105740c32fd83f4c449","impliedFormat":1},{"version":"1b3dffaa4ca8e38ac434856843505af767a614d187fb3a5ef4fcebb023c355aa","impliedFormat":1},{"version":"1ba59c8bbeed2cb75b239bb12041582fa3e8ef32f8d0bd0ec802e38442d3f317","impliedFormat":1}],"root":[462,463,[472,478],[484,500]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"target":1},"referencedMap":[[502,1],[503,2],[508,3],[501,4],[513,5],[512,6],[511,7],[509,8],[507,9],[518,10],[514,8],[519,11],[510,8],[515,8],[520,12],[505,8],[506,8],[521,8],[504,13],[522,14],[517,15],[516,16],[523,4],[524,17],[104,8],[493,18],[494,19],[495,20],[496,21],[497,22],[498,23],[491,24],[492,25],[499,26],[500,27],[490,28],[477,29],[478,30],[484,31],[485,32],[486,33],[487,34],[474,35],[475,36],[473,37],[488,29],[489,29],[463,8],[472,38],[476,39],[462,40],[411,8],[467,8],[468,41],[469,42],[470,43],[143,44],[144,44],[145,45],[99,46],[146,47],[147,48],[148,49],[94,8],[97,50],[95,8],[96,8],[149,51],[150,52],[151,53],[152,54],[153,55],[154,56],[155,56],[156,57],[157,58],[158,59],[159,60],[100,8],[98,8],[160,61],[161,62],[162,63],[194,64],[163,65],[164,66],[165,67],[166,68],[167,69],[168,70],[169,71],[170,72],[171,73],[172,74],[173,74],[174,75],[175,8],[176,76],[178,77],[177,78],[179,79],[180,80],[181,81],[182,82],[183,83],[184,84],[185,85],[186,86],[187,87],[188,88],[189,89],[190,90],[191,91],[101,8],[102,8],[103,8],[142,92],[192,93],[193,94],[198,95],[199,96],[197,97],[195,98],[196,99],[83,8],[85,100],[304,101],[84,8],[482,102],[483,103],[481,104],[480,105],[479,8],[471,101],[92,106],[414,107],[419,28],[421,108],[219,109],[365,110],[390,111],[294,8],[212,8],[217,8],[356,112],[286,113],[218,8],[392,114],[393,115],[337,116],[353,117],[259,118],[360,119],[361,120],[359,121],[358,8],[357,122],[391,123],[220,124],[293,8],[295,125],[215,8],[230,126],[221,127],[234,126],[263,126],[205,126],[364,128],[374,8],[211,8],[317,129],[318,130],[305,131],[320,8],[306,132],[443,133],[442,134],[321,131],[440,8],[395,8],[351,8],[352,135],[307,101],[237,136],[235,137],[441,8],[236,138],[435,139],[438,140],[246,141],[245,142],[244,143],[446,101],[243,144],[281,8],[449,8],[465,145],[464,8],[452,8],[451,101],[453,146],[201,8],[362,147],[363,148],[384,8],[210,149],[200,8],[203,150],[331,101],[330,151],[322,8],[323,8],[325,8],[328,152],[324,8],[326,153],[329,154],[327,153],[216,8],[208,8],[209,126],[413,155],[422,156],[426,157],[368,158],[367,8],[278,8],[454,159],[377,160],[315,161],[316,162],[301,163],[311,8],[299,8],[300,164],[335,165],[312,166],[336,167],[333,168],[332,8],[334,8],[290,169],[369,170],[370,171],[313,172],[314,173],[309,174],[348,175],[376,176],[379,177],[279,178],[206,179],[375,180],[202,111],[396,181],[407,182],[394,8],[406,183],[93,8],[382,184],[266,8],[296,185],[225,8],[405,186],[214,8],[269,187],[366,188],[404,8],[399,189],[207,8],[400,190],[402,191],[403,192],[385,8],[398,179],[233,193],[383,194],[408,195],[340,8],[343,8],[341,8],[345,8],[342,8],[344,8],[346,196],[339,8],[272,197],[271,8],[277,198],[273,199],[276,200],[275,200],[274,199],[229,201],[261,202],[373,203],[455,8],[430,204],[432,205],[298,8],[431,206],[371,170],[319,170],[213,8],[262,207],[226,208],[227,209],[228,210],[224,211],[347,211],[240,211],[264,212],[241,212],[223,213],[222,8],[270,214],[268,215],[267,216],[265,217],[372,218],[303,219],[338,220],[302,221],[355,222],[354,223],[350,224],[258,225],[260,226],[257,227],[231,228],[289,8],[418,8],[288,229],[349,8],[280,230],[310,231],[308,232],[282,233],[284,234],[450,8],[283,235],[285,235],[416,8],[415,8],[417,8],[448,8],[287,236],[255,101],[91,8],[238,237],[247,8],[292,238],[232,8],[424,101],[434,239],[254,101],[428,131],[253,240],[410,241],[252,239],[204,8],[436,242],[250,101],[251,101],[242,8],[291,8],[249,243],[248,244],[239,245],[297,73],[378,73],[401,8],[381,246],[380,8],[420,8],[256,101],[412,247],[86,101],[89,248],[90,249],[87,101],[88,8],[397,250],[389,251],[388,8],[387,252],[386,8],[409,253],[423,254],[425,255],[427,256],[466,257],[429,258],[433,259],[461,260],[437,260],[460,261],[439,262],[444,263],[445,264],[447,265],[456,266],[459,149],[458,8],[457,12],[81,8],[82,8],[13,8],[14,8],[16,8],[15,8],[2,8],[17,8],[18,8],[19,8],[20,8],[21,8],[22,8],[23,8],[24,8],[3,8],[25,8],[26,8],[4,8],[27,8],[31,8],[28,8],[29,8],[30,8],[32,8],[33,8],[34,8],[5,8],[35,8],[36,8],[37,8],[38,8],[6,8],[42,8],[39,8],[40,8],[41,8],[43,8],[7,8],[44,8],[49,8],[50,8],[45,8],[46,8],[47,8],[48,8],[8,8],[54,8],[51,8],[52,8],[53,8],[55,8],[9,8],[56,8],[57,8],[58,8],[60,8],[59,8],[61,8],[62,8],[10,8],[63,8],[64,8],[65,8],[11,8],[66,8],[67,8],[68,8],[69,8],[70,8],[1,8],[71,8],[72,8],[12,8],[76,8],[74,8],[79,8],[78,8],[73,8],[77,8],[75,8],[80,8],[120,267],[130,268],[119,267],[140,269],[111,270],[110,271],[139,12],[133,272],[138,273],[113,274],[127,275],[112,276],[136,277],[108,278],[107,12],[137,279],[109,280],[114,281],[115,8],[118,281],[105,8],[141,282],[131,283],[122,284],[123,285],[125,286],[121,287],[124,288],[134,12],[116,289],[117,290],[126,291],[106,292],[129,283],[128,281],[132,8],[135,293]],"affectedFilesPendingEmit":[493,494,495,496,497,498,491,492,499,500,477,478,484,485,486,487,474,475,473,488,489,463,472,476],"version":"5.9.3"}