Jacksonnavigator7 commited on
Commit
679b4c8
Β·
verified Β·
1 Parent(s): cd8262f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1546 -229
app.py CHANGED
@@ -5,256 +5,1573 @@ import hashlib
5
  import os
6
  import threading
7
  import time
 
 
 
 
 
 
8
 
9
- DB_PATH = os.getenv("DB_PATH", "attendance.db")
10
-
11
- # Initialize database
12
- def init_db():
13
- with sqlite3.connect(DB_PATH) as conn:
14
- c = conn.cursor()
15
- c.execute('''CREATE TABLE IF NOT EXISTS users (
16
- id INTEGER PRIMARY KEY AUTOINCREMENT,
17
- username TEXT UNIQUE,
18
- password TEXT,
19
- role TEXT,
20
- device_id TEXT
21
- )''')
22
- c.execute('''CREATE TABLE IF NOT EXISTS attendance (
23
- id INTEGER PRIMARY KEY AUTOINCREMENT,
24
- username TEXT,
25
- date TEXT,
26
- time TEXT,
27
- session TEXT,
28
- status TEXT DEFAULT 'Present'
29
- )''')
30
- c.execute('''CREATE TABLE IF NOT EXISTS settings (
31
- id INTEGER PRIMARY KEY,
32
- attendance_open INTEGER,
33
- open_time TEXT,
34
- close_time TEXT
35
- )''')
36
- c.execute("INSERT OR IGNORE INTO settings (id, attendance_open, open_time, close_time) VALUES (1, 0, '', '')")
37
- conn.commit()
38
-
39
- init_db()
40
-
41
- # Utilities
42
-
43
- def hash_password(password):
44
- return hashlib.sha256(password.encode()).hexdigest()
45
 
46
- def check_password(stored, provided):
47
- return stored == hash_password(provided)
48
-
49
- # Auth functions
50
- def register_user(username, password, role):
51
- try:
52
- with sqlite3.connect(DB_PATH) as conn:
53
- c = conn.cursor()
54
- hashed = hash_password(password)
55
- c.execute("INSERT INTO users (username, password, role) VALUES (?, ?, ?)", (username, hashed, role))
56
- conn.commit()
57
- return "βœ… User registered successfully."
58
- except sqlite3.IntegrityError:
59
- return "❌ Username already exists."
60
- except Exception as e:
61
- return f"❌ Registration error: {str(e)}"
62
 
63
- def login_user(username, password):
64
- try:
65
- with sqlite3.connect(DB_PATH) as conn:
66
- c = conn.cursor()
67
- c.execute("SELECT * FROM users WHERE username=?", (username,))
68
- user = c.fetchone()
69
- if user and check_password(user[2], password):
70
- return user
71
- return None
72
- except Exception:
73
- return None
74
 
75
- def set_device(username, device_id):
76
- try:
77
- with sqlite3.connect(DB_PATH) as conn:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  c = conn.cursor()
79
- c.execute("UPDATE users SET device_id=? WHERE username=?", (device_id, username))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  conn.commit()
81
- return "Device registered."
82
- except Exception as e:
83
- return f"❌ Device registration error: {str(e)}"
 
 
 
 
 
 
84
 
85
- def check_device(username, device_id):
86
- try:
87
- with sqlite3.connect(DB_PATH) as conn:
88
- c = conn.cursor()
89
- c.execute("SELECT device_id FROM users WHERE username=?", (username,))
90
- result = c.fetchone()
91
- return result and result[0] == device_id
92
- except:
93
- return False
94
 
95
- # Attendance functions
96
- def mark_attendance(username, session):
97
- try:
98
- with sqlite3.connect(DB_PATH) as conn:
99
- c = conn.cursor()
100
- c.execute("SELECT attendance_open FROM settings WHERE id=1")
101
- if c.fetchone()[0] != 1:
102
- return "❌ Attendance window is closed."
103
- date = datetime.date.today().isoformat()
104
- time_str = datetime.datetime.now().strftime("%H:%M:%S")
105
- c.execute("INSERT INTO attendance (username, date, time, session) VALUES (?, ?, ?, ?)", (username, date, time_str, session))
106
- conn.commit()
107
- return f"βœ… Attendance marked at {time_str} for session: {session}"
108
- except Exception as e:
109
- return f"❌ Attendance error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- def view_attendance():
112
- try:
113
- with sqlite3.connect(DB_PATH) as conn:
114
- c = conn.cursor()
115
- c.execute("SELECT * FROM attendance ORDER BY date DESC, time DESC")
116
- records = c.fetchall()
117
- if not records:
118
- return "No attendance records yet."
119
- return "\n".join([f"{r[1]} | {r[2]} | {r[3]} | {r[4]} | {r[5]}" for r in records])
120
- except Exception as e:
121
- return f"❌ Error fetching attendance: {str(e)}"
122
 
123
- def open_attendance():
124
- with sqlite3.connect(DB_PATH) as conn:
125
- c = conn.cursor()
126
- now = datetime.datetime.now().strftime("%H:%M:%S")
127
- c.execute("UPDATE settings SET attendance_open=1, open_time=? WHERE id=1", (now,))
128
- conn.commit()
129
- threading.Thread(target=auto_close_window, daemon=True).start()
130
- return "βœ… Attendance window opened for 5 minutes."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
- def auto_close_window():
133
- time.sleep(300) # 5 minutes
134
- close_attendance()
135
 
136
- def close_attendance():
137
- with sqlite3.connect(DB_PATH) as conn:
138
- c = conn.cursor()
139
- now = datetime.datetime.now().strftime("%H:%M:%S")
140
- c.execute("UPDATE settings SET attendance_open=0, close_time=? WHERE id=1", (now,))
141
- conn.commit()
142
- return "βœ… Attendance window closed."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- def view_today_status(username):
145
- with sqlite3.connect(DB_PATH) as conn:
146
- c = conn.cursor()
147
- today = datetime.date.today().isoformat()
148
- c.execute("SELECT * FROM attendance WHERE username=? AND date=?", (username, today))
149
- return "βœ… Present today." if c.fetchone() else "❌ Not marked today."
150
 
151
- def add_student(username):
152
- try:
153
- with sqlite3.connect(DB_PATH) as conn:
154
- c = conn.cursor()
155
- c.execute("INSERT INTO users (username, password, role) VALUES (?, '', 'student')", (username,))
156
- conn.commit()
157
- return "βœ… Student added."
158
- except sqlite3.IntegrityError:
159
- return "❌ Student already exists."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
- def toggle_status(username, date):
162
- with sqlite3.connect(DB_PATH) as conn:
163
- c = conn.cursor()
164
- c.execute("SELECT status FROM attendance WHERE username=? AND date=?", (username, date))
165
- current = c.fetchone()
166
- if not current:
167
- return "❌ Record not found."
168
- new_status = 'Absent' if current[0] == 'Present' else 'Present'
169
- c.execute("UPDATE attendance SET status=? WHERE username=? AND date=?", (new_status, username, date))
170
- conn.commit()
171
- return f"βœ… Status updated to {new_status}"
172
 
173
- def monthly_report():
174
- with sqlite3.connect(DB_PATH) as conn:
175
- c = conn.cursor()
176
- c.execute("SELECT username, COUNT(*) FROM attendance WHERE strftime('%Y-%m', date) = strftime('%Y-%m', 'now') GROUP BY username")
177
- records = c.fetchall()
178
- if not records:
179
- return "No records this month."
180
- return "\n".join([f"{r[0]}: {r[1]} days present" for r in records])
181
 
182
- # UI
183
- with gr.Blocks() as app:
184
- gr.Markdown("# πŸ“ Attendance System (Enhanced)")
 
185
 
186
- with gr.Tab("πŸ†• Register"):
187
- with gr.Row():
188
- reg_username = gr.Text(label="Username")
189
- reg_password = gr.Text(label="Password", type="password")
190
- reg_role = gr.Radio(["teacher", "student"], label="Role")
191
- reg_button = gr.Button("Register")
192
- reg_output = gr.Textbox(label="Result")
193
- reg_button.click(fn=register_user, inputs=[reg_username, reg_password, reg_role], outputs=reg_output)
 
194
 
195
- with gr.Tab("🟒 Login & Attendance"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  with gr.Row():
197
- log_username = gr.Text(label="Username")
198
- log_password = gr.Text(label="Password", type="password")
199
- log_device = gr.Text(label="Device ID")
200
- session_input = gr.Text(label="Session ID")
201
- login_button = gr.Button("Login & Mark Attendance")
202
- login_output = gr.Textbox(label="Result")
203
-
204
- def login_and_mark(username, password, device_id, session):
205
- user = login_user(username, password)
206
- if not user:
207
- return "❌ Login failed."
208
- if not user[4]:
209
- set_device(username, device_id)
210
- return mark_attendance(username, session)
211
- elif check_device(username, device_id):
212
- return mark_attendance(username, session)
213
- else:
214
- return "❌ Unauthorized device."
215
-
216
- login_button.click(fn=login_and_mark, inputs=[log_username, log_password, log_device, session_input], outputs=login_output)
217
-
218
- with gr.Tab("πŸ“Š My Status"):
219
- stat_user = gr.Text(label="Username")
220
- stat_button = gr.Button("Check Today's Attendance")
221
- stat_output = gr.Textbox(label="Status")
222
- stat_button.click(fn=view_today_status, inputs=stat_user, outputs=stat_output)
223
-
224
- with gr.Tab("πŸ‘©β€πŸ« Teacher Panel"):
225
  with gr.Row():
226
- t_user = gr.Text(label="Username")
227
- t_pass = gr.Text(label="Password", type="password")
228
-
229
  with gr.Row():
230
- open_btn = gr.Button("πŸ”“ Open Attendance")
231
- close_btn = gr.Button("πŸ”’ Close Attendance")
232
- dash_btn = gr.Button("πŸ“‹ View Dashboard")
 
 
 
 
 
233
  with gr.Row():
234
- add_stud = gr.Text(label="New Student Username")
235
- add_btn = gr.Button("βž• Add Student")
236
- rep_btn = gr.Button("πŸ“… Monthly Report")
237
- out = gr.Textbox(label="Output", lines=15)
238
-
239
- def teacher_action(action, username, password, extra=None):
240
- user = login_user(username, password)
241
- if not user or user[3] != "teacher":
242
- return "❌ Unauthorized."
243
- if action == "open":
244
- return open_attendance()
245
- elif action == "close":
246
- return close_attendance()
247
- elif action == "dash":
248
- return view_attendance()
249
- elif action == "add":
250
- return add_student(extra)
251
- elif action == "report":
252
- return monthly_report()
253
-
254
- open_btn.click(fn=lambda u, p: teacher_action("open", u, p), inputs=[t_user, t_pass], outputs=out)
255
- close_btn.click(fn=lambda u, p: teacher_action("close", u, p), inputs=[t_user, t_pass], outputs=out)
256
- dash_btn.click(fn=lambda u, p: teacher_action("dash", u, p), inputs=[t_user, t_pass], outputs=out)
257
- add_btn.click(fn=lambda u, p, s: teacher_action("add", u, p, s), inputs=[t_user, t_pass, add_stud], outputs=out)
258
- rep_btn.click(fn=lambda u, p: teacher_action("report", u, p), inputs=[t_user, t_pass], outputs=out)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
- app.launch()
 
 
 
 
 
 
 
5
  import os
6
  import threading
7
  import time
8
+ import secrets
9
+ import re
10
+ import json
11
+ import logging
12
+ from typing import Dict, List, Optional, Tuple, Union
13
+ from pathlib import Path
14
 
15
+ # Configure logging
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
+ handlers=[
20
+ logging.FileHandler("attendance_system.log"),
21
+ logging.StreamHandler()
22
+ ]
23
+ )
24
+ logger = logging.getLogger("attendance_system")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ # Constants and Configuration
27
+ DB_PATH = os.getenv("DB_PATH", "attendance.db")
28
+ SESSION_EXPIRY = 3600 # Session token expires after 1 hour
29
+ ATTENDANCE_WINDOW = 300 # Default attendance window: 5 minutes
30
+ DEFAULT_ROLES = ["admin", "teacher", "student"]
31
+ SALT_LENGTH = 16 # For password security
 
 
 
 
 
 
 
 
 
 
32
 
33
+ # ========== DATABASE LAYER ==========
 
 
 
 
 
 
 
 
 
 
34
 
35
+ class Database:
36
+ """Database management layer with connection pooling"""
37
+
38
+ def __init__(self, db_path: str):
39
+ self.db_path = db_path
40
+ self._ensure_directory_exists()
41
+ self.init_db()
42
+
43
+ def _ensure_directory_exists(self):
44
+ """Ensure the directory for the database exists"""
45
+ directory = os.path.dirname(self.db_path)
46
+ if directory and not os.path.exists(directory):
47
+ os.makedirs(directory)
48
+
49
+ def get_connection(self):
50
+ """Get a database connection"""
51
+ return sqlite3.connect(self.db_path)
52
+
53
+ def init_db(self):
54
+ """Initialize the database schema"""
55
+ with self.get_connection() as conn:
56
  c = conn.cursor()
57
+
58
+ # Users table with improved fields
59
+ c.execute('''CREATE TABLE IF NOT EXISTS users (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ username TEXT UNIQUE NOT NULL,
62
+ password_hash TEXT NOT NULL,
63
+ salt TEXT NOT NULL,
64
+ role TEXT NOT NULL,
65
+ email TEXT UNIQUE,
66
+ full_name TEXT,
67
+ created_at TEXT NOT NULL,
68
+ last_login TEXT,
69
+ active INTEGER DEFAULT 1
70
+ )''')
71
+
72
+ # Sessions table for better auth management
73
+ c.execute('''CREATE TABLE IF NOT EXISTS sessions (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ user_id INTEGER NOT NULL,
76
+ token TEXT UNIQUE NOT NULL,
77
+ device_id TEXT,
78
+ created_at TEXT NOT NULL,
79
+ expires_at TEXT NOT NULL,
80
+ FOREIGN KEY (user_id) REFERENCES users(id)
81
+ )''')
82
+
83
+ # Enhanced attendance table
84
+ c.execute('''CREATE TABLE IF NOT EXISTS attendance (
85
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
86
+ user_id INTEGER NOT NULL,
87
+ date TEXT NOT NULL,
88
+ time_in TEXT NOT NULL,
89
+ time_out TEXT,
90
+ session_id TEXT NOT NULL,
91
+ status TEXT DEFAULT 'Present',
92
+ latitude TEXT,
93
+ longitude TEXT,
94
+ device_info TEXT,
95
+ notes TEXT,
96
+ FOREIGN KEY (user_id) REFERENCES users(id),
97
+ UNIQUE(user_id, date, session_id)
98
+ )''')
99
+
100
+ # Sessions/classes table
101
+ c.execute('''CREATE TABLE IF NOT EXISTS class_sessions (
102
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
103
+ name TEXT NOT NULL,
104
+ description TEXT,
105
+ created_by INTEGER NOT NULL,
106
+ start_time TEXT NOT NULL,
107
+ end_time TEXT,
108
+ attendance_window INTEGER DEFAULT 300,
109
+ code TEXT UNIQUE,
110
+ active INTEGER DEFAULT 1,
111
+ FOREIGN KEY (created_by) REFERENCES users(id)
112
+ )''')
113
+
114
+ # Settings table
115
+ c.execute('''CREATE TABLE IF NOT EXISTS settings (
116
+ id INTEGER PRIMARY KEY,
117
+ site_name TEXT DEFAULT 'Attendance System',
118
+ attendance_mode TEXT DEFAULT 'manual',
119
+ auto_close_window INTEGER DEFAULT 1,
120
+ default_window_time INTEGER DEFAULT 300,
121
+ geo_verification INTEGER DEFAULT 0,
122
+ allowed_radius INTEGER DEFAULT 100,
123
+ site_latitude TEXT,
124
+ site_longitude TEXT,
125
+ require_device_verification INTEGER DEFAULT 0,
126
+ theme TEXT DEFAULT 'light'
127
+ )''')
128
+
129
+ # Create admin user if not exists
130
+ c.execute("SELECT COUNT(*) FROM users WHERE role='admin'")
131
+ if c.fetchone()[0] == 0:
132
+ salt = secrets.token_hex(SALT_LENGTH)
133
+ password_hash = self._hash_password("admin", salt)
134
+ now = datetime.datetime.now().isoformat()
135
+ c.execute(
136
+ "INSERT INTO users (username, password_hash, salt, role, created_at) VALUES (?, ?, ?, ?, ?)",
137
+ ("admin", password_hash, salt, "admin", now)
138
+ )
139
+
140
+ # Create default settings if not exists
141
+ c.execute("SELECT COUNT(*) FROM settings")
142
+ if c.fetchone()[0] == 0:
143
+ c.execute("INSERT INTO settings (id) VALUES (1)")
144
+
145
  conn.commit()
146
+
147
+ def _hash_password(self, password: str, salt: str) -> str:
148
+ """Hash the password with the given salt"""
149
+ return hashlib.pbkdf2_hmac(
150
+ 'sha256',
151
+ password.encode(),
152
+ salt.encode(),
153
+ 100000 # 100,000 iterations for security
154
+ ).hex()
155
 
156
+ # ========== AUTHENTICATION & USER MANAGEMENT ==========
 
 
 
 
 
 
 
 
157
 
158
+ class UserManager:
159
+ """Handles user authentication and management"""
160
+
161
+ def __init__(self, db: Database):
162
+ self.db = db
163
+
164
+ def register_user(self, username: str, password: str, role: str, email: str = None, full_name: str = None) -> Tuple[bool, str]:
165
+ """Register a new user"""
166
+ # Validate inputs
167
+ if not username or not password or not role:
168
+ return False, "All required fields must be provided."
169
+
170
+ if role not in DEFAULT_ROLES:
171
+ return False, f"Invalid role. Must be one of: {', '.join(DEFAULT_ROLES)}"
172
+
173
+ if not self._validate_password_strength(password):
174
+ return False, "Password is too weak. Must be at least 8 characters with numbers and letters."
175
+
176
+ if email and not self._validate_email(email):
177
+ return False, "Invalid email format."
178
+
179
+ # Create the user
180
+ try:
181
+ with self.db.get_connection() as conn:
182
+ c = conn.cursor()
183
+ salt = secrets.token_hex(SALT_LENGTH)
184
+ password_hash = self.db._hash_password(password, salt)
185
+ now = datetime.datetime.now().isoformat()
186
+
187
+ c.execute(
188
+ "INSERT INTO users (username, password_hash, salt, role, email, full_name, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
189
+ (username, password_hash, salt, role, email, full_name, now)
190
+ )
191
+ conn.commit()
192
+ logger.info(f"User registered: {username} with role {role}")
193
+ return True, "User registered successfully"
194
+ except sqlite3.IntegrityError:
195
+ return False, "Username or email already exists."
196
+ except Exception as e:
197
+ logger.error(f"Error registering user: {str(e)}")
198
+ return False, f"Registration error: {str(e)}"
199
+
200
+ def login(self, username: str, password: str, device_id: str = None) -> Tuple[bool, Union[str, Dict]]:
201
+ """Authenticate a user and create a session"""
202
+ try:
203
+ with self.db.get_connection() as conn:
204
+ c = conn.cursor()
205
+ # Get user by username
206
+ c.execute("SELECT id, username, password_hash, salt, role FROM users WHERE username=? AND active=1", (username,))
207
+ user = c.fetchone()
208
+
209
+ if not user:
210
+ logger.warning(f"Login attempt for non-existent user: {username}")
211
+ return False, "Invalid username or password."
212
+
213
+ # Verify password
214
+ user_id, username, password_hash, salt, role = user
215
+ if password_hash != self.db._hash_password(password, salt):
216
+ logger.warning(f"Failed login attempt for user: {username}")
217
+ return False, "Invalid username or password."
218
+
219
+ # Create session token
220
+ token = secrets.token_urlsafe(32)
221
+ now = datetime.datetime.now()
222
+ expires = now + datetime.timedelta(seconds=SESSION_EXPIRY)
223
+
224
+ c.execute(
225
+ "INSERT INTO sessions (user_id, token, device_id, created_at, expires_at) VALUES (?, ?, ?, ?, ?)",
226
+ (user_id, token, device_id, now.isoformat(), expires.isoformat())
227
+ )
228
+
229
+ # Update last login
230
+ c.execute("UPDATE users SET last_login=? WHERE id=?", (now.isoformat(), user_id))
231
+ conn.commit()
232
+
233
+ logger.info(f"User logged in: {username}")
234
+ return True, {
235
+ "token": token,
236
+ "user_id": user_id,
237
+ "username": username,
238
+ "role": role,
239
+ "expires_at": expires.isoformat()
240
+ }
241
+ except Exception as e:
242
+ logger.error(f"Login error: {str(e)}")
243
+ return False, f"Login error: {str(e)}"
244
+
245
+ def verify_session(self, token: str, device_id: str = None) -> Tuple[bool, Union[str, Dict]]:
246
+ """Verify a session token"""
247
+ try:
248
+ with self.db.get_connection() as conn:
249
+ c = conn.cursor()
250
+ now = datetime.datetime.now().isoformat()
251
+
252
+ query = """
253
+ SELECT s.id, s.user_id, u.username, u.role, s.device_id
254
+ FROM sessions s
255
+ JOIN users u ON s.user_id = u.id
256
+ WHERE s.token = ? AND s.expires_at > ? AND u.active = 1
257
+ """
258
+ c.execute(query, (token, now))
259
+ session = c.fetchone()
260
+
261
+ if not session:
262
+ return False, "Session expired or invalid."
263
+
264
+ session_id, user_id, username, role, stored_device_id = session
265
+
266
+ # Device verification if required
267
+ if device_id and stored_device_id and device_id != stored_device_id:
268
+ logger.warning(f"Device mismatch for user {username}: {device_id} vs {stored_device_id}")
269
+ return False, "Device verification failed."
270
+
271
+ return True, {
272
+ "user_id": user_id,
273
+ "username": username,
274
+ "role": role
275
+ }
276
+ except Exception as e:
277
+ logger.error(f"Session verification error: {str(e)}")
278
+ return False, f"Session error: {str(e)}"
279
+
280
+ def logout(self, token: str) -> Tuple[bool, str]:
281
+ """Invalidate a session token"""
282
+ try:
283
+ with self.db.get_connection() as conn:
284
+ c = conn.cursor()
285
+ c.execute("DELETE FROM sessions WHERE token = ?", (token,))
286
+ conn.commit()
287
+ return True, "Logged out successfully."
288
+ except Exception as e:
289
+ logger.error(f"Logout error: {str(e)}")
290
+ return False, f"Logout error: {str(e)}"
291
+
292
+ def _validate_password_strength(self, password: str) -> bool:
293
+ """Validate password strength"""
294
+ if len(password) < 8:
295
+ return False
296
+ if not re.search(r'\d', password): # At least one digit
297
+ return False
298
+ if not re.search(r'[a-zA-Z]', password): # At least one letter
299
+ return False
300
+ return True
301
+
302
+ def _validate_email(self, email: str) -> bool:
303
+ """Validate email format"""
304
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
305
+ return bool(re.match(pattern, email))
306
+
307
+ def get_users(self, role: str = None) -> List[Dict]:
308
+ """Get list of users, optionally filtered by role"""
309
+ try:
310
+ with self.db.get_connection() as conn:
311
+ c = conn.cursor()
312
+ query = "SELECT id, username, role, email, full_name, active FROM users"
313
+ params = ()
314
+
315
+ if role:
316
+ query += " WHERE role = ?"
317
+ params = (role,)
318
+
319
+ c.execute(query, params)
320
+ users = c.fetchall()
321
+ return [
322
+ {
323
+ "id": user[0],
324
+ "username": user[1],
325
+ "role": user[2],
326
+ "email": user[3],
327
+ "full_name": user[4],
328
+ "active": bool(user[5])
329
+ }
330
+ for user in users
331
+ ]
332
+ except Exception as e:
333
+ logger.error(f"Error getting users: {str(e)}")
334
+ return []
335
 
336
+ # ========== ATTENDANCE MANAGEMENT ==========
 
 
 
 
 
 
 
 
 
 
337
 
338
+ class AttendanceManager:
339
+ """Handles attendance-related operations"""
340
+
341
+ def __init__(self, db: Database):
342
+ self.db = db
343
+
344
+ def create_session(self, name: str, created_by: int, description: str = None,
345
+ window_time: int = None) -> Tuple[bool, Union[str, Dict]]:
346
+ """Create a new attendance session/class"""
347
+ try:
348
+ with self.db.get_connection() as conn:
349
+ c = conn.cursor()
350
+ now = datetime.datetime.now()
351
+
352
+ # Get default window time from settings if not provided
353
+ if window_time is None:
354
+ c.execute("SELECT default_window_time FROM settings WHERE id=1")
355
+ window_time = c.fetchone()[0]
356
+
357
+ # Generate unique code for the session
358
+ code = secrets.token_hex(3).upper()
359
+
360
+ c.execute(
361
+ """INSERT INTO class_sessions
362
+ (name, description, created_by, start_time, attendance_window, code, active)
363
+ VALUES (?, ?, ?, ?, ?, ?, 1)""",
364
+ (name, description, created_by, now.isoformat(), window_time, code)
365
+ )
366
+ session_id = c.lastrowid
367
+ conn.commit()
368
+
369
+ # Auto-close thread if needed
370
+ c.execute("SELECT auto_close_window FROM settings WHERE id=1")
371
+ auto_close = c.fetchone()[0]
372
+ if auto_close:
373
+ threading.Thread(
374
+ target=self._auto_close_session,
375
+ args=(session_id, window_time),
376
+ daemon=True
377
+ ).start()
378
+
379
+ logger.info(f"Created session: {name} with code {code}")
380
+ return True, {
381
+ "session_id": session_id,
382
+ "code": code,
383
+ "name": name,
384
+ "start_time": now.isoformat(),
385
+ "window_time": window_time
386
+ }
387
+ except Exception as e:
388
+ logger.error(f"Error creating session: {str(e)}")
389
+ return False, f"Error creating session: {str(e)}"
390
+
391
+ def _auto_close_session(self, session_id: int, window_time: int):
392
+ """Automatically close a session after window_time expires"""
393
+ time.sleep(window_time)
394
+ try:
395
+ with self.db.get_connection() as conn:
396
+ c = conn.cursor()
397
+ now = datetime.datetime.now().isoformat()
398
+ c.execute(
399
+ "UPDATE class_sessions SET active=0, end_time=? WHERE id=?",
400
+ (now, session_id)
401
+ )
402
+ conn.commit()
403
+ logger.info(f"Auto-closed session ID: {session_id}")
404
+ except Exception as e:
405
+ logger.error(f"Error auto-closing session {session_id}: {str(e)}")
406
+
407
+ def close_session(self, session_id: int) -> Tuple[bool, str]:
408
+ """Manually close an attendance session"""
409
+ try:
410
+ with self.db.get_connection() as conn:
411
+ c = conn.cursor()
412
+ now = datetime.datetime.now().isoformat()
413
+ c.execute(
414
+ "UPDATE class_sessions SET active=0, end_time=? WHERE id=?",
415
+ (now, session_id)
416
+ )
417
+ conn.commit()
418
+ logger.info(f"Manually closed session ID: {session_id}")
419
+ return True, "Session closed successfully."
420
+ except Exception as e:
421
+ logger.error(f"Error closing session: {str(e)}")
422
+ return False, f"Error closing session: {str(e)}"
423
+
424
+ def mark_attendance(self, user_id: int, session_code: str,
425
+ device_info: str = None, geo_data: Dict = None) -> Tuple[bool, str]:
426
+ """Mark attendance for a user"""
427
+ try:
428
+ with self.db.get_connection() as conn:
429
+ c = conn.cursor()
430
+ now = datetime.datetime.now()
431
+ today = now.date().isoformat()
432
+
433
+ # Check if session exists and is active
434
+ c.execute("SELECT id, active FROM class_sessions WHERE code=?", (session_code,))
435
+ session = c.fetchone()
436
+
437
+ if not session:
438
+ return False, "Invalid session code."
439
+
440
+ session_id, is_active = session
441
+
442
+ if not is_active:
443
+ return False, "This session is closed. Attendance cannot be marked."
444
+
445
+ # Check if attendance already marked
446
+ c.execute(
447
+ "SELECT id FROM attendance WHERE user_id=? AND date=? AND session_id=?",
448
+ (user_id, today, session_id)
449
+ )
450
+ if c.fetchone():
451
+ return False, "You have already marked attendance for this session today."
452
+
453
+ # Prepare geo data
454
+ latitude = longitude = None
455
+ if geo_data:
456
+ latitude = geo_data.get('latitude')
457
+ longitude = geo_data.get('longitude')
458
+
459
+ # Verify geo location if needed
460
+ c.execute("SELECT geo_verification, site_latitude, site_longitude, allowed_radius FROM settings WHERE id=1")
461
+ geo_settings = c.fetchone()
462
+ geo_verification, site_lat, site_lon, allowed_radius = geo_settings
463
+
464
+ if geo_verification and site_lat and site_lon:
465
+ if not self._verify_location(latitude, longitude, float(site_lat), float(site_lon), allowed_radius):
466
+ return False, "You are outside the allowed area for attendance."
467
+
468
+ # Insert attendance record
469
+ c.execute(
470
+ """INSERT INTO attendance
471
+ (user_id, date, time_in, session_id, device_info, latitude, longitude)
472
+ VALUES (?, ?, ?, ?, ?, ?, ?)""",
473
+ (user_id, today, now.isoformat(), session_id,
474
+ json.dumps(device_info) if device_info else None,
475
+ str(latitude) if latitude else None,
476
+ str(longitude) if longitude else None)
477
+ )
478
+ conn.commit()
479
+
480
+ logger.info(f"Attendance marked for user {user_id} in session {session_code}")
481
+ return True, f"Attendance marked successfully at {now.strftime('%H:%M:%S')}"
482
+ except sqlite3.IntegrityError:
483
+ return False, "You have already marked attendance for this session."
484
+ except Exception as e:
485
+ logger.error(f"Error marking attendance: {str(e)}")
486
+ return False, f"Error marking attendance: {str(e)}"
487
+
488
+ def _verify_location(self, user_lat: float, user_lon: float,
489
+ site_lat: float, site_lon: float, max_distance: float) -> bool:
490
+ """Verify if user is within allowed distance from site"""
491
+ # Simple Euclidean distance for demo purposes
492
+ # In production, use proper geospatial calculations
493
+ if not user_lat or not user_lon:
494
+ return False
495
+
496
+ # Approximate conversion from lat/long to meters
497
+ # This is a rough approximation - works only for short distances
498
+ lat_diff = abs(user_lat - site_lat) * 111000 # 1 degree lat β‰ˆ 111km
499
+ lon_diff = abs(user_lon - site_lon) * 111000 * abs(math.cos(math.radians(site_lat)))
500
+ distance = math.sqrt(lat_diff**2 + lon_diff**2)
501
+
502
+ return distance <= max_distance
503
+
504
+ def get_attendance_report(self, filters: Dict = None) -> List[Dict]:
505
+ """Get attendance report with optional filters"""
506
+ try:
507
+ with self.db.get_connection() as conn:
508
+ c = conn.cursor()
509
+
510
+ # Build query based on filters
511
+ query = """
512
+ SELECT a.id, u.username, u.full_name, a.date, a.time_in, a.time_out,
513
+ cs.name as session_name, cs.code as session_code, a.status
514
+ FROM attendance a
515
+ JOIN users u ON a.user_id = u.id
516
+ JOIN class_sessions cs ON a.session_id = cs.id
517
+ """
518
+
519
+ conditions = []
520
+ params = []
521
+
522
+ if filters:
523
+ if 'user_id' in filters:
524
+ conditions.append("a.user_id = ?")
525
+ params.append(filters['user_id'])
526
+
527
+ if 'date' in filters:
528
+ conditions.append("a.date = ?")
529
+ params.append(filters['date'])
530
+
531
+ if 'session_id' in filters:
532
+ conditions.append("a.session_id = ?")
533
+ params.append(filters['session_id'])
534
+
535
+ if 'status' in filters:
536
+ conditions.append("a.status = ?")
537
+ params.append(filters['status'])
538
+
539
+ if 'date_range' in filters:
540
+ start, end = filters['date_range']
541
+ conditions.append("a.date BETWEEN ? AND ?")
542
+ params.extend([start, end])
543
+
544
+ if conditions:
545
+ query += " WHERE " + " AND ".join(conditions)
546
+
547
+ query += " ORDER BY a.date DESC, a.time_in DESC"
548
+
549
+ c.execute(query, params)
550
+ records = c.fetchall()
551
+
552
+ return [
553
+ {
554
+ "id": r[0],
555
+ "username": r[1],
556
+ "full_name": r[2],
557
+ "date": r[3],
558
+ "time_in": r[4],
559
+ "time_out": r[5],
560
+ "session_name": r[6],
561
+ "session_code": r[7],
562
+ "status": r[8]
563
+ }
564
+ for r in records
565
+ ]
566
+ except Exception as e:
567
+ logger.error(f"Error generating attendance report: {str(e)}")
568
+ return []
569
+
570
+ def update_attendance_status(self, attendance_id: int, status: str) -> Tuple[bool, str]:
571
+ """Update the status of an attendance record"""
572
+ valid_statuses = ["Present", "Absent", "Late", "Excused"]
573
+ if status not in valid_statuses:
574
+ return False, f"Invalid status. Must be one of: {', '.join(valid_statuses)}"
575
+
576
+ try:
577
+ with self.db.get_connection() as conn:
578
+ c = conn.cursor()
579
+ c.execute("UPDATE attendance SET status=? WHERE id=?", (status, attendance_id))
580
+ conn.commit()
581
+ logger.info(f"Updated attendance {attendance_id} status to {status}")
582
+ return True, f"Status updated to {status}"
583
+ except Exception as e:
584
+ logger.error(f"Error updating attendance status: {str(e)}")
585
+ return False, f"Error updating status: {str(e)}"
586
+
587
+ def get_active_sessions(self) -> List[Dict]:
588
+ """Get all active attendance sessions"""
589
+ try:
590
+ with self.db.get_connection() as conn:
591
+ c = conn.cursor()
592
+ c.execute("""
593
+ SELECT cs.id, cs.name, cs.description, cs.code, cs.start_time,
594
+ u.username as creator
595
+ FROM class_sessions cs
596
+ JOIN users u ON cs.created_by = u.id
597
+ WHERE cs.active = 1
598
+ ORDER BY cs.start_time DESC
599
+ """)
600
+ sessions = c.fetchall()
601
+ return [
602
+ {
603
+ "id": s[0],
604
+ "name": s[1],
605
+ "description": s[2],
606
+ "code": s[3],
607
+ "start_time": s[4],
608
+ "creator": s[5]
609
+ }
610
+ for s in sessions
611
+ ]
612
+ except Exception as e:
613
+ logger.error(f"Error getting active sessions: {str(e)}")
614
+ return []
615
+
616
+ def get_sessions_history(self, limit: int = 50) -> List[Dict]:
617
+ """Get history of attendance sessions"""
618
+ try:
619
+ with self.db.get_connection() as conn:
620
+ c = conn.cursor()
621
+ c.execute("""
622
+ SELECT cs.id, cs.name, cs.code, cs.start_time, cs.end_time,
623
+ (SELECT COUNT(*) FROM attendance WHERE session_id = cs.id) as count
624
+ FROM class_sessions cs
625
+ ORDER BY cs.start_time DESC
626
+ LIMIT ?
627
+ """, (limit,))
628
+ sessions = c.fetchall()
629
+ return [
630
+ {
631
+ "id": s[0],
632
+ "name": s[1],
633
+ "code": s[2],
634
+ "start_time": s[3],
635
+ "end_time": s[4],
636
+ "attendance_count": s[5]
637
+ }
638
+ for s in sessions
639
+ ]
640
+ except Exception as e:
641
+ logger.error(f"Error getting sessions history: {str(e)}")
642
+ return []
643
 
644
+ # ========== SETTINGS MANAGEMENT ==========
 
 
645
 
646
+ class SettingsManager:
647
+ """Manages system settings"""
648
+
649
+ def __init__(self, db: Database):
650
+ self.db = db
651
+
652
+ def get_settings(self) -> Dict:
653
+ """Get all system settings"""
654
+ try:
655
+ with self.db.get_connection() as conn:
656
+ c = conn.cursor()
657
+ c.execute("SELECT * FROM settings WHERE id=1")
658
+ columns = [description[0] for description in c.description]
659
+ values = c.fetchone()
660
+
661
+ if not values:
662
+ return {}
663
+
664
+ return dict(zip(columns, values))
665
+ except Exception as e:
666
+ logger.error(f"Error fetching settings: {str(e)}")
667
+ return {}
668
+
669
+ def update_settings(self, settings: Dict) -> Tuple[bool, str]:
670
+ """Update system settings"""
671
+ try:
672
+ with self.db.get_connection() as conn:
673
+ c = conn.cursor()
674
+ allowed_keys = {
675
+ 'site_name', 'attendance_mode', 'auto_close_window',
676
+ 'default_window_time', 'geo_verification', 'allowed_radius',
677
+ 'site_latitude', 'site_longitude', 'require_device_verification',
678
+ 'theme'
679
+ }
680
+
681
+ # Validate keys
682
+ invalid_keys = set(settings.keys()) - allowed_keys
683
+ if invalid_keys:
684
+ return False, f"Invalid settings: {', '.join(invalid_keys)}"
685
+
686
+ # Build update query
687
+ if not settings:
688
+ return False, "No settings provided to update."
689
+
690
+ query_parts = []
691
+ params = []
692
+
693
+ for key, value in settings.items():
694
+ query_parts.append(f"{key} = ?")
695
+ params.append(value)
696
+
697
+ query = "UPDATE settings SET " + ", ".join(query_parts) + " WHERE id=1"
698
+ c.execute(query, params)
699
+ conn.commit()
700
+
701
+ logger.info(f"Updated settings: {', '.join(settings.keys())}")
702
+ return True, "Settings updated successfully."
703
+ except Exception as e:
704
+ logger.error(f"Error updating settings: {str(e)}")
705
+ return False, f"Error updating settings: {str(e)}"
706
 
707
+ # ========== REPORTING ==========
 
 
 
 
 
708
 
709
+ class ReportGenerator:
710
+ """Generates various reports from attendance data"""
711
+
712
+ def __init__(self, db: Database):
713
+ self.db = db
714
+
715
+ def summary_report(self, start_date: str, end_date: str) -> Dict:
716
+ """Generate summary attendance report for a date range"""
717
+ try:
718
+ with self.db.get_connection() as conn:
719
+ c = conn.cursor()
720
+
721
+ # Total attendance counts
722
+ c.execute("""
723
+ SELECT COUNT(*) as total_records,
724
+ COUNT(DISTINCT user_id) as unique_students,
725
+ COUNT(DISTINCT session_id) as unique_sessions
726
+ FROM attendance
727
+ WHERE date BETWEEN ? AND ?
728
+ """, (start_date, end_date))
729
+
730
+ totals = c.fetchone()
731
+
732
+ # Status breakdown
733
+ c.execute("""
734
+ SELECT status, COUNT(*) as count
735
+ FROM attendance
736
+ WHERE date BETWEEN ? AND ?
737
+ GROUP BY status
738
+ """, (start_date, end_date))
739
+
740
+ status_counts = {status: count for status, count in c.fetchall()}
741
+
742
+ # Session attendance rates
743
+ c.execute("""
744
+ SELECT cs.name, cs.code, COUNT(*) as attendance_count
745
+ FROM attendance a
746
+ JOIN class_sessions cs ON a.session_id = cs.id
747
+ WHERE a.date BETWEEN ? AND ?
748
+ GROUP BY a.session_id
749
+ ORDER BY attendance_count DESC
750
+ """, (start_date, end_date))
751
+
752
+ session_stats = [
753
+ {"name": name, "code": code, "count": count}
754
+ for name, code, count in c.fetchall()
755
+ ]
756
+
757
+ # Student attendance frequency
758
+ c.execute("""
759
+ SELECT u.username, u.full_name, COUNT(*) as attendance_count
760
+ FROM attendance a
761
+ JOIN users u ON a.user_id = u.id
762
+ WHERE a.date BETWEEN ? AND ?
763
+ GROUP BY a.user_id
764
+ ORDER BY attendance_count DESC
765
+ """, (start_date, end_date))
766
+
767
+ student_stats = [
768
+ {"username": username, "full_name": full_name, "count": count}
769
+ for username, full_name, count in c.fetchall()
770
+ ]
771
+
772
+ return {
773
+ "date_range": {"start": start_date, "end": end_date},
774
+ "totals": {
775
+ "records": totals[0],
776
+ "students": totals[1],
777
+ "sessions": totals[2]
778
+ },
779
+ "status_breakdown": status_counts,
780
+ "top_sessions": session_stats[:10],
781
+ "top_students": student_stats[:10]
782
+ }
783
+ except Exception as e:
784
+ logger.error(f"Error generating summary report: {str(e)}")
785
+ return {
786
+ "error": str(e),
787
+ "date_range": {"start": start_date, "end": end_date},
788
+ "totals": {"records": 0, "students": 0, "sessions": 0},
789
+ "status_breakdown": {},
790
+ "top_sessions": [],
791
+ "top_students": []
792
+ }
793
+
794
+ def student_detail_report(self, user_id: int, start_date: str = None, end_date: str = None) -> Dict:
795
+ """Generate detailed report for a specific student"""
796
+ try:
797
+ with self.db.get_connection() as conn:
798
+ c = conn.cursor()
799
+
800
+ # Get student info
801
+ c.execute("SELECT username, full_name, email FROM users WHERE id=?", (user_id,))
802
+ student = c.fetchone()
803
+
804
+ if not student:
805
+ return {"error": "Student not found"}
806
+
807
+ username, full_name, email = student
808
+
809
+ # Build query for attendance records
810
+ query = """
811
+ SELECT a.date, a.time_in, cs.name as session_name, cs.code, a.status
812
+ FROM attendance a
813
+ JOIN class_sessions cs ON a.session_id = cs.id
814
+ WHERE a.user_id = ?
815
+ """
816
+ params = [user_id]
817
+
818
+ if start_date:
819
+ query += " AND a.date >= ?"
820
+ params.append(start_date)
821
+
822
+ if end_date:
823
+ query += " AND a.date <= ?"
824
+ params.append(end_date)
825
+
826
+ query += " ORDER BY a.date DESC, a.time_in DESC"
827
+
828
+ c.execute(query, params)
829
+ attendance_records = [
830
+ {
831
+ "date": date,
832
+ "time": time_in,
833
+ "session": session_name,
834
+ "code": code,
835
+ "status": status
836
+ }
837
+ for date, time_in, session_name, code, status in c.fetchall()
838
+ ]
839
+
840
+ # Calculate statistics
841
+ attendance_count = len(attendance_records)
842
+ status_counts = {}
843
+ for record in attendance_records:
844
+ status = record["status"]
845
+ status_counts[status] = status_counts.get(status, 0) + 1
846
+
847
+ return {
848
+ "student": {
849
+ "id": user_id,
850
+ "username": username,
851
+ "full_name": full_name,
852
+ "email": email
853
+ },
854
+ "attendance": {
855
+ "total": attendance_count,
856
+ "status_breakdown": status_counts,
857
+ "records": attendance_records
858
+ }
859
+ }
860
+ except Exception as e:
861
+ logger.error(f"Error generating student report: {str(e)}")
862
+ return {"error": str(e)}
863
+
864
+ def export_csv(self, data: List[Dict], filename: str) -> str:
865
+ """Export report data to CSV file"""
866
+ try:
867
+ if not data:
868
+ return "No data to export"
869
+
870
+ # Create directory if it doesn't exist
871
+ os.makedirs("exports", exist_ok=True)
872
+ filepath = os.path.join("exports", filename)
873
+
874
+ # Write to CSV
875
+ with open(filepath, 'w', newline='') as csvfile:
876
+ if isinstance(data[0], dict):
877
+ fieldnames = data[0].keys()
878
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
879
+ writer.writeheader()
880
+ writer.writerows(data)
881
+ else:
882
+ writer = csv.writer(csvfile)
883
+ writer.writerows(data)
884
+
885
+ return filepath
886
+ except Exception as e:
887
+ logger.error(f"Error exporting to CSV: {str(e)}")
888
+ return f"Error: {str(e)}"
889
 
890
+ # ========== GRADIO UI ==========
 
 
 
 
 
 
 
 
 
 
891
 
892
+ # Initialize system components
893
+ db = Database(DB_PATH)
894
+ user_manager = UserManager(db)
895
+ attendance_manager = AttendanceManager(db)
896
+ settings_manager = SettingsManager(db)
897
+ report_generator = ReportGenerator(db)
 
 
898
 
899
+ # UI Helper functions
900
+ def format_message(success: bool, message: str) -> str:
901
+ """Format a message with appropriate emoji"""
902
+ return f"βœ… {message}" if success else f"❌ {message}"
903
 
904
+ def format_time(time_str: str) -> str:
905
+ """Format time string for display"""
906
+ if not time_str:
907
+ return ""
908
+ try:
909
+ dt = datetime.datetime.fromisoformat(time_str)
910
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
911
+ except:
912
+ return time_str
913
 
914
+ # Main Gradio UI
915
+ with gr.Blocks(title="Advanced Attendance System", theme="default") as app:
916
+ # Store session state
917
+ session_state = gr.State({})
918
+
919
+ # Header and theme
920
+ with gr.Row():
921
+ gr.Markdown("# πŸ“ Advanced Attendance System")
922
+ theme_toggle = gr.Radio(
923
+ ["Light", "Dark"],
924
+ label="Theme",
925
+ value="Light",
926
+ interactive=True
927
+ )
928
+
929
+ # Login/Register tabs
930
+ with gr.Tab("πŸ‘€ Authentication"):
931
  with gr.Row():
932
+ with gr.Column():
933
+ gr.Markdown("### πŸ”‘ Login")
934
+ login_username = gr.Text(label="Username")
935
+ login_password = gr.Text(label="Password", type="password")
936
+ login_device = gr.Text(label="Device ID (Optional)", placeholder="e.g., device-uuid")
937
+ login_button = gr.Button("Login", variant="primary")
938
+ login_output = gr.Markdown()
939
+
940
+ with gr.Column():
941
+ gr.Markdown("### πŸ“ Register")
942
+ reg_username = gr.Text(label="Username")
943
+ reg_password = gr.Text(label="Password", type="password")
944
+ reg_confirm = gr.Text(label="Confirm Password", type="password")
945
+ reg_email = gr.Text(label="Email (Optional)")
946
+ reg_fullname = gr.Text(label="Full Name (Optional)")
947
+ reg_role = gr.Radio(["student", "teacher"], label="Role", value="student")
948
+ reg_button = gr.Button("Register")
949
+ reg_output = gr.Markdown()
950
+
951
+ # Student tab
952
+ with gr.Tab("πŸ‘¨β€πŸŽ“ Student"):
953
+ gr.Markdown("### πŸ“Š Mark Attendance")
954
+
 
 
 
 
 
955
  with gr.Row():
956
+ student_code = gr.Text(label="Session Code", placeholder="Enter the code provided by your teacher")
957
+ student_mark_btn = gr.Button("Mark Attendance", variant="primary")
958
+
959
  with gr.Row():
960
+ student_status = gr.Markdown("Please login to access student features")
961
+
962
+ with gr.Accordion("My Attendance History", open=False):
963
+ student_refresh_btn = gr.Button("Refresh History")
964
+ student_history = gr.DataFrame(label="My Attendance Records")
965
+
966
+ # Teacher tab
967
+ with gr.Tab("πŸ‘©β€πŸ« Teacher") as teacher_tab:
968
  with gr.Row():
969
+ teacher_status = gr.Markdown("Please login as a teacher to access these features")
970
+
971
+ with gr.Tabs() as teacher_tabs:
972
+ with gr.Tab("Create Session"):
973
+ with gr.Row():
974
+ session_name = gr.Text(label="Session Name", placeholder="e.g., Math 101 - Week 3")
975
+ session_desc = gr.Text(label="Description (Optional)", placeholder="Brief description of this session")
976
+
977
+ with gr.Row():
978
+ session_window = gr.Slider(label="Attendance Window (seconds)",
979
+ minimum=60, maximum=1800, value=300, step=60)
980
+ create_session_btn = gr.Button("Create New Session", variant="primary")
981
+
982
+ create_output = gr.Markdown()
983
+
984
+ with gr.Accordion("Active Sessions", open=True):
985
+ active_refresh = gr.Button("Refresh Active Sessions")
986
+ active_sessions = gr.DataFrame(label="Currently Active Sessions")
987
+ close_session_id = gr.Number(label="Session ID to Close", precision=0)
988
+ close_btn = gr.Button("Close Selected Session")
989
+ close_output = gr.Markdown()
990
+
991
+ with gr.Tab("Attendance Reports"):
992
+ with gr.Row():
993
+ report_date_start = gr.Date(label="Start Date")
994
+ report_date_end = gr.Date(label="End Date")
995
+ report_type = gr.Radio(
996
+ ["Summary", "Detailed", "By Student", "By Session"],
997
+ label="Report Type",
998
+ value="Summary"
999
+ )
1000
+
1001
+ with gr.Row():
1002
+ report_filter = gr.Dropdown(label="Additional Filter (Optional)")
1003
+ report_gen_btn = gr.Button("Generate Report", variant="primary")
1004
+
1005
+ report_output = gr.Markdown()
1006
+ report_data = gr.DataFrame(label="Report Data")
1007
+ export_btn = gr.Button("Export to CSV")
1008
+ export_output = gr.Markdown()
1009
+
1010
+ # Admin tab
1011
+ with gr.Tab("βš™οΈ Admin"):
1012
+ admin_status = gr.Markdown("Please login as admin to access these features")
1013
+
1014
+ with gr.Tabs() as admin_tabs:
1015
+ with gr.Tab("User Management"):
1016
+ with gr.Row():
1017
+ user_filter = gr.Radio(
1018
+ ["All Users", "Students", "Teachers", "Admins"],
1019
+ label="Filter Users",
1020
+ value="All Users"
1021
+ )
1022
+ user_refresh = gr.Button("Refresh User List")
1023
+
1024
+ users_table = gr.DataFrame(label="Users")
1025
+
1026
+ with gr.Row():
1027
+ user_action = gr.Radio(
1028
+ ["Activate", "Deactivate", "Change Role", "Reset Password"],
1029
+ label="Action"
1030
+ )
1031
+ user_id = gr.Number(label="User ID", precision=0)
1032
+ user_param = gr.Text(label="New Value (for role/password)")
1033
+ user_action_btn = gr.Button("Apply Action")
1034
+
1035
+ user_action_output = gr.Markdown()
1036
+
1037
+ with gr.Tab("System Settings"):
1038
+ with gr.Row():
1039
+ settings_refresh = gr.Button("Load Current Settings")
1040
+
1041
+ with gr.Group():
1042
+ gr.Markdown("### General Settings")
1043
+ site_name = gr.Text(label="Site Name")
1044
+ theme_setting = gr.Radio(["light", "dark"], label="Default Theme")
1045
+
1046
+ with gr.Group():
1047
+ gr.Markdown("### Attendance Settings")
1048
+ attendance_mode = gr.Radio(
1049
+ ["manual", "auto", "scheduled"],
1050
+ label="Attendance Mode"
1051
+ )
1052
+ auto_close = gr.Checkbox(label="Auto-close attendance windows")
1053
+ window_time = gr.Slider(
1054
+ label="Default Window Time (seconds)",
1055
+ minimum=60, maximum=3600, value=300, step=60
1056
+ )
1057
+
1058
+ with gr.Group():
1059
+ gr.Markdown("### Security Settings")
1060
+ geo_verify = gr.Checkbox(label="Enable Geographic Verification")
1061
+ with gr.Row(visible=False) as geo_settings:
1062
+ site_lat = gr.Number(label="Site Latitude")
1063
+ site_lon = gr.Number(label="Site Longitude")
1064
+ geo_radius = gr.Slider(
1065
+ label="Allowed Radius (meters)",
1066
+ minimum=10, maximum=1000, value=100
1067
+ )
1068
+
1069
+ device_verify = gr.Checkbox(label="Require Device Verification")
1070
+
1071
+ settings_save = gr.Button("Save Settings", variant="primary")
1072
+ settings_output = gr.Markdown()
1073
+
1074
+ # Login function
1075
+ def do_login(username, password, device_id):
1076
+ success, result = user_manager.login(username, password, device_id)
1077
+ if success:
1078
+ return {
1079
+ "content": f"βœ… Welcome, {result['username']}! You are now logged in.",
1080
+ "user": result
1081
+ }
1082
+ else:
1083
+ return {"content": f"❌ {result}", "user": None}
1084
+
1085
+ # Register function
1086
+ def do_register(username, password, confirm, email, fullname, role):
1087
+ if not username or not password:
1088
+ return "❌ Username and password are required."
1089
+
1090
+ if password != confirm:
1091
+ return "❌ Passwords do not match."
1092
+
1093
+ success, message = user_manager.register_user(
1094
+ username=username,
1095
+ password=password,
1096
+ role=role,
1097
+ email=email,
1098
+ full_name=fullname
1099
+ )
1100
+ return format_message(success, message)
1101
+
1102
+ # Mark attendance function
1103
+ def do_mark_attendance(code, state):
1104
+ if not state.get("user"):
1105
+ return "❌ Please login first."
1106
+
1107
+ user = state["user"]
1108
+ success, message = attendance_manager.mark_attendance(
1109
+ user_id=user["user_id"],
1110
+ session_code=code,
1111
+ device_info={"device_id": state.get("device_id")}
1112
+ )
1113
+ return format_message(success, message)
1114
+
1115
+ # Get student history
1116
+ def get_student_history(state):
1117
+ if not state.get("user"):
1118
+ return []
1119
+
1120
+ user = state["user"]
1121
+ filters = {"user_id": user["user_id"]}
1122
+ records = attendance_manager.get_attendance_report(filters)
1123
+
1124
+ return pd.DataFrame([
1125
+ {
1126
+ "Date": r["date"],
1127
+ "Time": r["time_in"],
1128
+ "Session": r["session_name"],
1129
+ "Code": r["session_code"],
1130
+ "Status": r["status"]
1131
+ } for r in records
1132
+ ])
1133
+
1134
+ # Create session function
1135
+ def do_create_session(name, desc, window, state):
1136
+ if not state.get("user") or state["user"]["role"] not in ["teacher", "admin"]:
1137
+ return "❌ You must be logged in as a teacher or admin."
1138
+
1139
+ if not name:
1140
+ return "❌ Session name is required."
1141
+
1142
+ success, result = attendance_manager.create_session(
1143
+ name=name,
1144
+ description=desc,
1145
+ created_by=state["user"]["user_id"],
1146
+ window_time=int(window)
1147
+ )
1148
+
1149
+ if success:
1150
+ return f"βœ… Session created successfully!\n\nSession code: **{result['code']}**\n\nShare this code with students to allow them to mark attendance."
1151
+ else:
1152
+ return f"❌ {result}"
1153
+
1154
+ # Get active sessions
1155
+ def get_active_sessions():
1156
+ sessions = attendance_manager.get_active_sessions()
1157
+ if not sessions:
1158
+ return []
1159
+
1160
+ return pd.DataFrame([
1161
+ {
1162
+ "ID": s["id"],
1163
+ "Name": s["name"],
1164
+ "Description": s["description"] or "-",
1165
+ "Code": s["code"],
1166
+ "Started": format_time(s["start_time"]),
1167
+ "Creator": s["creator"]
1168
+ } for s in sessions
1169
+ ])
1170
+
1171
+ # Close session function
1172
+ def do_close_session(session_id, state):
1173
+ if not state.get("user") or state["user"]["role"] not in ["teacher", "admin"]:
1174
+ return "❌ You must be logged in as a teacher or admin."
1175
+
1176
+ if not session_id:
1177
+ return "❌ Session ID is required."
1178
+
1179
+ success, message = attendance_manager.close_session(int(session_id))
1180
+ return format_message(success, message)
1181
+
1182
+ # Generate report function
1183
+ def generate_report(start_date, end_date, report_type, filter_value, state):
1184
+ if not state.get("user") or state["user"]["role"] not in ["teacher", "admin"]:
1185
+ return "❌ You must be logged in as a teacher or admin.", None
1186
+
1187
+ if not start_date or not end_date:
1188
+ return "❌ Please select both start and end dates.", None
1189
+
1190
+ if start_date > end_date:
1191
+ return "❌ Start date must be before end date.", None
1192
+
1193
+ if report_type == "Summary":
1194
+ report = report_generator.summary_report(start_date.isoformat(), end_date.isoformat())
1195
+
1196
+ # Format report summary
1197
+ summary = f"### Report Summary: {start_date} to {end_date}\n\n"
1198
+ summary += f"Total Records: {report['totals']['records']}\n"
1199
+ summary += f"Unique Students: {report['totals']['students']}\n"
1200
+ summary += f"Sessions: {report['totals']['sessions']}\n\n"
1201
+
1202
+ # Prepare DataFrame
1203
+ session_data = []
1204
+ for s in report["top_sessions"]:
1205
+ session_data.append({
1206
+ "Session Name": s["name"],
1207
+ "Code": s["code"],
1208
+ "Attendance Count": s["count"]
1209
+ })
1210
+
1211
+ return summary, pd.DataFrame(session_data)
1212
+
1213
+ elif report_type == "By Student":
1214
+ if not filter_value:
1215
+ return "❌ Please select a student.", None
1216
+
1217
+ report = report_generator.student_detail_report(
1218
+ int(filter_value),
1219
+ start_date.isoformat(),
1220
+ end_date.isoformat()
1221
+ )
1222
+
1223
+ if "error" in report:
1224
+ return f"❌ {report['error']}", None
1225
+
1226
+ summary = f"### Student Report: {report['student']['full_name'] or report['student']['username']}\n\n"
1227
+ summary += f"Total Attendance: {report['attendance']['total']} records\n\n"
1228
+ summary += "Status Breakdown:\n"
1229
+ for status, count in report['attendance']['status_breakdown'].items():
1230
+ summary += f"- {status}: {count}\n"
1231
+
1232
+ # Prepare DataFrame
1233
+ attendance_data = []
1234
+ for r in report["attendance"]["records"]:
1235
+ attendance_data.append({
1236
+ "Date": r["date"],
1237
+ "Time": r["time"],
1238
+ "Session": r["session"],
1239
+ "Code": r["code"],
1240
+ "Status": r["status"]
1241
+ })
1242
+
1243
+ return summary, pd.DataFrame(attendance_data)
1244
+
1245
+ else:
1246
+ records = attendance_manager.get_attendance_report({
1247
+ "date_range": (start_date.isoformat(), end_date.isoformat())
1248
+ })
1249
+
1250
+ summary = f"### Detailed Attendance Report: {start_date} to {end_date}\n\n"
1251
+ summary += f"Total Records: {len(records)}\n"
1252
+
1253
+ # Prepare DataFrame
1254
+ data = []
1255
+ for r in records:
1256
+ data.append({
1257
+ "Student": r["username"],
1258
+ "Full Name": r["full_name"] or "-",
1259
+ "Date": r["date"],
1260
+ "Time": r["time_in"],
1261
+ "Session": r["session_name"],
1262
+ "Status": r["status"]
1263
+ })
1264
+
1265
+ return summary, pd.DataFrame(data)
1266
+
1267
+ # Export report
1268
+ def export_report_csv(report_data, state):
1269
+ if not state.get("user") or state["user"]["role"] not in ["teacher", "admin"]:
1270
+ return "❌ You must be logged in as a teacher or admin."
1271
+
1272
+ if report_data is None or report_data.empty:
1273
+ return "❌ No data to export."
1274
+
1275
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
1276
+ filename = f"attendance_report_{timestamp}.csv"
1277
+
1278
+ # Convert DataFrame to dict list
1279
+ data = report_data.to_dict('records')
1280
+ filepath = report_generator.export_csv(data, filename)
1281
+
1282
+ if filepath.startswith("Error"):
1283
+ return f"❌ {filepath}"
1284
+ else:
1285
+ return f"βœ… Report exported successfully to {filepath}"
1286
+
1287
+ # Get users list
1288
+ def get_users_list(filter_type, state):
1289
+ if not state.get("user") or state["user"]["role"] != "admin":
1290
+ return []
1291
+
1292
+ role_filter = None
1293
+ if filter_type == "Students":
1294
+ role_filter = "student"
1295
+ elif filter_type == "Teachers":
1296
+ role_filter = "teacher"
1297
+ elif filter_type == "Admins":
1298
+ role_filter = "admin"
1299
+
1300
+ users = user_manager.get_users(role_filter)
1301
+
1302
+ return pd.DataFrame([
1303
+ {
1304
+ "ID": u["id"],
1305
+ "Username": u["username"],
1306
+ "Role": u["role"],
1307
+ "Email": u["email"] or "-",
1308
+ "Full Name": u["full_name"] or "-",
1309
+ "Active": "Yes" if u["active"] else "No"
1310
+ } for u in users
1311
+ ])
1312
+
1313
+ # Process user action
1314
+ def process_user_action(action, user_id, param, state):
1315
+ if not state.get("user") or state["user"]["role"] != "admin":
1316
+ return "❌ You must be an admin to perform this action."
1317
+
1318
+ if not user_id:
1319
+ return "❌ Please select a user ID."
1320
+
1321
+ # Connect and perform action
1322
+ try:
1323
+ with db.get_connection() as conn:
1324
+ c = conn.cursor()
1325
+
1326
+ if action == "Activate":
1327
+ c.execute("UPDATE users SET active=1 WHERE id=?", (int(user_id),))
1328
+ result = "User activated successfully."
1329
+
1330
+ elif action == "Deactivate":
1331
+ c.execute("UPDATE users SET active=0 WHERE id=?", (int(user_id),))
1332
+ result = "User deactivated successfully."
1333
+
1334
+ elif action == "Change Role":
1335
+ if param not in DEFAULT_ROLES:
1336
+ return f"❌ Invalid role. Must be one of: {', '.join(DEFAULT_ROLES)}"
1337
+ c.execute("UPDATE users SET role=? WHERE id=?", (param, int(user_id)))
1338
+ result = "User role updated successfully."
1339
+
1340
+ elif action == "Reset Password":
1341
+ if not param or len(param) < 8:
1342
+ return "❌ Password must be at least 8 characters."
1343
+
1344
+ salt = secrets.token_hex(SALT_LENGTH)
1345
+ password_hash = db._hash_password(param, salt)
1346
+ c.execute("UPDATE users SET password_hash=?, salt=? WHERE id=?",
1347
+ (password_hash, salt, int(user_id)))
1348
+ result = "Password reset successfully."
1349
+
1350
+ else:
1351
+ return "❌ Invalid action."
1352
+
1353
+ conn.commit()
1354
+ return f"βœ… {result}"
1355
+ except Exception as e:
1356
+ logger.error(f"Error in user action: {str(e)}")
1357
+ return f"❌ Error: {str(e)}"
1358
+
1359
+ # Get current settings
1360
+ def get_current_settings(state):
1361
+ if not state.get("user") or state["user"]["role"] != "admin":
1362
+ return ("", "light", "manual", False, 300, False, 0, 0, 100, False)
1363
+
1364
+ settings = settings_manager.get_settings()
1365
+ if not settings:
1366
+ return ("", "light", "manual", False, 300, False, 0, 0, 100, False)
1367
+
1368
+ return (
1369
+ settings.get("site_name", ""),
1370
+ settings.get("theme", "light"),
1371
+ settings.get("attendance_mode", "manual"),
1372
+ bool(settings.get("auto_close_window", False)),
1373
+ settings.get("default_window_time", 300),
1374
+ bool(settings.get("geo_verification", False)),
1375
+ float(settings.get("site_latitude", 0) or 0),
1376
+ float(settings.get("site_longitude", 0) or 0),
1377
+ settings.get("allowed_radius", 100),
1378
+ bool(settings.get("require_device_verification", False))
1379
+ )
1380
+
1381
+ # Save settings
1382
+ def save_settings(site_name, theme, mode, auto_close, window_time,
1383
+ geo_verify, site_lat, site_lon, radius, device_verify, state):
1384
+ if not state.get("user") or state["user"]["role"] != "admin":
1385
+ return "❌ You must be an admin to change settings."
1386
+
1387
+ updated_settings = {
1388
+ "site_name": site_name,
1389
+ "theme": theme,
1390
+ "attendance_mode": mode,
1391
+ "auto_close_window": 1 if auto_close else 0,
1392
+ "default_window_time": int(window_time),
1393
+ "geo_verification": 1 if geo_verify else 0,
1394
+ "require_device_verification": 1 if device_verify else 0
1395
+ }
1396
+
1397
+ if geo_verify:
1398
+ updated_settings.update({
1399
+ "site_latitude": str(site_lat),
1400
+ "site_longitude": str(site_lon),
1401
+ "allowed_radius": int(radius)
1402
+ })
1403
+
1404
+ success, message = settings_manager.update_settings(updated_settings)
1405
+ return format_message(success, message)
1406
+
1407
+ # Connect UI components to functions
1408
+ login_button.click(
1409
+ fn=do_login,
1410
+ inputs=[login_username, login_password, login_device],
1411
+ outputs=[login_output, session_state]
1412
+ )
1413
+
1414
+ reg_button.click(
1415
+ fn=do_register,
1416
+ inputs=[reg_username, reg_password, reg_confirm, reg_email, reg_fullname, reg_role],
1417
+ outputs=reg_output
1418
+ )
1419
+
1420
+ student_mark_btn.click(
1421
+ fn=do_mark_attendance,
1422
+ inputs=[student_code, session_state],
1423
+ outputs=student_status
1424
+ )
1425
+
1426
+ student_refresh_btn.click(
1427
+ fn=get_student_history,
1428
+ inputs=[session_state],
1429
+ outputs=student_history
1430
+ )
1431
+
1432
+ create_session_btn.click(
1433
+ fn=do_create_session,
1434
+ inputs=[session_name, session_desc, session_window, session_state],
1435
+ outputs=create_output
1436
+ )
1437
+
1438
+ active_refresh.click(
1439
+ fn=get_active_sessions,
1440
+ inputs=None,
1441
+ outputs=active_sessions
1442
+ )
1443
+
1444
+ close_btn.click(
1445
+ fn=do_close_session,
1446
+ inputs=[close_session_id, session_state],
1447
+ outputs=close_output
1448
+ )
1449
+
1450
+ report_gen_btn.click(
1451
+ fn=generate_report,
1452
+ inputs=[report_date_start, report_date_end, report_type, report_filter, session_state],
1453
+ outputs=[report_output, report_data]
1454
+ )
1455
+
1456
+ export_btn.click(
1457
+ fn=export_report_csv,
1458
+ inputs=[report_data, session_state],
1459
+ outputs=export_output
1460
+ )
1461
+
1462
+ user_refresh.click(
1463
+ fn=get_users_list,
1464
+ inputs=[user_filter, session_state],
1465
+ outputs=users_table
1466
+ )
1467
+
1468
+ user_action_btn.click(
1469
+ fn=process_user_action,
1470
+ inputs=[user_action, user_id, user_param, session_state],
1471
+ outputs=user_action_output
1472
+ )
1473
+
1474
+ settings_refresh.click(
1475
+ fn=get_current_settings,
1476
+ inputs=[session_state],
1477
+ outputs=[site_name, theme_setting, attendance_mode, auto_close, window_time,
1478
+ geo_verify, site_lat, site_lon, geo_radius, device_verify]
1479
+ )
1480
+
1481
+ settings_save.click(
1482
+ fn=save_settings,
1483
+ inputs=[site_name, theme_setting, attendance_mode, auto_close, window_time,
1484
+ geo_verify, site_lat, site_lon, geo_radius, device_verify, session_state],
1485
+ outputs=settings_output
1486
+ )
1487
+
1488
+ # Dynamic visibility and state update handlers
1489
+ def update_student_status(state):
1490
+ if not state:
1491
+ return "Please login to access student features"
1492
+ return f"Logged in as: {state.get('username', 'Unknown')}"
1493
+
1494
+ def update_teacher_status(state):
1495
+ if not state:
1496
+ return "Please login to access teacher features"
1497
+
1498
+ user = state.get("user", {})
1499
+ if user and user.get("role") in ["teacher", "admin"]:
1500
+ return f"πŸ‘‹ Welcome, {user.get('username')}! You have access to teacher features."
1501
+ else:
1502
+ return "❌ You must be logged in as a teacher to access these features."
1503
+
1504
+ def update_admin_status(state):
1505
+ if not state:
1506
+ return "Please login to access admin features"
1507
+
1508
+ user = state.get("user", {})
1509
+ if user and user.get("role") == "admin":
1510
+ return f"πŸ‘‹ Welcome, Administrator {user.get('username')}!"
1511
+ else:
1512
+ return "❌ You must be logged in as an admin to access these features."
1513
+
1514
+ def update_teacher_tabs_visibility(state):
1515
+ user = state.get("user", {})
1516
+ return gr.update(visible=(user and user.get("role") in ["teacher", "admin"]))
1517
+
1518
+ def update_admin_tabs_visibility(state):
1519
+ user = state.get("user", {})
1520
+ return gr.update(visible=(user and user.get("role") == "admin"))
1521
+
1522
+ def update_geo_settings_visibility(geo_verify):
1523
+ return gr.update(visible=geo_verify)
1524
+
1525
+ session_state.change(
1526
+ fn=update_student_status,
1527
+ inputs=session_state,
1528
+ outputs=student_status
1529
+ )
1530
+
1531
+ session_state.change(
1532
+ fn=update_teacher_status,
1533
+ inputs=session_state,
1534
+ outputs=teacher_status
1535
+ )
1536
+
1537
+ session_state.change(
1538
+ fn=update_admin_status,
1539
+ inputs=session_state,
1540
+ outputs=admin_status
1541
+ )
1542
+
1543
+ session_state.change(
1544
+ fn=update_teacher_tabs_visibility,
1545
+ inputs=session_state,
1546
+ outputs=teacher_tabs
1547
+ )
1548
+
1549
+ session_state.change(
1550
+ fn=update_admin_tabs_visibility,
1551
+ inputs=session_state,
1552
+ outputs=admin_tabs
1553
+ )
1554
+
1555
+ geo_verify.change(
1556
+ fn=update_geo_settings_visibility,
1557
+ inputs=geo_verify,
1558
+ outputs=geo_settings
1559
+ )
1560
+
1561
+ # Change theme function
1562
+ def change_theme(choice):
1563
+ return gr.update(theme=choice.lower())
1564
+
1565
+ theme_toggle.change(
1566
+ fn=change_theme,
1567
+ inputs=theme_toggle,
1568
+ outputs=app
1569
+ )
1570
 
1571
+ # Launch the app
1572
+ if __name__ == "__main__":
1573
+ # Create the log directory if it doesn't exist
1574
+ os.makedirs("logs", exist_ok=True)
1575
+
1576
+ # Start the app
1577
+ app.launch()