From 047acd46c300c5dfdb918b64f197146a2d2cafb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:08:48 +0000 Subject: [PATCH 1/4] Initial plan From 0be789df6d6e379c86c8ade0873de637e7b5fa4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:11:29 +0000 Subject: [PATCH 2/4] Initial commit - planning microservices refactoring Co-authored-by: Ritvik-km <116180586+Ritvik-km@users.noreply.github.com> --- __pycache__/app_display.cpython-312.pyc | Bin 0 -> 55868 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 __pycache__/app_display.cpython-312.pyc diff --git a/__pycache__/app_display.cpython-312.pyc b/__pycache__/app_display.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3b5f0b7d7bbac9858063d046fc5a6b735c67bca GIT binary patch literal 55868 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 S2C)ag&;lCN%3q`aOnCq@5Hz3w literal 0 HcmV?d00001 From 28e407f7fc687e2a2bf6f8d46f7c0ebea881ad7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:14:45 +0000 Subject: [PATCH 3/4] Create microservices architecture - 5 service modules Co-authored-by: Ritvik-km <116180586+Ritvik-km@users.noreply.github.com> --- .gitignore | 33 ++ __pycache__/app_display.cpython-312.pyc | Bin 55868 -> 0 bytes admin_service.py | 107 +++++++ attendance_service.py | 143 +++++++++ database_service.py | 185 +++++++++++ face_recognition_service.py | 128 ++++++++ main_app.py | 408 ++++++++++++++++++++++++ 7 files changed, 1004 insertions(+) create mode 100644 .gitignore delete mode 100644 __pycache__/app_display.cpython-312.pyc create mode 100644 admin_service.py create mode 100644 attendance_service.py create mode 100644 database_service.py create mode 100644 face_recognition_service.py create mode 100644 main_app.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d11aba --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Python cache files +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python + +# Virtual environment +venv/ +env/ +.venv/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log + +# Database files +*.db +*.sqlite + +# Environment variables +.env.local +.env.development +.env.production \ No newline at end of file diff --git a/__pycache__/app_display.cpython-312.pyc b/__pycache__/app_display.cpython-312.pyc deleted file mode 100644 index e3b5f0b7d7bbac9858063d046fc5a6b735c67bca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55868 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 S2C)ag&;lCN%3q`aOnCq@5Hz3w diff --git a/admin_service.py b/admin_service.py new file mode 100644 index 0000000..9c7686a --- /dev/null +++ b/admin_service.py @@ -0,0 +1,107 @@ +""" +Admin Service Module +Handles admin authentication, dashboard, and management operations +""" +import bcrypt +import mysql.connector +from tkinter import messagebox + +class InvalidDepartmentSelectionError(Exception): + pass + +class AdminService: + def __init__(self, database_service): + self.db = database_service + + def authenticate_admin(self, username, password): + """Authenticate admin user""" + try: + admin_record = self.db.get_admin_by_username(username) + if admin_record: + stored_password = admin_record[2] # password is at index 2 + if bcrypt.checkpw(password.encode('utf-8'), stored_password): + return True, "Login successful" + else: + return False, "Invalid password" + else: + return False, "Admin not found" + except Exception as e: + return False, f"Authentication error: {str(e)}" + + def add_new_admin(self, username, password): + """Add new admin user""" + if not username or not password: + return False, "Please fill all fields." + + try: + self.db.add_admin(username, password) + return True, "New admin added successfully." + except mysql.connector.IntegrityError: + return False, "Username already exists." + except Exception as e: + return False, f"Error adding admin: {str(e)}" + + def change_admin_password(self, current_username, current_password, new_password): + """Change admin password""" + if not new_password: + return False, "Please enter a new password." + + # Verify current credentials + is_authenticated, message = self.authenticate_admin(current_username, current_password) + if not is_authenticated: + return False, "Current password is incorrect." + + try: + self.db.update_admin_password(current_username, new_password) + return True, "Password changed successfully." + except Exception as e: + return False, f"Error changing password: {str(e)}" + + def get_dashboard_data(self): + """Get dashboard data including total employees, attendance rate, and department data""" + try: + total_employees = self.db.get_total_employees() + attendance_rate = self.db.get_attendance_rate() + attendance_by_department = self.db.get_attendance_by_department() + + attendance_data = [ + (dept, attendance_count, (attendance_count / employee_count) * 100 if employee_count > 0 else 0) + for dept, employee_count, attendance_count in attendance_by_department + ] + return total_employees, attendance_rate, attendance_data + except mysql.connector.Error as err: + print(f"Error: {err}") + return 0, 0, [] + + def get_all_departments(self): + """Get all active departments""" + return self.db.get_all_departments() + + def register_employee(self, emp_id, emp_name, emp_department, emp_designation, face_encoding): + """Register new employee""" + if not emp_id or not emp_name or not emp_department or not emp_designation: + return False, "Please fill all the fields." + + try: + emp_dept_id = self.db.get_department_id_by_name(emp_department) + if emp_dept_id is None: + return False, "Invalid department selected." + + encoding_data = face_encoding.tobytes() + self.db.add_employee(emp_id, emp_name, emp_dept_id, emp_designation, encoding_data) + return True, "Employee registered successfully." + except mysql.connector.IntegrityError: + return False, "Employee ID already exists." + except Exception as e: + return False, f"Error registering employee: {str(e)}" + + def validate_department_selection(self, selected_department): + """Validate if the selected department is valid""" + if selected_department == "Select Department" or not selected_department: + raise InvalidDepartmentSelectionError("Please select a valid department.") + + departments = self.get_all_departments() + if selected_department not in departments: + raise InvalidDepartmentSelectionError("Invalid department selected.") + + return True \ No newline at end of file diff --git a/attendance_service.py b/attendance_service.py new file mode 100644 index 0000000..7bb6799 --- /dev/null +++ b/attendance_service.py @@ -0,0 +1,143 @@ +""" +Attendance Service Module +Handles attendance marking and viewing operations +""" +import os +import pandas as pd +import face_recognition +from datetime import datetime, timedelta +from tkinter import messagebox, filedialog + +class AttendanceService: + def __init__(self, database_service, face_recognition_service): + self.db = database_service + self.face_service = face_recognition_service + + def mark_attendance(self, emp_id, is_markin=True): + """Mark attendance for an employee""" + if not emp_id: + return False, "Employee ID is required." + + # Process image for attendance + frame, face_encoding, error = self.face_service.process_attendance_image(emp_id) + if error: + return False, error + + # Get employee record + employee_record = self.db.get_employee_by_id(emp_id) + if not employee_record: + return False, "Employee not found." + + name = employee_record[0] + known_face_encoding = employee_record[1] + + # Validate face + is_match, face_distance = self.face_service.validate_employee_face(face_encoding, known_face_encoding) + + if is_match: + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + date_today = datetime.now().strftime("%Y-%m-%d") + + if is_markin: + return self._handle_checkin(emp_id, name, timestamp, frame, face_encoding) + else: + return self._handle_checkout(emp_id, name, timestamp, date_today, frame, face_encoding) + else: + # Save failed attempt image + self._save_failed_attempt(emp_id, frame, face_encoding) + return False, f"Face recognition failed. Distance: {face_distance:.3f}" + + def _handle_checkin(self, emp_id, name, timestamp, frame, face_encoding): + """Handle employee check-in""" + date_today = datetime.now().strftime("%Y-%m-%d") + + # Check if already checked in today + existing_record = self.db.check_attendance_exists(emp_id, date_today) + if existing_record: + return False, "Attendance already marked for today." + + # Save image and add attendance record + output_dir = "C:\\Users\\kumar\\Desktop\\attend" + output_dir = os.path.join(output_dir, date_today) + + image_path = self.face_service.save_face_image(frame, face_recognition.face_locations(frame), emp_id, output_dir) + transaction_id = self.db.add_attendance_in(emp_id, timestamp, image_path) + + return True, f"Checked in {name} at {timestamp}" + + def _handle_checkout(self, emp_id, name, timestamp, date_today, frame, face_encoding): + """Handle employee check-out""" + # Check if there's a check-in record for today + existing_record = self.db.check_attendance_exists(emp_id, date_today) + if not existing_record: + return False, "No check-in record found for today." + + # Save checkout image + output_dir_out = "C:\\Users\\kumar\\Desktop\\attend" + output_dir_out = os.path.join(output_dir_out, date_today) + + image_path = self.face_service.save_face_image(frame, face_recognition.face_locations(frame), f"{emp_id}_out", output_dir_out) + self.db.update_attendance_out(emp_id, timestamp, image_path, date_today) + + return True, f"Checked out {name} at {timestamp}" + + def _save_failed_attempt(self, emp_id, frame, face_encoding): + """Save failed attendance attempt image""" + output_dir = "C:\\Users\\kumar\\Desktop\\failed_attempts" + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + output_dir = os.path.join(output_dir, emp_id) + + self.face_service.save_face_image(frame, face_recognition.face_locations(frame), timestamp, output_dir) + + def get_attendance_records(self, selected_department, start_date, end_date): + """Get attendance records for a department within date range""" + try: + start_datetime = datetime.combine(start_date, datetime.min.time()) + end_datetime = datetime.combine(end_date, datetime.min.time()) + timedelta(days=1) + + export_records = self.db.get_attendance_records(selected_department, start_datetime, end_datetime) + + # Generate full date range for display + attendance_display_data = [] + full_date_range = [start_datetime + timedelta(days=i) for i in range((end_datetime - start_datetime).days)] + + for single_date in full_date_range: + date_str = single_date.strftime('%Y-%m-%d') + daily_records = self.db.get_daily_attendance_records(selected_department, single_date.date()) + + if daily_records: + for record in daily_records: + emp_id, emp_name, checkin_time, checkout_time, worktime = record + + checkin_display = checkin_time.strftime('%H:%M:%S') if checkin_time else "N/A" + checkout_display = checkout_time.strftime('%H:%M:%S') if checkout_time else "N/A" + worktime_display = str(worktime) if worktime else "N/A" + + attendance_display_data.append((date_str, emp_id, emp_name, checkin_display, checkout_display, worktime_display)) + else: + attendance_display_data.append((date_str, "No records", "", "", "", "")) + + return attendance_display_data, export_records + + except Exception as e: + messagebox.showerror("Error", f"Failed to fetch attendance data: {str(e)}") + return [], [] + + def export_attendance_to_excel(self, records): + """Export attendance records to Excel file""" + if not records: + messagebox.showerror("Error", "No data available to export.") + return False + + df = pd.DataFrame(records, columns=['Date', 'Employee ID', 'Name', 'Checkin Time', 'Checkout Time', 'Worked For']) + + file_path = filedialog.asksaveasfilename(defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx")]) + if file_path: + try: + df.to_excel(file_path, index=False) + messagebox.showinfo("Success", f"Attendance data has been exported to {file_path}") + return True + except Exception as e: + messagebox.showerror("Error", f"Failed to export data: {str(e)}") + return False + return False \ No newline at end of file diff --git a/database_service.py b/database_service.py new file mode 100644 index 0000000..9981d4f --- /dev/null +++ b/database_service.py @@ -0,0 +1,185 @@ +""" +Database Service Module +Handles all database operations for the Biometric Attendance System +""" +import mysql.connector +import os +from dotenv import load_dotenv +import bcrypt + +class DatabaseService: + def __init__(self): + load_dotenv() + self.host = os.getenv("host") + self.user = os.getenv("user") + self.password = os.getenv("password") + self.database = os.getenv("database") + self.conn = None + self.cursor = None + self.connect() + self.initialize_tables() + + def connect(self): + """Establish database connection""" + self.conn = mysql.connector.connect( + host=self.host, + user=self.user, + password=self.password, + database=self.database + ) + self.cursor = self.conn.cursor() + + def initialize_tables(self): + """Create necessary tables if they don't exist""" + # Create admins table + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS admins ( + id INT PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(255) UNIQUE, + password VARCHAR(255) + ) + ''') + + # Check if default admin exists, if not create one + self.cursor.execute("SELECT * FROM admins") + if self.cursor.fetchone() is None: + hashed_password = bcrypt.hashpw("admin123".encode('utf-8'), bcrypt.gensalt()) + self.cursor.execute("INSERT INTO admins (username, password) VALUES (%s, %s)", ('admin', hashed_password)) + self.conn.commit() + + def get_admin_by_username(self, username): + """Get admin by username""" + self.cursor.execute("SELECT * FROM admins WHERE username=%s", (username,)) + return self.cursor.fetchone() + + def add_admin(self, username, password): + """Add new admin""" + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + self.cursor.execute("INSERT INTO admins (username, password) VALUES (%s, %s)", (username, hashed_password)) + self.conn.commit() + + def update_admin_password(self, username, new_password): + """Update admin password""" + hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()) + self.cursor.execute("UPDATE admins SET password=%s WHERE username=%s", (hashed_password, username)) + self.conn.commit() + + def get_employee_by_id(self, emp_id): + """Get employee by ID""" + self.cursor.execute("SELECT em_employee_name, em_employee_face_encoding FROM employee_master WHERE em_employee_id=%s", (emp_id,)) + return self.cursor.fetchone() + + def add_employee(self, emp_id, emp_name, emp_dept_id, emp_designation, face_encoding): + """Add new employee""" + self.cursor.execute(""" + INSERT INTO employee_master + (em_employee_id, em_employee_name, em_employee_dept, em_employee_designation, em_employee_face_encoding, em_employee_active) + VALUES (%s, %s, %s, %s, %s, %s) + """, (emp_id, emp_name, emp_dept_id, emp_designation, face_encoding, 1)) + self.conn.commit() + + def get_department_id_by_name(self, dept_name): + """Get department ID by name""" + self.cursor.execute("SELECT dm_dept_id FROM department_master WHERE dm_dept_desc=%s", (dept_name,)) + result = self.cursor.fetchone() + return result[0] if result else None + + def get_all_departments(self): + """Get all active departments""" + self.cursor.execute("SELECT dm_dept_desc FROM department_master WHERE dm_dept_active = 1") + return [row[0] for row in self.cursor.fetchall()] + + def get_total_employees(self): + """Get total number of active employees""" + self.cursor.execute("SELECT COUNT(*) FROM employee_master WHERE em_employee_active = 1") + return self.cursor.fetchone()[0] + + def get_attendance_rate(self): + """Get today's attendance rate""" + self.cursor.execute(""" + SELECT + (SELECT COUNT(DISTINCT et_employee_id) + FROM employee_transactions + WHERE DATE(et_employee_in_time) = CURRENT_DATE) / + (SELECT COUNT(*) + FROM employee_master + WHERE em_employee_active = 1) * 100 AS attendance_rate; + """) + result = self.cursor.fetchone()[0] + return result or 0 + + def get_attendance_by_department(self): + """Get attendance data by department""" + self.cursor.execute(""" + SELECT + dm.dm_dept_desc AS department_desc, + COUNT(em.em_employee_id) AS employee_count, + COUNT(DISTINCT et.et_employee_id) AS attendance_count + FROM + department_master dm + LEFT JOIN + employee_master em ON dm.dm_dept_id = em.em_employee_dept AND em.em_employee_active = 1 + LEFT JOIN + employee_transactions et ON em.em_employee_id = et.et_employee_id + AND DATE(et.et_employee_in_time) = CURRENT_DATE + WHERE + dm.dm_dept_active = 1 + GROUP BY + dm.dm_dept_desc; + """) + return self.cursor.fetchall() + + def check_attendance_exists(self, emp_id, date_today): + """Check if attendance already exists for employee today""" + self.cursor.execute(""" + SELECT * FROM employee_transactions + WHERE et_employee_id=%s AND DATE(et_employee_in_time)=%s AND DATE(et_employee_out_time)=%s + """, (emp_id, date_today, date_today)) + return self.cursor.fetchone() + + def add_attendance_in(self, emp_id, timestamp, image_path): + """Add check-in attendance""" + self.cursor.execute(""" + INSERT INTO employee_transactions (et_employee_id, et_employee_in_time, et_employee_in_imgpth) + VALUES (%s, %s, %s) + """, (emp_id, timestamp, image_path)) + self.conn.commit() + return self.cursor.lastrowid + + def update_attendance_out(self, emp_id, timestamp, image_path, date_today): + """Update check-out attendance""" + self.cursor.execute(""" + UPDATE employee_transactions + SET et_employee_out_time=%s, et_employee_out_imgpth=%s, et_worktime = TIMEDIFF(et_employee_out_time, et_employee_in_time) + WHERE et_employee_id=%s AND DATE(et_employee_in_time)=%s AND et_employee_out_time IS NULL + """, (timestamp, image_path, emp_id, date_today)) + self.conn.commit() + + def get_attendance_records(self, department, start_datetime, end_datetime): + """Get attendance records for a department within date range""" + self.cursor.execute(''' + SELECT DATE(et.et_employee_in_time), et.et_employee_id, em.em_employee_name, et.et_employee_in_time, et.et_employee_out_time, et.et_worktime + FROM employee_transactions et + JOIN employee_master em ON et.et_employee_id = em.em_employee_id + JOIN department_master d ON em.em_employee_dept = d.dm_dept_id + WHERE d.dm_dept_desc = %s AND et.et_employee_in_time >= %s AND et.et_employee_in_time < %s; + ''', (department, start_datetime, end_datetime)) + return self.cursor.fetchall() + + def get_daily_attendance_records(self, department, single_date): + """Get daily attendance records for a department""" + self.cursor.execute(''' + SELECT et.et_employee_id, em.em_employee_name, et.et_employee_in_time, et.et_employee_out_time, et.et_worktime + FROM employee_transactions et + JOIN employee_master em ON et.et_employee_id = em.em_employee_id + JOIN department_master d ON em.em_employee_dept = d.dm_dept_id + WHERE d.dm_dept_desc = %s AND DATE(et.et_employee_in_time) = %s; + ''', (department, single_date)) + return self.cursor.fetchall() + + def close(self): + """Close database connection""" + if self.cursor: + self.cursor.close() + if self.conn: + self.conn.close() \ No newline at end of file diff --git a/face_recognition_service.py b/face_recognition_service.py new file mode 100644 index 0000000..ed1c2d8 --- /dev/null +++ b/face_recognition_service.py @@ -0,0 +1,128 @@ +""" +Face Recognition Service Module +Handles all face detection, encoding, and recognition operations +""" +import cv2 +import face_recognition +import numpy as np +import os +from datetime import datetime + +class FaceRecognitionService: + def __init__(self): + self.FACE_DISTANCE_THRESHOLD = 0.6 + + def capture_image_from_camera(self): + """Capture image from camera""" + cap = cv2.VideoCapture(0) + ret, frame = cap.read() + cap.release() + return ret, frame + + def detect_faces(self, frame): + """Detect faces in the frame and return face locations""" + small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25) + rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) + face_locations = face_recognition.face_locations(rgb_small_frame) + return face_locations, rgb_small_frame + + def encode_face(self, rgb_frame, face_locations): + """Generate face encoding from detected face""" + if len(face_locations) == 0: + return None, "No face detected in the image." + elif len(face_locations) > 1: + return None, "Multiple faces detected. Ensure only one face is in the frame." + + face_encodings = face_recognition.face_encodings(rgb_frame, face_locations) + if len(face_encodings) > 0: + return face_encodings[0], None + return None, "Could not encode face." + + def compare_faces(self, known_face_encoding, face_encoding): + """Compare face encodings and determine if they match""" + if known_face_encoding is None or face_encoding is None: + return False, 1.0 + + face_distance = face_recognition.face_distance([known_face_encoding], face_encoding)[0] + matches = face_distance <= self.FACE_DISTANCE_THRESHOLD + return matches, face_distance + + def extract_and_save_face(self, frame, face_locations, emp_id, output_dir): + """Extract face from frame and save to directory""" + if len(face_locations) > 0: + y1, x2, y2, x1 = face_locations[0] + face_image = frame[y1*4:y2*4, x1*4:x2*4] + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + image_path = os.path.join(output_dir, f"{emp_id}.jpg") + cv2.imwrite(image_path, face_image) + return image_path + return None + + def save_face_image(self, frame, face_loc, id, output_dir): + """Save face image for attendance tracking""" + y1, x2, y2, x1 = face_loc[0], face_loc[1], face_loc[2], face_loc[3] + face_image = frame[y1*4:y2*4, x1*4:x2*4] + + # Display captured image briefly + cv2.imshow("Captured image", frame) + cv2.waitKey(2000) + cv2.destroyAllWindows() + + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + image_path = os.path.join(output_dir, f"{id}.jpg") + cv2.imwrite(image_path, face_image) + return image_path + + def process_attendance_image(self, emp_id): + """Process image for attendance marking""" + cap = cv2.VideoCapture(0) + ret, frame = cap.read() + cap.release() + + if not ret: + return None, None, "Failed to capture image." + + face_locations, rgb_small_frame = self.detect_faces(frame) + + if len(face_locations) == 0: + return None, None, "No face detected." + + face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations) + + if len(face_encodings) > 0: + face_encoding = face_encodings[0] + return frame, face_encoding, None + + return None, None, "Could not encode face." + + def validate_employee_face(self, face_encoding, known_face_encoding_bytes): + """Validate employee face against stored encoding""" + if known_face_encoding_bytes is None: + return False, 1.0 + + known_face_encoding = np.frombuffer(known_face_encoding_bytes) + return self.compare_faces(known_face_encoding, face_encoding) + + def create_camera_window_update_function(self, lmain, cap, capture_window): + """Create update function for camera preview window""" + def update_frame(): + ret, frame = cap.read() + if ret: + cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) + from PIL import Image, ImageTk + img = Image.fromarray(cv2image) + imgtk = ImageTk.PhotoImage(image=img) + lmain.imgtk = imgtk + lmain.configure(image=imgtk) + lmain.after(10, update_frame) + else: + cap.release() + capture_window.destroy() + from tkinter import messagebox + messagebox.showerror("Error", "Failed to access camera.") + return update_frame \ No newline at end of file diff --git a/main_app.py b/main_app.py new file mode 100644 index 0000000..6ac0a39 --- /dev/null +++ b/main_app.py @@ -0,0 +1,408 @@ +""" +Main Application File +Biometric Attendance System - Microservices Architecture +This file orchestrates all the services and provides the GUI interface +""" +import tkinter as tk +import os +import sys +import cv2 +import numpy as np +from tkinter import messagebox, ttk +from PIL import Image, ImageTk +from tkcalendar import DateEntry +from datetime import datetime, timedelta +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg + +# Import our microservices +from database_service import DatabaseService +from face_recognition_service import FaceRecognitionService +from admin_service import AdminService, InvalidDepartmentSelectionError +from attendance_service import AttendanceService + +class BiometricAttendanceApp: + def __init__(self): + # Initialize services + self.db_service = DatabaseService() + self.face_service = FaceRecognitionService() + self.admin_service = AdminService(self.db_service) + self.attendance_service = AttendanceService(self.db_service, self.face_service) + + # Initialize GUI + self.root = tk.Tk() + self.root.title("Biometric Attendance System") + self.root.geometry("800x600") + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + + # Create frames + self.setup_frames() + self.setup_main_frame() + self.setup_admin_login_frame() + self.setup_attendance_frame() + + # Show initial frame + self.show_frame(self.main_frame) + + def setup_frames(self): + """Setup main container frames""" + self.main_frame = tk.Frame(self.root) + self.admin_login_frame = tk.Frame(self.root) + self.admin_dashboard_frame = tk.Frame(self.root) + self.attendance_frame = tk.Frame(self.root) + + for frame in (self.main_frame, self.admin_login_frame, self.admin_dashboard_frame, self.attendance_frame): + frame.grid(row=0, column=0, sticky="nsew") + + def show_frame(self, frame): + """Show specified frame""" + frame.tkraise() + + def on_closing(self): + """Handle application closing""" + if messagebox.askokcancel("Quit", "Do you want to quit?"): + try: + self.db_service.close() + except: + pass + self.root.destroy() + sys.exit() + + def setup_main_frame(self): + """Setup main welcome frame""" + for widget in self.main_frame.winfo_children(): + widget.destroy() + + tk.Label(self.main_frame, text="Biometric Attendance System", font=("Arial", 24)).pack(pady=50) + + btn_admin = tk.Button(self.main_frame, text="Admin Panel", font=("Arial", 16), + command=lambda: self.show_frame(self.admin_login_frame)) + btn_admin.pack(pady=20) + + btn_attendance = tk.Button(self.main_frame, text="Mark Attendance", font=("Arial", 16), + command=lambda: self.show_frame(self.attendance_frame)) + btn_attendance.pack(pady=20) + + def setup_admin_login_frame(self): + """Setup admin login frame""" + for widget in self.admin_login_frame.winfo_children(): + widget.destroy() + + tk.Label(self.admin_login_frame, text="Admin Login", font=("Arial", 20)).pack(pady=20) + + tk.Label(self.admin_login_frame, text="Username:").pack(pady=5) + self.admin_username_entry = tk.Entry(self.admin_login_frame) + self.admin_username_entry.pack(pady=5) + + tk.Label(self.admin_login_frame, text="Password:").pack(pady=5) + self.admin_password_entry = tk.Entry(self.admin_login_frame, show="*") + self.admin_password_entry.pack(pady=5) + + btn_login = tk.Button(self.admin_login_frame, text="Login", command=self.admin_login) + btn_login.pack(pady=10) + + btn_back = tk.Button(self.admin_login_frame, text="Back", command=lambda: self.show_frame(self.main_frame)) + btn_back.pack(pady=10) + + def admin_login(self): + """Handle admin login""" + username = self.admin_username_entry.get() + password = self.admin_password_entry.get() + + is_authenticated, message = self.admin_service.authenticate_admin(username, password) + + if is_authenticated: + self.current_admin = username + self.setup_admin_dashboard() + self.show_frame(self.admin_dashboard_frame) + self.admin_username_entry.delete(0, tk.END) + self.admin_password_entry.delete(0, tk.END) + else: + messagebox.showerror("Login Failed", message) + + def setup_admin_dashboard(self): + """Setup admin dashboard frame""" + for widget in self.admin_dashboard_frame.winfo_children(): + widget.destroy() + + tk.Label(self.admin_dashboard_frame, text="Admin Dashboard", font=("Arial", 20)).pack(pady=20) + + # Dashboard data + total_employees, attendance_rate, attendance_data = self.admin_service.get_dashboard_data() + + # Display dashboard info + info_frame = tk.Frame(self.admin_dashboard_frame) + info_frame.pack(pady=10) + + tk.Label(info_frame, text=f"Total Employees: {total_employees}", font=("Arial", 12)).pack() + tk.Label(info_frame, text=f"Today's Attendance Rate: {attendance_rate:.1f}%", font=("Arial", 12)).pack() + + # Dashboard plot + if attendance_data: + self.create_dashboard_plot(attendance_data) + + # Admin menu buttons + btn_frame = tk.Frame(self.admin_dashboard_frame) + btn_frame.pack(pady=20) + + tk.Button(btn_frame, text="View Attendance", command=self.setup_view_attendance_frame).pack(side=tk.LEFT, padx=5) + tk.Button(btn_frame, text="Register Employee", command=self.setup_registration_frame).pack(side=tk.LEFT, padx=5) + tk.Button(btn_frame, text="Add Admin", command=self.setup_add_admin_frame).pack(side=tk.LEFT, padx=5) + tk.Button(btn_frame, text="Change Password", command=self.setup_change_password_frame).pack(side=tk.LEFT, padx=5) + tk.Button(btn_frame, text="Logout", command=lambda: self.show_frame(self.main_frame)).pack(side=tk.LEFT, padx=5) + + def create_dashboard_plot(self, attendance_data): + """Create attendance dashboard plot""" + departments = [data[0] for data in attendance_data] + attendance_percentages = [data[2] for data in attendance_data] + + fig, ax = plt.subplots(figsize=(8, 4)) + ax.bar(departments, attendance_percentages) + ax.set_ylabel('Attendance Percentage') + ax.set_title('Department-wise Attendance Today') + ax.set_ylim(0, 100) + + canvas = FigureCanvasTkAgg(fig, self.admin_dashboard_frame) + canvas.draw() + canvas.get_tk_widget().pack() + + def setup_view_attendance_frame(self): + """Setup view attendance frame""" + view_window = tk.Toplevel(self.root) + view_window.title("View Attendance") + view_window.geometry("800x600") + + tk.Label(view_window, text="View Attendance Records", font=("Arial", 16)).pack(pady=10) + + # Department selection + tk.Label(view_window, text="Select Department:").pack() + departments = self.admin_service.get_all_departments() + department_var = tk.StringVar(value="Select Department") + department_combo = ttk.Combobox(view_window, textvariable=department_var, values=departments, state="readonly") + department_combo.pack(pady=5) + + # Date selection + date_frame = tk.Frame(view_window) + date_frame.pack(pady=10) + + tk.Label(date_frame, text="From:").grid(row=0, column=0, padx=5) + start_date = DateEntry(date_frame) + start_date.grid(row=0, column=1, padx=5) + + tk.Label(date_frame, text="To:").grid(row=0, column=2, padx=5) + end_date = DateEntry(date_frame) + end_date.grid(row=0, column=3, padx=5) + + # Display button + def display_attendance(): + selected_department = department_var.get() + try: + self.admin_service.validate_department_selection(selected_department) + attendance_data, export_records = self.attendance_service.get_attendance_records( + selected_department, start_date.get(), end_date.get()) + + # Create treeview for display + tree = ttk.Treeview(view_window, columns=('Date', 'EmpID', 'Name', 'CheckIn', 'CheckOut', 'WorkTime'), show='headings') + + for col in tree['columns']: + tree.heading(col, text=col) + tree.column(col, width=100) + + for record in attendance_data: + tree.insert('', tk.END, values=record) + + tree.pack(fill=tk.BOTH, expand=True, pady=10) + + # Export button + tk.Button(view_window, text="Export to Excel", + command=lambda: self.attendance_service.export_attendance_to_excel(export_records)).pack(pady=5) + + except InvalidDepartmentSelectionError as e: + messagebox.showerror("Error", str(e)) + + tk.Button(view_window, text="Display Attendance", command=display_attendance).pack(pady=10) + + def setup_registration_frame(self): + """Setup employee registration frame""" + reg_window = tk.Toplevel(self.root) + reg_window.title("Register Employee") + reg_window.geometry("400x500") + + tk.Label(reg_window, text="Employee Registration", font=("Arial", 16)).pack(pady=10) + + # Form fields + tk.Label(reg_window, text="Employee ID:").pack() + emp_id_entry = tk.Entry(reg_window) + emp_id_entry.pack(pady=5) + + tk.Label(reg_window, text="Name:").pack() + name_entry = tk.Entry(reg_window) + name_entry.pack(pady=5) + + tk.Label(reg_window, text="Department:").pack() + departments = self.admin_service.get_all_departments() + dept_var = tk.StringVar(value="Select Department") + dept_combo = ttk.Combobox(reg_window, textvariable=dept_var, values=departments, state="readonly") + dept_combo.pack(pady=5) + + tk.Label(reg_window, text="Designation:").pack() + designation_entry = tk.Entry(reg_window) + designation_entry.pack(pady=5) + + def capture_image(): + """Capture and process employee image""" + capture_window = tk.Toplevel(reg_window) + capture_window.title("Capture Image") + + lmain = tk.Label(capture_window) + lmain.pack() + + cap = cv2.VideoCapture(0) + + update_frame = self.face_service.create_camera_window_update_function(lmain, cap, capture_window) + + def capture_and_save(): + ret, frame = cap.read() + if ret: + cap.release() + capture_window.destroy() + + face_locations, rgb_frame = self.face_service.detect_faces(frame) + face_encoding, error = self.face_service.encode_face(rgb_frame, face_locations) + + if error: + messagebox.showerror("Error", error) + return + + emp_id = emp_id_entry.get() + emp_name = name_entry.get() + emp_department = dept_var.get() + emp_designation = designation_entry.get() + + # Save face image + output_dir = "C:\\Users\\kumar\\Desktop\\register" + self.face_service.extract_and_save_face(frame, face_locations, emp_id, output_dir) + + # Register employee + success, message = self.admin_service.register_employee( + emp_id, emp_name, emp_department, emp_designation, face_encoding) + + if success: + messagebox.showinfo("Success", message) + reg_window.destroy() + else: + messagebox.showerror("Error", message) + + tk.Button(capture_window, text="Capture", command=capture_and_save).pack() + + def on_closing(): + cap.release() + capture_window.destroy() + + capture_window.protocol("WM_DELETE_WINDOW", on_closing) + update_frame() + + tk.Button(reg_window, text="Capture Image", command=capture_image).pack(pady=20) + + def setup_add_admin_frame(self): + """Setup add admin frame""" + admin_window = tk.Toplevel(self.root) + admin_window.title("Add Admin") + admin_window.geometry("300x200") + + tk.Label(admin_window, text="Add New Admin", font=("Arial", 16)).pack(pady=10) + + tk.Label(admin_window, text="Username:").pack() + username_entry = tk.Entry(admin_window) + username_entry.pack(pady=5) + + tk.Label(admin_window, text="Password:").pack() + password_entry = tk.Entry(admin_window, show="*") + password_entry.pack(pady=5) + + def save_admin(): + username = username_entry.get() + password = password_entry.get() + + success, message = self.admin_service.add_new_admin(username, password) + + if success: + messagebox.showinfo("Success", message) + admin_window.destroy() + else: + messagebox.showerror("Error", message) + + tk.Button(admin_window, text="Save", command=save_admin).pack(pady=10) + + def setup_change_password_frame(self): + """Setup change password frame""" + pwd_window = tk.Toplevel(self.root) + pwd_window.title("Change Password") + pwd_window.geometry("300x250") + + tk.Label(pwd_window, text="Change Admin Password", font=("Arial", 16)).pack(pady=10) + + tk.Label(pwd_window, text="Current Password:").pack() + current_pwd_entry = tk.Entry(pwd_window, show="*") + current_pwd_entry.pack(pady=5) + + tk.Label(pwd_window, text="New Password:").pack() + new_pwd_entry = tk.Entry(pwd_window, show="*") + new_pwd_entry.pack(pady=5) + + def change_password(): + current_pwd = current_pwd_entry.get() + new_pwd = new_pwd_entry.get() + + success, message = self.admin_service.change_admin_password( + self.current_admin, current_pwd, new_pwd) + + if success: + messagebox.showinfo("Success", message) + pwd_window.destroy() + else: + messagebox.showerror("Error", message) + + tk.Button(pwd_window, text="Change Password", command=change_password).pack(pady=10) + + def setup_attendance_frame(self): + """Setup attendance marking frame""" + for widget in self.attendance_frame.winfo_children(): + widget.destroy() + + tk.Label(self.attendance_frame, text="Mark Attendance", font=("Arial", 20)).pack(pady=20) + + tk.Label(self.attendance_frame, text="Employee ID:").pack(pady=10) + self.employee_id_entry = tk.Entry(self.attendance_frame) + self.employee_id_entry.pack(pady=5) + + def mark_attendance(is_markin=True): + emp_id = self.employee_id_entry.get().strip() + success, message = self.attendance_service.mark_attendance(emp_id, is_markin) + + if success: + messagebox.showinfo("Attendance", message) + else: + messagebox.showerror("Error", message) + + self.employee_id_entry.delete(0, tk.END) + + btn_mark_in = tk.Button(self.attendance_frame, text="Mark In", + command=lambda: mark_attendance(is_markin=True)) + btn_mark_in.pack(pady=10) + + btn_mark_out = tk.Button(self.attendance_frame, text="Mark Out", + command=lambda: mark_attendance(is_markin=False)) + btn_mark_out.pack(pady=10) + + btn_back = tk.Button(self.attendance_frame, text="Back to Main Screen", + command=lambda: [self.employee_id_entry.delete(0, tk.END), self.show_frame(self.main_frame)]) + btn_back.pack(pady=10) + + def run(self): + """Start the application""" + self.root.mainloop() + +if __name__ == "__main__": + app = BiometricAttendanceApp() + app.run() \ No newline at end of file From 36bde079138a44c5e11f6fb6087dd79028e142bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Sep 2025 16:17:54 +0000 Subject: [PATCH 4/4] Complete microservices refactoring with documentation and validation Co-authored-by: Ritvik-km <116180586+Ritvik-km@users.noreply.github.com> --- README.md | 122 +++++++++++++++++++++++++++++++++++++++++++++++++ app_display.py | 18 +++++++- 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc92619 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# Biometric Attendance System - Microservices Architecture + +This repository contains a biometric attendance system that has been refactored from a monolithic architecture to a microservices-based approach. + +## Architecture Overview + +The system has been transformed from a single monolithic file (`app_display.py` - 865 lines) into a modular microservices architecture with 5 focused services: + +### Microservices + +1. **`main_app.py`** (408 lines) - Main Application Entry Point + - Orchestrates all services + - Provides the complete GUI interface using Tkinter + - Handles service dependency injection + - Maintains the original user experience + +2. **`database_service.py`** (185 lines) - Database Operations Service + - Database connection and initialization + - Admin management (authentication, CRUD operations) + - Employee management (registration, retrieval) + - Attendance operations (check-in/out, records) + - Department management + +3. **`face_recognition_service.py`** (128 lines) - Face Recognition Service + - Camera operations and image capture + - Face detection and location identification + - Face encoding generation and comparison + - Image processing and storage + - Camera preview window management + +4. **`admin_service.py`** (107 lines) - Admin Management Service + - Admin authentication and password management + - Dashboard data aggregation + - Employee registration workflow + - Department validation + +5. **`attendance_service.py`** (143 lines) - Attendance Operations Service + - Attendance marking (check-in/check-out) + - Face validation for attendance + - Attendance records retrieval and filtering + - Excel export functionality + - Failed attempt logging + +## Usage + +### Running the Application + +To run the new microservices-based application: + +```bash +python3 main_app.py +``` + +### Legacy Version + +The original monolithic version is still available in `app_display.py` for reference, but the new microservices architecture is recommended for all use cases. + +## Benefits of Microservices Architecture + +- **Separation of Concerns**: Each service has a single, well-defined responsibility +- **Maintainability**: Easier to modify and extend individual components +- **Testability**: Services can be tested independently +- **Modularity**: Clear interfaces between services +- **Scalability**: Individual services can be optimized or replaced as needed + +## Dependencies + +The application requires the following Python packages: +- `tkinter` (GUI framework) +- `opencv-python` (cv2 - computer vision operations) +- `face_recognition` (face detection and recognition) +- `mysql-connector-python` (database connectivity) +- `numpy` (numerical operations) +- `PIL/Pillow` (image processing) +- `tkcalendar` (date picker widget) +- `bcrypt` (password hashing) +- `pandas` (data manipulation) +- `matplotlib` (plotting and visualization) +- `python-dotenv` (environment variable management) + +## Environment Setup + +1. Install required dependencies: +```bash +pip install opencv-python face_recognition mysql-connector-python numpy Pillow tkcalendar bcrypt pandas matplotlib python-dotenv +``` + +2. Set up your database configuration in a `.env` file: +``` +host=your_database_host +user=your_database_user +password=your_database_password +database=your_database_name +``` + +3. Run the application: +```bash +python3 main_app.py +``` + +## File Structure + +``` +. +├── main_app.py # Main application entry point +├── database_service.py # Database operations service +├── face_recognition_service.py # Face recognition service +├── admin_service.py # Admin management service +├── attendance_service.py # Attendance operations service +├── app_display.py # Legacy monolithic version +├── .env # Environment configuration +├── .gitignore # Git ignore rules +└── README.md # This file +``` + +## Refactoring Summary + +- **Original**: 1 file, 865 lines, monolithic architecture +- **Refactored**: 5 services, 971 lines total, microservices architecture +- **Increase**: +106 lines (12% increase for much better architecture) + +The slight increase in total lines is due to proper class structures, comprehensive documentation, better error handling, and clear separation of concerns. This is a worthwhile trade-off for the significant improvements in maintainability and modularity. \ No newline at end of file diff --git a/app_display.py b/app_display.py index d4cb1e0..4f72df2 100644 --- a/app_display.py +++ b/app_display.py @@ -1,3 +1,19 @@ +""" +LEGACY MONOLITHIC VERSION - app_display.py +=========================================== +This is the original monolithic implementation of the Biometric Attendance System. + +For the new microservices architecture, please use: +- main_app.py (main application entry point) +- database_service.py (database operations) +- face_recognition_service.py (face recognition logic) +- admin_service.py (admin management) +- attendance_service.py (attendance operations) + +The microservices architecture provides better separation of concerns, +easier maintenance, and more modular code structure. +=========================================== +""" import tkinter as tk import os import sys @@ -17,8 +33,6 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from dotenv import load_dotenv -from refactored_project import database - load_dotenv() host = os.getenv("host")