diff --git a/app.py b/app.py index bc29ced..ab25c49 100644 --- a/app.py +++ b/app.py @@ -326,7 +326,6 @@ def update_existing_qr_codes(): lh.logger.info(f"Updated {updated_count} existing QR codes with missing URLs/images") except Exception as e: lh.log_database_error('update_existing_qr_codes', e) - print(f"Error updating existing QR codes: {e}") def log_slow_query_performance(app_instance): @@ -368,15 +367,15 @@ if __name__ == '__main__': create_tables() log_slow_query_performance(app) - print("๐Ÿš€ Initializing performance optimizations...") from extensions import logger_handler + logger_handler.logger.info("Initializing performance optimizations") cached_query = initialize_performance_optimizations(app, db, logger_handler) performance_monitor = PerformanceMonitor(app, db, logger_handler) if cached_query: - print("โœ… Performance optimizations completed successfully") + logger_handler.logger.info("Performance optimizations completed successfully") else: - print("โš ๏ธ Performance optimizations completed with warnings") + logger_handler.logger.warning("Performance optimizations completed with warnings") logger_handler.logger.info("QR Attendance Management System started successfully") diff --git a/logger_handler.py b/logger_handler.py index a4c2c9b..acf9859 100644 --- a/logger_handler.py +++ b/logger_handler.py @@ -25,7 +25,7 @@ import os import traceback from datetime import datetime, date, timedelta from functools import wraps -from flask import request, session, g, render_template, has_request_context +from flask import request, session, g, render_template, has_request_context, current_app from sqlalchemy import text from sqlalchemy.exc import SQLAlchemyError import uuid @@ -169,7 +169,7 @@ class AppLogger: self.db.session.commit() except Exception as e: - print(f"Warning: Could not create log_events table: {e}") + logging.getLogger('qr_attendance_app').warning(f"Could not create log_events table: {e}") def _register_error_handlers(self): """Register Flask error handlers for automatic logging""" @@ -258,7 +258,7 @@ class AppLogger: except Exception as e: # Don't let logging errors break the application - print(f"Database logging error: {e}") + logging.getLogger('qr_attendance_app').warning(f"Database logging error (non-fatal): {e}") try: self.db.session.rollback() except: @@ -618,10 +618,10 @@ class AppLogger: try: table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone() if not table_check: - print("โš ๏ธ log_events table does not exist") + self.logger.warning("get_log_statistics: log_events table does not exist") return stats except Exception as table_error: - print(f"โš ๏ธ Cannot check if log_events table exists: {table_error}") + self.logger.warning(f"get_log_statistics: cannot check table existence: {table_error}") return stats # Get total events count @@ -635,9 +635,9 @@ class AppLogger: total_result = self.db.session.execute(text(total_sql), {'cutoff_date': cutoff_date}).fetchone() if total_result: stats['total_events'] = total_result.total_events - print(f"โœ… Found {stats['total_events']} total events in last {days} days") + self.logger.debug(f"get_log_statistics: {stats['total_events']} total events in last {days} days") except Exception as total_error: - print(f"โš ๏ธ Error getting total events: {total_error}") + self.logger.warning(f"get_log_statistics: error getting total events: {total_error}") # Get events by category try: @@ -655,7 +655,7 @@ class AppLogger: for row in category_result: category = row.event_category count = row.event_count - print(f"โœ… Found {count} events in category: {category}") + self.logger.debug(f"get_log_statistics: {count} events in category: {category}") # Map categories to stats keys if category == 'security': @@ -672,13 +672,13 @@ class AppLogger: stats['system_events'] = count except Exception as category_error: - print(f"โš ๏ธ Error getting category stats: {category_error}") + self.logger.warning(f"get_log_statistics: error getting category stats: {category_error}") - print(f"๐Ÿ“Š Final stats: {stats}") + self.logger.debug(f"get_log_statistics result: {stats}") return stats except Exception as e: - print(f"โŒ Error in get_log_statistics: {e}") + self.logger.error(f"Error in get_log_statistics: {e}", exc_info=True) self.log_database_error('get_log_statistics', e) return { 'total_events': 0, @@ -695,16 +695,16 @@ class AppLogger: try: from datetime import datetime, timedelta # Import here as backup cutoff_date = datetime.now() - timedelta(days=days_to_keep) - print(f"๐Ÿงน Starting log cleanup: removing entries older than {cutoff_date}") + self.logger.info(f"Starting log cleanup: removing entries older than {cutoff_date}") # Check if table exists first try: table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone() if not table_check: - print("โš ๏ธ log_events table does not exist") + self.logger.warning("cleanup_old_logs: log_events table does not exist") return 0 except Exception as table_error: - print(f"โš ๏ธ Cannot check if log_events table exists: {table_error}") + self.logger.warning(f"cleanup_old_logs: cannot check table existence: {table_error}") return 0 # First, count how many records will be deleted @@ -719,14 +719,14 @@ class AppLogger: count_result = self.db.session.execute(text(count_sql), {'cutoff_date': cutoff_date}).fetchone() count_to_delete = count_result.count_to_delete if count_result else 0 - print(f"๐Ÿ“Š Found {count_to_delete} records to delete") + self.logger.debug(f"cleanup_old_logs: {count_to_delete} records to delete") if count_to_delete == 0: - print("โœ… No old records found to cleanup") + self.logger.info("cleanup_old_logs: no old records found to cleanup") return 0 except Exception as count_error: - print(f"โš ๏ธ Error counting records to delete: {count_error}") + self.logger.warning(f"cleanup_old_logs: error counting records: {count_error}") return 0 # Perform the cleanup - exclude critical logs @@ -741,7 +741,7 @@ class AppLogger: deleted_count = result.rowcount self.db.session.commit() - print(f"๐Ÿ—‘๏ธ Successfully deleted {deleted_count} old log entries") + self.logger.info(f"cleanup_old_logs: deleted {deleted_count} old log entries") # Log the cleanup operation self.logger.info(f"Log cleanup completed: {deleted_count} entries removed (keeping entries newer than {days_to_keep} days)") @@ -749,12 +749,12 @@ class AppLogger: return deleted_count except Exception as delete_error: - print(f"โŒ Error during deletion: {delete_error}") + self.logger.error(f"cleanup_old_logs: error during deletion: {delete_error}", exc_info=True) self.db.session.rollback() return 0 except Exception as e: - print(f"โŒ Error in cleanup_old_logs: {e}") + self.logger.error(f"Error in cleanup_old_logs: {e}", exc_info=True) self.db.session.rollback() self.log_database_error('cleanup_old_logs', e) return 0 @@ -819,7 +819,7 @@ class AppLogger: except Exception as e: self.log_database_error('get_recent_logs', e) - print(f"Error in get_recent_logs: {e}") + self.logger.error(f"Error in get_recent_logs: {e}", exc_info=True) return [] def log_system_event(self, event_type, description, severity='INFO', additional_data=None): @@ -873,15 +873,15 @@ class AppLogger: """ result = self.db.session.execute(text(check_table_sql)).fetchone() if result.table_exists == 0: - print("โš ๏ธ log_events table does not exist. Creating it now...") + self.logger.warning("log_events table does not exist โ€” creating it now") self._create_log_table() return True count_sql = "SELECT COUNT(*) as record_count FROM log_events" count_result = self.db.session.execute(text(count_sql)).fetchone() - print(f"โœ… log_events table exists with {count_result.record_count} records") + self.logger.debug(f"log_events table exists with {count_result.record_count} records") return True except Exception as e: - print(f"โŒ Error verifying log table: {e}") + self.logger.error(f"Error verifying log table: {e}", exc_info=True) return False def log_modal_interaction(self, event_type, description, additional_data=None): @@ -904,66 +904,76 @@ class AppLogger: severity='INFO' ) except Exception as e: - print(f"Error logging modal interaction: {e}") + self.logger.warning(f"Error logging modal interaction: {e}") # DECORATOR FUNCTIONS FOR AUTOMATIC LOGGING def log_user_activity(activity_type): - """Decorator to automatically log user activities""" + """Decorator to automatically log user activities to file and database.""" def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): try: result = f(*args, **kwargs) - - # Log successful activity - if hasattr(g, 'app') and hasattr(g.app, 'logger_handler'): - logger = g.app.logger_handler - logger.logger.info(json.dumps({ - 'event': f'user_activity_{activity_type}', - 'data': { - 'user_id': session.get('user_id'), - 'username': session.get('username'), - 'activity': activity_type, - 'timestamp': datetime.now().isoformat() - } - })) - + + # Log successful activity via the AppLogger instance on current_app + try: + lh = current_app.logger_handler + lh.log_user_activity( + activity_type=activity_type, + description=( + f"User '{session.get('username', 'anonymous')}' " + f"completed activity: {activity_type}" + ) + ) + except Exception as log_error: + # Logging must never break the decorated route + logging.getLogger('qr_attendance_app').warning( + f"log_user_activity decorator failed for '{activity_type}': {log_error}" + ) + return result - + except Exception as e: - # Log error - if hasattr(g, 'app') and hasattr(g.app, 'logger_handler'): - logger = g.app.logger_handler - logger.log_flask_error( + # Log the error, then re-raise so Flask handles it normally + try: + lh = current_app.logger_handler + lh.log_flask_error( error_type=f"activity_error_{activity_type}", error_message=str(e), stack_trace=traceback.format_exc() ) + except Exception as log_error: + logging.getLogger('qr_attendance_app').warning( + f"log_user_activity error-branch failed for '{activity_type}': {log_error}" + ) raise - + return decorated_function return decorator def log_database_operations(operation_name): - """Decorator to automatically log database operations""" + """Decorator to automatically log database operation errors.""" def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): try: - result = f(*args, **kwargs) - return result - + return f(*args, **kwargs) + except Exception as e: - # Log database error - if hasattr(g, 'app') and hasattr(g.app, 'logger_handler'): - logger = g.app.logger_handler - logger.log_database_error( + # Log the database error via the AppLogger instance on current_app + try: + lh = current_app.logger_handler + lh.log_database_error( operation=operation_name, error=e ) + except Exception as log_error: + logging.getLogger('qr_attendance_app').warning( + f"log_database_operations decorator failed for '{operation_name}': {log_error}" + ) raise - + return decorated_function return decorator diff --git a/routes/admin.py b/routes/admin.py index 5049aef..3298feb 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -77,8 +77,10 @@ def api_recent_logs(): severity = request.args.get('severity', '') search = request.args.get('search', '') - print(f"๐Ÿ“Š API request - Days: {days}, Limit: {limit}, Page: {page}") - print(f"๐Ÿ“Š Filters - Category: {category}, Severity: {severity}, Search: {search}") + logger_handler.logger.debug( + f"api_recent_logs: days={days}, limit={limit}, page={page}, " + f"category={category!r}, severity={severity!r}, search={search!r}" + ) cutoff_date = datetime.now() - timedelta(days=days) @@ -164,7 +166,7 @@ def api_recent_logs(): 'ip_address': row.ip_address or '-' }) - print(f"๐Ÿ“Š Returning {len(logs)} logs out of {total_count} total") + logger_handler.logger.debug(f"api_recent_logs: returning {len(logs)} of {total_count} total records") return jsonify({ 'success': True, @@ -179,7 +181,6 @@ def api_recent_logs(): except Exception as e: logger_handler.log_database_error('api_recent_logs', e) - print(f"Error in api_recent_logs: {e}") return jsonify({ 'success': False, 'error': f'Failed to fetch recent logs: {str(e)}' @@ -191,11 +192,11 @@ def api_log_stats(): """API endpoint to get logging statistics""" try: days = request.args.get('days', 7, type=int) - print(f"๐Ÿ“Š Getting log statistics for last {days} days") + logger_handler.logger.debug(f"api_log_stats: fetching statistics for last {days} days") # Get statistics from logger handler stats = logger_handler.get_log_statistics(days=days) - print(f"๐Ÿ“ˆ Retrieved stats: {stats}") + # Ensure all expected keys exist with updated categories expected_stats = { @@ -217,7 +218,6 @@ def api_log_stats(): except Exception as e: logger_handler.log_database_error('api_log_stats', e) - print(f"โŒ Error in api_log_stats: {e}") return jsonify({ 'success': False, 'error': f'Failed to fetch log statistics: {str(e)}', @@ -240,25 +240,21 @@ def api_cleanup_logs(): # Get JSON data data = request.get_json() if not data: - print("โŒ No JSON data provided") return jsonify({ 'success': False, 'error': 'No JSON data provided' }), 400 days_to_keep = data.get('days_to_keep', 90) - print(f"๐Ÿงน Cleanup request: keep last {days_to_keep} days") # Validate input if not isinstance(days_to_keep, int) or days_to_keep < 7: - print(f"โŒ Invalid days_to_keep: {days_to_keep}") return jsonify({ 'success': False, 'error': 'days_to_keep must be an integer >= 7' }), 400 if days_to_keep > 365: - print(f"โŒ days_to_keep too large: {days_to_keep}") return jsonify({ 'success': False, 'error': 'days_to_keep cannot exceed 365 days' @@ -268,7 +264,10 @@ def api_cleanup_logs(): deleted_count = logger_handler.cleanup_old_logs(days_to_keep=days_to_keep) admin_username = session.get('username', 'unknown') - print(f"โœ… Cleanup completed by {admin_username}: {deleted_count} records deleted") + logger_handler.logger.info( + f"Admin {admin_username} performed log cleanup: {deleted_count} records deleted " + f"(keeping last {days_to_keep} days)" + ) # Log the admin action logger_handler.log_security_event( @@ -294,7 +293,6 @@ def api_cleanup_logs(): except Exception as e: logger_handler.log_database_error('api_cleanup_logs', e) - print(f"โŒ Error in api_cleanup_logs: {e}") return jsonify({ 'success': False, 'error': f'Failed to cleanup old logs: {str(e)}' @@ -306,7 +304,7 @@ def api_clear_logs(): """API endpoint to clear ALL log entries""" try: admin_username = session.get('username', 'unknown') - print(f"๐Ÿงน Clear logs request by admin: {admin_username}") + logger_handler.logger.info(f"Admin {admin_username} initiated full log clear") # Count existing logs before deletion try: @@ -314,10 +312,7 @@ def api_clear_logs(): count_result = db.session.execute(text(count_sql)).fetchone() total_logs = count_result.total_logs if count_result else 0 - print(f"๐Ÿ“Š Total logs to be cleared: {total_logs}") - if total_logs == 0: - print("โœ… No logs found to clear") return jsonify({ 'success': True, 'deleted_count': 0, @@ -325,7 +320,7 @@ def api_clear_logs(): }) except Exception as count_error: - print(f"โš ๏ธ Error counting logs: {count_error}") + logger_handler.logger.warning(f"Error counting logs before clear: {count_error}") total_logs = 0 # Perform the clear operation @@ -335,7 +330,9 @@ def api_clear_logs(): deleted_count = result.rowcount db.session.commit() - print(f"๐Ÿ—‘๏ธ Successfully cleared {deleted_count} log entries") + logger_handler.logger.info( + f"Admin {admin_username} cleared all log entries: {deleted_count} records deleted" + ) # Log the clear operation (this will be the first entry in the new log) logger_handler.log_security_event( @@ -358,7 +355,7 @@ def api_clear_logs(): }) except Exception as delete_error: - print(f"โŒ Error during log clearing: {delete_error}") + logger_handler.log_database_error('api_clear_logs_delete', delete_error) db.session.rollback() return jsonify({ 'success': False, @@ -367,7 +364,6 @@ def api_clear_logs(): except Exception as e: logger_handler.log_database_error('api_clear_logs', e) - print(f"โŒ Error in api_clear_logs: {e}") return jsonify({ 'success': False, 'error': f'Failed to clear logs: {str(e)}' @@ -381,7 +377,6 @@ def api_clear_old_logs(): # Get JSON data data = request.get_json() if not data: - print("โŒ No JSON data provided") return jsonify({ 'success': False, 'error': 'No JSON data provided' @@ -389,11 +384,9 @@ def api_clear_old_logs(): days_threshold = data.get('days_threshold', 90) admin_username = session.get('username', 'unknown') - print(f"๐Ÿงน Clear old logs request by admin: {admin_username}, threshold: {days_threshold} days") # Validate input if not isinstance(days_threshold, int) or days_threshold not in [30, 60, 90]: - print(f"โŒ Invalid days_threshold: {days_threshold}") return jsonify({ 'success': False, 'error': 'days_threshold must be 30, 60, or 90' @@ -408,10 +401,7 @@ def api_clear_old_logs(): count_result = db.session.execute(text(count_sql), {'cutoff_date': cutoff_date}).fetchone() total_logs = count_result.total_logs if count_result else 0 - print(f"๐Ÿ“Š Total logs older than {days_threshold} days to be cleared: {total_logs}") - if total_logs == 0: - print("โœ… No old logs found to clear") return jsonify({ 'success': True, 'deleted_count': 0, @@ -419,7 +409,7 @@ def api_clear_old_logs(): }) except Exception as count_error: - print(f"โš ๏ธ Error counting old logs: {count_error}") + logger_handler.logger.warning(f"Error counting old logs before clear: {count_error}") total_logs = 0 # Perform the clear operation @@ -429,7 +419,9 @@ def api_clear_old_logs(): deleted_count = result.rowcount db.session.commit() - print(f"๐Ÿ—‘๏ธ Successfully cleared {deleted_count} log entries older than {days_threshold} days") + logger_handler.logger.info( + f"Admin {admin_username} cleared {deleted_count} log entries older than {days_threshold} days" + ) # Log the clear operation logger_handler.log_security_event( @@ -455,7 +447,7 @@ def api_clear_old_logs(): }) except Exception as delete_error: - print(f"โŒ Error during old log clearing: {delete_error}") + logger_handler.log_database_error('api_clear_old_logs_delete', delete_error) db.session.rollback() return jsonify({ 'success': False, @@ -464,7 +456,6 @@ def api_clear_old_logs(): except Exception as e: logger_handler.log_database_error('api_clear_old_logs', e) - print(f"โŒ Error in api_clear_old_logs: {e}") return jsonify({ 'success': False, 'error': f'Failed to clear old logs: {str(e)}' @@ -481,7 +472,10 @@ def api_export_logs(): search = request.args.get('search', '') admin_username = session.get('username', 'unknown') - print(f"๐Ÿ“Š Export logs request by admin: {admin_username}") + logger_handler.logger.info( + f"Admin {admin_username} initiated log export: last {days} days, " + f"category={category!r}, severity={severity!r}" + ) cutoff_date = datetime.now() - timedelta(days=days) @@ -578,7 +572,6 @@ def api_export_logs(): except Exception as e: logger_handler.log_database_error('api_export_logs', e) - print(f"โŒ Error in api_export_logs: {e}") return jsonify({ 'success': False, 'error': f'Failed to export logs: {str(e)}' diff --git a/routes/attendance.py b/routes/attendance.py index c0147d3..1531c86 100644 --- a/routes/attendance.py +++ b/routes/attendance.py @@ -44,18 +44,18 @@ bp = Blueprint('attendance', __name__) def attendance_report(): """Safe attendance report with backward compatibility for location_accuracy and fixed datetime handling""" try: - print("๐Ÿ“Š Loading attendance report...") + logger_handler.logger.debug("Loading attendance report") # Log attendance report access try: user_role = session.get('role', 'unknown') logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed attendance report") - except Exception as log_error: - print(f"โš ๏ธ Logging error (non-critical): {log_error}") + except Exception: + pass # Check if location_accuracy column exists has_location_accuracy = check_location_accuracy_column_exists() - print(f"๐Ÿ” Location accuracy column exists: {has_location_accuracy}") + logger_handler.logger.debug(f"Location accuracy column exists: {has_location_accuracy}") # Get filter parameters date_from = request.args.get('date_from', '') @@ -98,7 +98,7 @@ def attendance_report(): # Check if user is Project Manager and get their permissions if user_role == 'project_manager': - print(f"๐Ÿ”’ Project Manager access control enabled for user {session.get('username')}") + logger_handler.logger.debug(f"Project Manager access control enabled for user {session.get('username')}") try: # Get assigned projects @@ -115,10 +115,9 @@ def attendance_report(): f"Projects: {allowed_project_ids}, Locations: {allowed_location_names}" ) - print(f"๐Ÿ”’ Allowed projects: {allowed_project_ids}") - print(f"๐Ÿ”’ Allowed locations: {allowed_location_names}") + logger_handler.logger.debug(f"PM allowed projects: {allowed_project_ids}, locations: {allowed_location_names}") except Exception as perm_error: - print(f"โš ๏ธ Error loading permissions: {perm_error}") + logger_handler.logger.warning(f"Error loading PM permissions: {perm_error}") logger_handler.logger.error(f"Error loading Project Manager permissions: {perm_error}") # If no permissions assigned, user cannot view anything @@ -278,12 +277,12 @@ def attendance_report(): base_query += " ORDER BY ad.check_in_date DESC, ad.check_in_time DESC LIMIT 1000" - print(f"๐Ÿ” Executing attendance query with filters: {list(query_params.keys())}") + logger_handler.logger.debug(f"Executing attendance query with filters: {list(query_params.keys())}") # Execute query result = db.session.execute(text(base_query), query_params) records = result.fetchall() - print(f"โœ… Loaded {len(records)} attendance records") + logger_handler.logger.debug(f"Loaded {len(records)} attendance records") # Process records processed_records = [] @@ -322,7 +321,7 @@ def attendance_report(): record_dict['accuracy_level'] = 'unknown' processed_records.append(record_dict) except Exception as rec_error: - print(f"โš ๏ธ Error processing record: {rec_error}") + logger_handler.logger.warning(f"Error processing attendance record: {rec_error}") continue # Get unique locations for filter dropdown @@ -333,7 +332,7 @@ def attendance_report(): if user_role == 'project_manager' and allowed_location_names: # Only show locations the PM has access to locations = sorted(allowed_location_names) - print(f"โœ… Filtered to {len(locations)} locations for Project Manager") + logger_handler.logger.debug(f"Filtered to {len(locations)} locations for Project Manager") else: # Show all locations for Admin/Staff/Payroll locations_query = db.session.execute(text(""" @@ -343,12 +342,12 @@ def attendance_report(): ORDER BY location_name """)) locations = [row[0] for row in locations_query.fetchall()] - print(f"โœ… Found {len(locations)} unique locations") + logger_handler.logger.debug(f"Found {len(locations)} unique locations") # ============================================================ # END: FILTER LOCATIONS FOR PROJECT MANAGER # ============================================================ except Exception as e: - print(f"โš ๏ธ Error loading locations: {e}") + logger_handler.logger.warning(f"Error loading locations filter: {e}") locations = [] # Get projects for filter dropdown @@ -369,7 +368,7 @@ def attendance_report(): ORDER BY p.name """)) projects = projects_query.fetchall() - print(f"โœ… Filtered to {len(projects)} projects for Project Manager") + logger_handler.logger.debug(f"Filtered to {len(projects)} projects for Project Manager") else: # Show all projects for Admin/Staff/Payroll projects = db.session.execute(text(""" @@ -382,18 +381,18 @@ def attendance_report(): HAVING COUNT(DISTINCT ad.id) > 0 ORDER BY p.name """)).fetchall() - print(f"โœ… Loaded {len(projects)} projects with attendance data") + logger_handler.logger.debug(f"Loaded {len(projects)} projects with attendance data") # ============================================================ # END: FILTER PROJECTS FOR PROJECT MANAGER # ============================================================ except Exception as e: - print(f"โš ๏ธ Error loading projects: {e}") + logger_handler.logger.warning(f"Error loading projects filter: {e}") projects = [] # ============================================================ # STATISTICS - COMPLETELY REWRITTEN FOR SAFETY # ============================================================ - print("๐Ÿ“Š Loading statistics...") + logger_handler.logger.debug("Loading attendance statistics") # Create simple dict for stats (most compatible approach) stats_dict = { @@ -455,15 +454,13 @@ def attendance_report(): if stats_conditions: stats_query_text += " AND " + " AND ".join(stats_conditions) - print(f"๐Ÿ“Š Executing stats query...") - print(f"๐Ÿ“Š Stats params: {list(stats_params.keys())}") + logger_handler.logger.debug(f"Executing stats query with params: {list(stats_params.keys())}") # Execute stats query stats_result = db.session.execute(text(stats_query_text), stats_params) stats_row = stats_result.fetchone() - print(f"๐Ÿ“Š Stats row type: {type(stats_row)}") - print(f"๐Ÿ“Š Stats row value: {stats_row}") + logger_handler.logger.debug(f"Stats row type: {type(stats_row).__name__}") # Safely extract stats from row if stats_row is not None and len(stats_row) >= 7: @@ -475,17 +472,15 @@ def attendance_report(): stats_dict['records_with_gps'] = int(stats_row[4]) if stats_row[4] is not None else 0 stats_dict['records_with_accuracy'] = int(stats_row[5]) if stats_row[5] is not None else 0 stats_dict['avg_location_accuracy'] = float(stats_row[6]) if stats_row[6] is not None else 0.0 - print(f"โœ… Loaded statistics: {stats_dict['total_checkins']} total check-ins") + logger_handler.logger.debug(f"Loaded statistics: {stats_dict['total_checkins']} total check-ins") except (IndexError, TypeError, ValueError) as extract_error: - print(f"โš ๏ธ Error extracting stats values: {extract_error}") + logger_handler.logger.warning(f"Error extracting stats values: {extract_error}") # stats_dict already has default values else: - print("โš ๏ธ Stats query returned None or insufficient columns, using default stats") + logger_handler.logger.warning("Stats query returned None or insufficient columns, using default stats") except Exception as stats_error: - print(f"โŒ Error loading statistics: {stats_error}") - import traceback - print(f"โŒ Stats error traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error loading statistics: {stats_error}", exc_info=True) # stats_dict already has default values # Convert dict to object-like for template compatibility @@ -495,7 +490,7 @@ def attendance_report(): setattr(self, key, value) stats = StatsObject(stats_dict) - print(f"โœ… Stats object created: total_checkins={stats.total_checkins}") + logger_handler.logger.debug(f"Stats object created: total_checkins={stats.total_checkins}") # ============================================================ # END: STATISTICS @@ -505,8 +500,7 @@ def attendance_report(): today_date = datetime.now().strftime('%Y-%m-%d') current_date_formatted = datetime.now().strftime('%B %d') - print("โœ… Rendering attendance report template") - print(f"โœ… Stats object: {stats}") + logger_handler.logger.debug("Rendering attendance report template") return render_template('attendance_report.html', attendance_records=processed_records, @@ -527,18 +521,17 @@ def attendance_report(): user_role=user_role) except Exception as e: - print(f"โŒ Error loading attendance report: {e}") - print(f"โŒ Exception type: {type(e)}") + logger_handler.logger.error(f"Error loading attendance report: {e}", exc_info=True) import traceback error_traceback = traceback.format_exc() - print(f"โŒ Traceback: {error_traceback}") + # Log the error try: logger_handler.log_database_error('attendance_report', e) except Exception as log_error: - print(f"โš ๏ธ Additional logging error: {log_error}") + logger_handler.logger.warning(f"Additional logging error: {log_error}") flash('Error loading attendance report. Please check the server logs for details.', 'error') return redirect(url_for('dashboard.dashboard')) @@ -595,7 +588,7 @@ def edit_attendance(record_id): qr_codes=QRCode.query.filter_by(active_status=True).all()) # Validate the QR code exists - new_qr_code = QRCode.query.get(int(new_qr_code_id)) + new_qr_code = db.session.get(QRCode, int(new_qr_code_id)) if not new_qr_code: flash('Selected location event not found.', 'error') projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() @@ -657,8 +650,10 @@ def edit_attendance(record_id): 'editor_username': session.get('username') } ) - print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} updated attendance record {record_id}: {changes}") - print(f"[LOG] Edit reason: {edit_note}") + logger_handler.logger.info( + f"User {session.get('username')} ({session.get('role', 'unknown')}) " + f"updated attendance record {record_id}: {changes}, reason: {edit_note}" + ) else: # Log even if no changes were made (for audit purposes) logger_handler.log_security_event( @@ -672,8 +667,10 @@ def edit_attendance(record_id): 'editor_username': session.get('username') } ) - print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} edited record {record_id} with no changes") - print(f"[LOG] Edit reason: {edit_note}") + logger_handler.logger.info( + f"User {session.get('username')} ({session.get('role', 'unknown')}) " + f"edited attendance record {record_id} with no changes, reason: {edit_note}" + ) flash(f'Attendance record for {new_employee_id} updated successfully! Edit reason logged for audit.', 'success') return redirect(url_for('attendance.attendance_report')) @@ -693,7 +690,7 @@ def edit_attendance(record_id): except Exception as e: db.session.rollback() logger_handler.log_database_error('attendance_update', e) - print(f"[LOG] Error updating attendance record {record_id}: {e}") + logger_handler.logger.error(f"Error updating attendance record {record_id}: {e}", exc_info=True) flash('Error updating attendance record. Please try again.', 'error') return redirect(url_for('attendance.attendance_report')) @@ -770,7 +767,7 @@ def save_manual_attendance(): return redirect(url_for('attendance.add_manual_attendance')) # Get QR code (location) - qr_code = QRCode.query.get(int(location_id)) + qr_code = db.session.get(QRCode, int(location_id)) if not qr_code: flash('Selected location not found.', 'error') return redirect(url_for('attendance.add_manual_attendance')) @@ -1093,7 +1090,10 @@ def delete_attendance(record_id): db.session.delete(attendance_record) db.session.commit() - print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} deleted attendance record {record_id} for employee {employee_id}") + logger_handler.logger.info( + f"User {session.get('username')} ({session.get('role', 'unknown')}) " + f"deleted attendance record {record_id} for employee {employee_id}" + ) # Return JSON response for AJAX requests if request.headers.get('X-Requested-With') == 'XMLHttpRequest': @@ -1108,7 +1108,7 @@ def delete_attendance(record_id): except Exception as e: db.session.rollback() logger_handler.log_database_error('attendance_delete', e) - print(f"[LOG] Error deleting attendance record {record_id}: {e}") + logger_handler.logger.error(f"Error deleting attendance record {record_id}: {e}", exc_info=True) if request.headers.get('X-Requested-With') == 'XMLHttpRequest': return jsonify({ @@ -1191,7 +1191,7 @@ def verification_review(): project_id = record.qr_code.project_id if project_id not in project_names: try: - project = Project.query.get(project_id) + project = db.session.get(Project, project_id) if project: project_names[project_id] = project.name else: @@ -1307,16 +1307,11 @@ def get_verification_details(record_id): record = AttendanceData.query.get_or_404(record_id) # DEBUG: Log record details - print(f"=== VERIFICATION DETAILS DEBUG ===") - print(f"Record ID: {record.id}") - print(f"Employee: {record.employee_id}") - print(f"check_in_date type: {type(record.check_in_date)}") - print(f"check_in_date value: {record.check_in_date}") - print(f"check_in_time type: {type(record.check_in_time)}") - print(f"check_in_time value: {record.check_in_time}") - print(f"verification_photo exists: {record.verification_photo is not None}") - print(f"verification_status: {record.verification_status}") - print(f"==================================") + logger_handler.logger.debug( + f"Verification details: record={record.id}, employee={record.employee_id}, " + f"date={record.check_in_date}, time={record.check_in_time}, " + f"has_photo={record.verification_photo is not None}, status={record.verification_status}" + ) # Check if user has permission to view # Allow admin and payroll staff to view verification details @@ -1333,13 +1328,13 @@ def get_verification_details(record_id): try: check_in_date_str = record.check_in_date.strftime('%Y-%m-%d') if record.check_in_date else 'N/A' except Exception as e: - print(f"Error formatting check_in_date: {e}") + logger_handler.logger.warning(f"Error formatting check_in_date for record {record_id}: {e}") check_in_date_str = str(record.check_in_date) if record.check_in_date else 'N/A' try: check_in_time_str = record.check_in_time.strftime('%I:%M %p') if record.check_in_time else 'N/A' except Exception as e: - print(f"Error formatting check_in_time: {e}") + logger_handler.logger.warning(f"Error formatting check_in_time for record {record_id}: {e}") check_in_time_str = str(record.check_in_time) if record.check_in_time else 'N/A' # Prepare record data with safe formatting @@ -1373,11 +1368,8 @@ def get_verification_details(record_id): }) except Exception as e: - logger_handler.logger.error(f"Error getting verification details for record {record_id}: {e}") - print(f"โŒ Error in get_verification_details for record {record_id}: {e}") - import traceback - print(f"โŒ Traceback: {traceback.format_exc()}") - + logger_handler.logger.error(f"Error in get_verification_details for record {record_id}: {e}", exc_info=True) + return jsonify({ 'success': False, 'message': 'Error loading verification details' @@ -1402,7 +1394,7 @@ def verification_review_detail(record_id): return redirect(url_for('attendance.attendance_report')) # Get the QR code information for additional context - qr_code = QRCode.query.get(record.qr_code_id) if record.qr_code_id else None + qr_code = db.session.get(QRCode, record.qr_code_id) if record.qr_code_id else None # Get employee name from Employee table employee_name = None @@ -1497,7 +1489,7 @@ def attendance_stats_api(): }) except Exception as e: - print(f"Error fetching attendance stats: {e}") + logger_handler.logger.error(f"Error fetching attendance stats: {e}", exc_info=True) return jsonify({'error': 'Failed to fetch attendance statistics'}), 500 @bp.route('/export-configuration', endpoint='export_configuration') @@ -1515,8 +1507,8 @@ def export_configuration(): try: logger_handler.logger.info(f"User {session.get('username', 'unknown')} (role: {user_role}) accessed export configuration") logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed export configuration page") - except Exception as log_error: - print(f"โš ๏ธ Logging error (non-critical): {log_error}") + except Exception: + pass # Get current filters from session or request args filters = { @@ -1527,24 +1519,24 @@ def export_configuration(): 'project_filter': request.args.get('project', '') } - print(f"๐Ÿ“Š Filters: {filters}") + logger_handler.logger.debug(f"Export config filters: {filters}") # Get project name if project filter is applied project_name = None if filters.get('project_filter'): try: - project = Project.query.get(int(filters['project_filter'])) + project = db.session.get(Project, int(filters['project_filter'])) if project: project_name = project.name - print(f"๐Ÿ“Š Project filter: ID={filters['project_filter']}, Name={project_name}") + logger_handler.logger.debug(f"Project filter: ID={filters['project_filter']}, Name={project_name}") except Exception as e: - print(f"โš ๏ธ Error fetching project name: {e}") + logger_handler.logger.warning(f"Error fetching project name for filter: {e}") # Check if location accuracy feature exists try: has_location_accuracy = check_location_accuracy_column_exists() except Exception as e: - print(f"โš ๏ธ Error checking location accuracy column: {e}") + logger_handler.logger.warning(f"Error checking location accuracy column: {e}") has_location_accuracy = False # Define all available columns with their default settings @@ -1574,7 +1566,7 @@ def export_configuration(): 'enabled': True # Changed from False to True }) - print(f"๐Ÿ“Š Rendering export configuration with {len(available_columns)} columns") + logger_handler.logger.debug(f"Rendering export configuration with {len(available_columns)} columns") return render_template('export_configuration.html', available_columns=available_columns, @@ -1583,8 +1575,7 @@ def export_configuration(): has_location_accuracy_feature=has_location_accuracy) except Exception as e: - print(f"โŒ Error in export_configuration route: {e}") - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error in export_configuration route: {e}", exc_info=True) # Use your existing logger error method with correct parameters try: @@ -1594,7 +1585,7 @@ def export_configuration(): stack_trace=traceback.format_exc() ) except Exception as log_error: - print(f"โš ๏ธ Could not log error: {log_error}") + logger_handler.logger.warning(f"Could not log error: {log_error}") flash('Error loading export configuration page.', 'error') return redirect(url_for('attendance.attendance_report')) @@ -1610,17 +1601,17 @@ def generate_excel_export(): flash('Access denied. Only administrators and payroll staff can export data.', 'error') return redirect(url_for('attendance.attendance_report')) - print("๐Ÿ“Š Excel export generation started") + logger_handler.logger.info(f"Excel export started by user {session.get('username', 'unknown')}") # Log export action using your existing logger try: logger_handler.logger.info(f"User {session.get('username', 'unknown')} generated Excel export") - except Exception as log_error: - print(f"โš ๏ธ Logging error (non-critical): {log_error}") + except Exception: + pass # Get selected columns and custom names from form selected_columns_raw = request.form.getlist('selected_columns') - print(f"๐Ÿ“Š Selected columns (raw): {selected_columns_raw}") + logger_handler.logger.debug(f"Selected columns (raw): {selected_columns_raw}") # Get column order from form column_order_json = request.form.get('column_order', '[]') @@ -1629,7 +1620,7 @@ def generate_excel_export(): except (json.JSONDecodeError, TypeError): column_order = [] - print(f"๐Ÿ“Š Column order from form: {column_order}") + logger_handler.logger.debug(f"Column order from form: {column_order}") # Determine final column order if column_order: @@ -1643,7 +1634,7 @@ def generate_excel_export(): # Fallback to raw selection order selected_columns = selected_columns_raw - print(f"๐Ÿ“Š Final column order: {selected_columns}") + logger_handler.logger.debug(f"Final column order: {selected_columns}") if not selected_columns: flash('Please select at least one column to export.', 'error') @@ -1662,8 +1653,7 @@ def generate_excel_export(): 'project_filter': request.form.get('project_filter') } - print(f"๐Ÿ“Š Export filters: {filters}") - print(f"๐Ÿ“Š Column names: {column_names}") + logger_handler.logger.debug(f"Export filters: {filters}") # Save user preferences in session for next time session['export_preferences'] = { @@ -1680,13 +1670,13 @@ def generate_excel_export(): project_name_for_filename = '' if filters.get('project_filter'): try: - project = Project.query.get(int(filters['project_filter'])) + project = db.session.get(Project, int(filters['project_filter'])) if project: # Replace spaces and special characters with underscores project_name_safe = project.name.replace(' ', '_').replace('/', '_').replace('\\', '_') project_name_for_filename = f"{project_name_safe}_" except Exception as e: - print(f"โš ๏ธ Error getting project name for filename: {e}") + logger_handler.logger.warning(f"Error getting project name for filename: {e}") # Format dates for filename (MMDDYYYY format) date_from_formatted = '' @@ -1717,14 +1707,13 @@ def generate_excel_export(): filename = f'{project_name_for_filename}attendance_report_{date_range_str}.xlsx' - print(f"๐Ÿ“Š Excel file generated successfully: {filename}") - print(f"๐Ÿ“Š Column order in export: {selected_columns}") + logger_handler.logger.info(f"Excel export generated successfully: {filename}") # Log successful export using your existing logger try: logger_handler.logger.info(f"Excel export generated successfully with {len(selected_columns)} columns in custom order by user {session.get('username', 'unknown')}: {filename}") - except Exception as log_error: - print(f"โš ๏ธ Logging error (non-critical): {log_error}") + except Exception: + pass return send_file( excel_file, @@ -1737,8 +1726,7 @@ def generate_excel_export(): return redirect(url_for('attendance.export_configuration')) except Exception as e: - print(f"โŒ Error in generate_excel_export route: {e}") - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error in generate_excel_export route: {e}", exc_info=True) # Use your existing logger error method with correct parameters try: @@ -1748,7 +1736,7 @@ def generate_excel_export(): stack_trace=traceback.format_exc() ) except Exception as log_error: - print(f"โš ๏ธ Could not log error: {log_error}") + logger_handler.logger.warning(f"Could not log error: {log_error}") flash('Error generating Excel export.', 'error') return redirect(url_for('attendance.export_configuration')) @@ -1756,7 +1744,7 @@ def generate_excel_export(): def create_excel_export(selected_columns, column_names, filters): """Create Excel file with selected attendance data - Updated to include employee names""" try: - print(f"๐Ÿ“Š Creating Excel export with {len(selected_columns)} columns") + logger_handler.logger.info(f"Creating Excel export with {len(selected_columns)} columns") # Import openpyxl modules try: @@ -1764,8 +1752,7 @@ def create_excel_export(selected_columns, column_names, filters): from openpyxl.styles import Font, Alignment, PatternFill from openpyxl.utils import get_column_letter except ImportError as e: - print(f"โŒ openpyxl import error: {e}") - print("๐Ÿ’ก Install openpyxl: pip install openpyxl") + logger_handler.logger.error(f"openpyxl import error: {e}. Run: pip install openpyxl") return None # Build query based on filters - JOIN with QRCode to get location_event and location_address @@ -1781,22 +1768,22 @@ def create_excel_export(selected_columns, column_names, filters): try: date_from = datetime.strptime(filters['date_from'], '%Y-%m-%d').date() query = query.filter(AttendanceData.check_in_date >= date_from) - print(f"๐Ÿ“Š Applied date_from filter: {date_from}") + logger_handler.logger.debug(f"Applied date_from filter: {date_from}") except ValueError as e: - print(f"โš ๏ธ Invalid date_from format: {e}") + logger_handler.logger.warning(f"Invalid date_from format: {e}") if filters.get('date_to'): try: date_to = datetime.strptime(filters['date_to'], '%Y-%m-%d').date() query = query.filter(AttendanceData.check_in_date <= date_to) - print(f"๐Ÿ“Š Applied date_to filter: {date_to}") + logger_handler.logger.debug(f"Applied date_to filter: {date_to}") except ValueError as e: - print(f"โš ๏ธ Invalid date_to format: {e}") + logger_handler.logger.warning(f"Invalid date_to format: {e}") # Apply location filter if filters.get('location_filter'): query = query.filter(AttendanceData.location_name.like(f"%{filters['location_filter']}%")) - print(f"๐Ÿ“Š Applied location filter: {filters['location_filter']}") + logger_handler.logger.debug(f"Applied location filter: {filters['location_filter']}") # Apply employee filter โ€” supports comma-separated multi-employee values if filters.get('employee_filter'): @@ -1805,26 +1792,26 @@ def create_excel_export(selected_columns, column_names, filters): query = query.filter(AttendanceData.employee_id == emp_ids[0]) elif len(emp_ids) > 1: query = query.filter(AttendanceData.employee_id.in_(emp_ids)) - print(f"๐Ÿ“Š Applied employee filter: {emp_ids}") + logger_handler.logger.debug(f"Applied employee filter: {emp_ids}") # Apply project filter if filters.get('project_filter'): try: project_id = int(filters['project_filter']) query = query.filter(QRCode.project_id == project_id) - print(f"๐Ÿ“Š Applied project filter: {project_id}") + logger_handler.logger.debug(f"Applied project filter: {project_id}") except (ValueError, TypeError) as e: - print(f"โš ๏ธ Invalid project filter: {e}") + logger_handler.logger.warning(f"Invalid project filter: {e}") # Order by date and time query = query.order_by(AttendanceData.check_in_date.desc(), AttendanceData.check_in_time.desc()) # Execute query results = query.all() - print(f"๐Ÿ“Š Query returned {len(results)} records") + logger_handler.logger.debug(f"Query returned {len(results)} records for export") if not results: - print("โš ๏ธ No records found for export") + logger_handler.logger.warning("No records found for export") return None # Create workbook @@ -1890,10 +1877,10 @@ def create_excel_export(selected_columns, column_names, filters): lng_formatted = f"{float(qr_record.address_longitude):.10f}" hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")' cell.value = hyperlink_formula - print(f"๐Ÿ“ Added QR address hyperlink for employee {attendance_record.employee_id}") + logger_handler.logger.debug(f"Added QR address hyperlink for employee {attendance_record.employee_id}") else: cell.value = address_text - print(f"๐Ÿ“ Using QR address for employee {attendance_record.employee_id} (accuracy: {accuracy_value:.3f} miles)") + logger_handler.logger.debug(f"Using QR address for employee {attendance_record.employee_id} (accuracy: {accuracy_value:.3f} miles)") else: # Lower accuracy - use actual check-in address with hyperlink address_text = attendance_record.address or '' @@ -1903,10 +1890,10 @@ def create_excel_export(selected_columns, column_names, filters): lng_formatted = f"{float(attendance_record.longitude):.10f}" hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")' cell.value = hyperlink_formula - print(f"๐Ÿ“ Added check-in address hyperlink for employee {attendance_record.employee_id}") + logger_handler.logger.debug(f"Added check-in address hyperlink for employee {attendance_record.employee_id}") else: cell.value = address_text - print(f"๐Ÿ“ Using check-in address for employee {attendance_record.employee_id} (accuracy: {accuracy_value:.3f} miles)") + logger_handler.logger.debug(f"Using check-in address for employee {attendance_record.employee_id} (accuracy: {accuracy_value:.3f} miles)") except (ValueError, TypeError): # If accuracy can't be converted to float, use check-in address with hyperlink address_text = attendance_record.address or '' @@ -1916,7 +1903,7 @@ def create_excel_export(selected_columns, column_names, filters): lng_formatted = f"{float(attendance_record.longitude):.10f}" hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")' cell.value = hyperlink_formula - print(f"๐Ÿ“ Added check-in address hyperlink for employee {attendance_record.employee_id} (fallback)") + logger_handler.logger.debug(f"Added check-in address hyperlink (fallback) for employee {attendance_record.employee_id}") else: cell.value = address_text else: @@ -1928,7 +1915,7 @@ def create_excel_export(selected_columns, column_names, filters): lng_formatted = f"{float(attendance_record.longitude):.10f}" hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")' cell.value = hyperlink_formula - print(f"๐Ÿ“ Added check-in address hyperlink for employee {attendance_record.employee_id} (no accuracy data)") + logger_handler.logger.debug(f"Added check-in address hyperlink (no accuracy data) for employee {attendance_record.employee_id}") else: cell.value = address_text elif column_key == 'device_info': @@ -1948,7 +1935,7 @@ def create_excel_export(selected_columns, column_names, filters): else: cell.value = '' except Exception as cell_error: - print(f"โš ๏ธ Error setting cell value for {column_key}: {cell_error}") + logger_handler.logger.warning(f"Error setting cell value for {column_key}: {cell_error}") cell.value = '' # Auto-adjust column widths based on content and header @@ -2007,26 +1994,25 @@ def create_excel_export(selected_columns, column_names, filters): ws.column_dimensions[column_letter].width = adjusted_width - print(f"๐Ÿ“ Column {column_letter} ({column_key}): set width to {adjusted_width} (content: {max_length} chars)") + logger_handler.logger.debug(f"Column {column_letter} ({column_key}): width={adjusted_width} (max_content={max_length})") # Save to BytesIO excel_buffer = io.BytesIO() wb.save(excel_buffer) excel_buffer.seek(0) - print("๐Ÿ“Š Excel file created successfully with employee names") + logger_handler.logger.info("Excel file created successfully with employee names") # Log export action with employee name column try: logger_handler.logger.info(f"Excel export with employee names generated by user {session.get('username', 'unknown')}") - except Exception as log_error: - print(f"โš ๏ธ Logging error (non-critical): {log_error}") + except Exception: + pass return excel_buffer except Exception as e: - print(f"โŒ Error creating Excel export: {e}") - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error creating Excel export: {e}", exc_info=True) # Log error try: @@ -2036,7 +2022,7 @@ def create_excel_export(selected_columns, column_names, filters): stack_trace=traceback.format_exc() ) except Exception as log_error: - print(f"โš ๏ธ Could not log error: {log_error}") + logger_handler.logger.warning(f"Could not log error: {log_error}") return None @@ -2052,7 +2038,7 @@ def format_employee_id_for_excel(employee_id): def create_excel_export_ordered(selected_columns, column_names, filters): """Create Excel file with selected attendance data in specified column order""" try: - print(f"๐Ÿ“Š Creating Excel export with {len(selected_columns)} columns in order: {selected_columns}") + logger_handler.logger.info(f"Creating Excel export with {len(selected_columns)} columns") # Import openpyxl modules try: @@ -2060,8 +2046,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters): from openpyxl.styles import Font, Alignment, PatternFill from openpyxl.utils import get_column_letter except ImportError as e: - print(f"โŒ openpyxl import error: {e}") - print("๐Ÿ’ก Install openpyxl: pip install openpyxl") + logger_handler.logger.error(f"openpyxl import error: {e}. Run: pip install openpyxl") return None # Build query based on filters - JOIN with QRCode to get location_event and location_address @@ -2077,22 +2062,22 @@ def create_excel_export_ordered(selected_columns, column_names, filters): try: date_from = datetime.strptime(filters['date_from'], '%Y-%m-%d').date() query = query.filter(AttendanceData.check_in_date >= date_from) - print(f"๐Ÿ“Š Applied date_from filter: {date_from}") + logger_handler.logger.debug(f"Applied date_from filter: {date_from}") except ValueError as e: - print(f"โš ๏ธ Invalid date_from format: {e}") + logger_handler.logger.warning(f"Invalid date_from format: {e}") if filters.get('date_to'): try: date_to = datetime.strptime(filters['date_to'], '%Y-%m-%d').date() query = query.filter(AttendanceData.check_in_date <= date_to) - print(f"๐Ÿ“Š Applied date_to filter: {date_to}") + logger_handler.logger.debug(f"Applied date_to filter: {date_to}") except ValueError as e: - print(f"โš ๏ธ Invalid date_to format: {e}") + logger_handler.logger.warning(f"Invalid date_to format: {e}") # Apply location filter if filters.get('location_filter'): query = query.filter(AttendanceData.location_name.like(f"%{filters['location_filter']}%")) - print(f"๐Ÿ“Š Applied location filter: {filters['location_filter']}") + logger_handler.logger.debug(f"Applied location filter: {filters['location_filter']}") # Apply employee filter โ€” supports comma-separated multi-employee values if filters.get('employee_filter'): @@ -2101,26 +2086,26 @@ def create_excel_export_ordered(selected_columns, column_names, filters): query = query.filter(AttendanceData.employee_id == emp_ids[0]) elif len(emp_ids) > 1: query = query.filter(AttendanceData.employee_id.in_(emp_ids)) - print(f"๐Ÿ“Š Applied employee filter: {emp_ids}") + logger_handler.logger.debug(f"Applied employee filter: {emp_ids}") # Apply project filter if filters.get('project_filter'): try: project_id = int(filters['project_filter']) query = query.filter(QRCode.project_id == project_id) - print(f"๐Ÿ“Š Applied project filter: {project_id}") + logger_handler.logger.debug(f"Applied project filter: {project_id}") except (ValueError, TypeError) as e: - print(f"โš ๏ธ Invalid project filter: {e}") + logger_handler.logger.warning(f"Invalid project filter: {e}") # Order by date and time query = query.order_by(AttendanceData.check_in_date.desc(), AttendanceData.check_in_time.desc()) # Execute query results = query.all() - print(f"๐Ÿ“Š Query returned {len(results)} records") + logger_handler.logger.debug(f"Query returned {len(results)} records for export") if not results: - print("โš ๏ธ No records found for export") + logger_handler.logger.warning("No records found for export") return None # Create workbook @@ -2192,10 +2177,10 @@ def create_excel_export_ordered(selected_columns, column_names, filters): lng_formatted = f"{float(qr_record.address_longitude):.10f}" hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")' cell.value = hyperlink_formula - print(f"๐Ÿ“ Added QR address hyperlink for employee {attendance_record.employee_id}") + logger_handler.logger.debug(f"Added QR address hyperlink for employee {attendance_record.employee_id}") else: cell.value = address_text - print(f"๐Ÿ“ Using QR address for employee {attendance_record.employee_id} (accuracy: {accuracy_value:.3f} miles)") + logger_handler.logger.debug(f"Using QR address for employee {attendance_record.employee_id} (accuracy: {accuracy_value:.3f} miles)") else: # Lower accuracy - use actual check-in address with hyperlink address_text = attendance_record.address or '' @@ -2205,10 +2190,10 @@ def create_excel_export_ordered(selected_columns, column_names, filters): lng_formatted = f"{float(attendance_record.longitude):.10f}" hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")' cell.value = hyperlink_formula - print(f"๐Ÿ“ Added check-in address hyperlink for employee {attendance_record.employee_id}") + logger_handler.logger.debug(f"Added check-in address hyperlink for employee {attendance_record.employee_id}") else: cell.value = address_text - print(f"๐Ÿ“ Using check-in address for employee {attendance_record.employee_id} (accuracy: {accuracy_value:.3f} miles)") + logger_handler.logger.debug(f"Using check-in address for employee {attendance_record.employee_id} (accuracy: {accuracy_value:.3f} miles)") except (ValueError, TypeError): # If accuracy can't be converted to float, use check-in address with hyperlink address_text = attendance_record.address or '' @@ -2218,7 +2203,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters): lng_formatted = f"{float(attendance_record.longitude):.10f}" hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")' cell.value = hyperlink_formula - print(f"๐Ÿ“ Added check-in address hyperlink for employee {attendance_record.employee_id} (fallback)") + logger_handler.logger.debug(f"Added check-in address hyperlink (fallback) for employee {attendance_record.employee_id}") else: cell.value = address_text else: @@ -2230,7 +2215,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters): lng_formatted = f"{float(attendance_record.longitude):.10f}" hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")' cell.value = hyperlink_formula - print(f"๐Ÿ“ Added check-in address hyperlink for employee {attendance_record.employee_id} (no accuracy data)") + logger_handler.logger.debug(f"Added check-in address hyperlink (no accuracy data) for employee {attendance_record.employee_id}") else: cell.value = address_text elif column_key == 'device_info': @@ -2259,7 +2244,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters): else: cell.value = '' except Exception as cell_error: - print(f"โš ๏ธ Error setting cell value for {column_key}: {cell_error}") + logger_handler.logger.warning(f"Error setting cell value for {column_key}: {cell_error}") cell.value = '' # Auto-adjust column widths based on content and header @@ -2318,26 +2303,25 @@ def create_excel_export_ordered(selected_columns, column_names, filters): ws.column_dimensions[column_letter].width = adjusted_width - print(f"๐Ÿ“ Column {column_letter} ({column_key}): set width to {adjusted_width} (content: {max_length} chars)") + logger_handler.logger.debug(f"Column {column_letter} ({column_key}): width={adjusted_width} (max_content={max_length})") # Save to BytesIO excel_buffer = io.BytesIO() wb.save(excel_buffer) excel_buffer.seek(0) - print("๐Ÿ“Š Excel file created successfully with employee names and verification status coloring") + logger_handler.logger.info("Excel file created successfully with employee names and verification status coloring") # Log export action with employee name column and verification status coloring try: logger_handler.logger.info(f"Excel export with employee names and verification status coloring generated by user {session.get('username', 'unknown')}") - except Exception as log_error: - print(f"โš ๏ธ Logging error (non-critical): {log_error}") + except Exception: + pass return excel_buffer except Exception as e: - print(f"โŒ Error creating Excel export: {e}") - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error creating Excel export: {e}", exc_info=True) # Log error try: @@ -2347,6 +2331,6 @@ def create_excel_export_ordered(selected_columns, column_names, filters): stack_trace=traceback.format_exc() ) except Exception as log_error: - print(f"โš ๏ธ Could not log error: {log_error}") + logger_handler.logger.warning(f"Could not log error: {log_error}") return None diff --git a/routes/auth.py b/routes/auth.py index 1434f07..be5cbb2 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -161,6 +161,7 @@ def login(): logger_handler.logger.warning(f"Failed login attempt for username: {username}") except Exception as e: + db.session.rollback() logger_handler.log_database_error('user_login', e) logger_handler.logger.error(f"Login error for username '{username}': {e}") flash('Login error. Please try again.', 'error') @@ -201,7 +202,7 @@ def logout(): def profile(): """User profile management with logging""" try: - user = User.query.get(session['user_id']) + user = db.session.get(User, session['user_id']) if request.method == 'POST': form_type = request.form.get('form_type') @@ -259,6 +260,7 @@ def profile(): return render_template('profile.html', user=user) except Exception as e: + db.session.rollback() logger_handler.log_database_error('profile_update', e) flash('Profile update failed. Please try again.', 'error') - return redirect(url_for('dashboard.dashboard')) \ No newline at end of file + return redirect(url_for('dashboard.dashboard')) diff --git a/routes/dashboard.py b/routes/dashboard.py index 65659cc..26d3101 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -26,7 +26,7 @@ bp = Blueprint('dashboard', __name__) def dashboard(): """Enhanced project-centric dashboard with search filters""" try: - user = User.query.get(session['user_id']) + user = db.session.get(User, session['user_id']) # Get search parameters from URL search_name = request.args.get('search_name', '').strip() @@ -71,7 +71,6 @@ def dashboard(): except Exception as e: logger_handler.log_database_error('dashboard_load', e) - print(f"Error loading dashboard: {e}") flash('Error loading dashboard. Please try again.', 'error') return redirect(url_for('auth.login')) @@ -127,7 +126,6 @@ def project_qr_codes(project_id): except Exception as e: logger_handler.log_database_error('project_qr_codes_view', e) - print(f"Error loading project QR codes: {e}") flash('Error loading project QR codes. Please try again.', 'error') return redirect(url_for('dashboard.dashboard')) diff --git a/routes/employees.py b/routes/employees.py index aabfdd6..7afd18a 100644 --- a/routes/employees.py +++ b/routes/employees.py @@ -134,7 +134,7 @@ def create_employee(): db.session.commit() # Log employee creation with project info - project = Project.query.get(contract_id_int) + project = db.session.get(Project, contract_id_int) project_name = project.name if project else f"Project {contract_id_int}" logger_handler.logger.info( f"User {session['username']} created new employee: " @@ -213,7 +213,7 @@ def edit_employee(employee_index): db.session.commit() # Log employee update with project info - project = Project.query.get(contract_id_int) + project = db.session.get(Project, contract_id_int) project_name = project.name if project else f"Project {contract_id_int}" logger_handler.logger.info( f"User {session['username']} updated employee: " @@ -372,4 +372,4 @@ def employee_detail(employee_index): except Exception as e: logger_handler.log_database_error('employee_detail', e) flash('Error loading employee details. Please try again.', 'error') - return redirect(url_for('employees.employees')) \ No newline at end of file + return redirect(url_for('employees.employees')) diff --git a/routes/payroll.py b/routes/payroll.py index 86b56ec..ccbf6db 100644 --- a/routes/payroll.py +++ b/routes/payroll.py @@ -44,7 +44,7 @@ def payroll_dashboard(): flash('Access denied. Only administrators and payroll staff can access payroll features.', 'error') return redirect(url_for('dashboard.dashboard')) - print("๐Ÿ“Š Loading payroll dashboard") + logger_handler.logger.debug("Loading payroll dashboard") # Log payroll dashboard access logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed payroll dashboard") @@ -65,9 +65,9 @@ def payroll_dashboard(): projects = [] try: projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() - print(f"๐Ÿ“Š Found {len(projects)} active projects for filter") + logger_handler.logger.debug(f"Found {len(projects)} active projects for payroll filter") except Exception as e: - print(f"โš ๏ธ Error loading projects: {e}") + logger_handler.logger.warning(f"Error loading projects for payroll filter: {e}") # Get attendance records for the period attendance_records = [] @@ -90,12 +90,12 @@ def payroll_dashboard(): # Apply project filter if selected if project_filter and project_filter != '': query = query.filter(QRCode.project_id == int(project_filter)) - print(f"๐Ÿ“Š Applied project filter: {project_filter}") + logger_handler.logger.debug(f"Applied project filter: {project_filter}") query = query.order_by(AttendanceData.employee_id, AttendanceData.check_in_date, AttendanceData.check_in_time) attendance_records = query.all() - print(f"๐Ÿ“Š Found {len(attendance_records)} attendance records for payroll calculation") + logger_handler.logger.debug(f"Found {len(attendance_records)} attendance records for payroll calculation") # Calculate working hours if we have records if attendance_records: @@ -103,13 +103,13 @@ def payroll_dashboard(): working_hours_data = calculator.calculate_all_employees_hours( start_date, end_date, attendance_records ) - print(f"๐Ÿ“Š Calculated hours for {working_hours_data['employee_count']} employees") + logger_handler.logger.debug(f"Calculated hours for {working_hours_data['employee_count']} employees") except ValueError as e: - print(f"โš ๏ธ Invalid date format: {e}") + logger_handler.logger.warning(f"Invalid date format in payroll dashboard: {e}") flash('Invalid date format. Please use YYYY-MM-DD format.', 'error') except Exception as e: - print(f"โŒ Error calculating working hours: {e}") + logger_handler.logger.error(f"Error calculating working hours: {e}", exc_info=True) logger_handler.log_database_error('payroll_calculation', e) flash('Error calculating working hours. Please check the server logs.', 'error') @@ -136,23 +136,21 @@ def payroll_dashboard(): if row[1]: # Only add if we got a name employee_names[str(row[0])] = row[1] - print(f"๐Ÿ“Š Retrieved names for {len(employee_names)} employees using CAST method") + logger_handler.logger.debug(f"Retrieved names for {len(employee_names)} employees") except Exception as e: - print(f"โš ๏ธ Could not load employee names: {e}") - import traceback - print(f"โš ๏ธ Traceback: {traceback.format_exc()}") + logger_handler.logger.warning(f"Could not load employee names: {e}", exc_info=True) # Continue without names - will use employee IDs # Get selected project name for display selected_project_name = '' if project_filter: try: - selected_project = Project.query.get(int(project_filter)) + selected_project = db.session.get(Project, int(project_filter)) if selected_project: selected_project_name = selected_project.name except Exception as e: - print(f"โš ๏ธ Error getting selected project name: {e}") + logger_handler.logger.warning(f"Error getting selected project name: {e}") return render_template('payroll_dashboard.html', working_hours_data=working_hours_data, @@ -165,9 +163,7 @@ def payroll_dashboard(): user_role=user_role) except Exception as e: - print(f"โŒ Error loading payroll dashboard: {e}") - import traceback - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error loading payroll dashboard: {e}", exc_info=True) logger_handler.log_flask_error( 'payroll_dashboard_error', @@ -191,7 +187,7 @@ def export_payroll_excel(): flash('Access denied. Only administrators and payroll staff can export payroll data.', 'error') return redirect(url_for('payroll.payroll_dashboard')) - print("๐Ÿ“Š Payroll Excel export started") + logger_handler.logger.info(f"Payroll Excel export started by user {session.get('username', 'unknown')}") # Get parameters from form date_from = request.form.get('date_from') @@ -222,7 +218,7 @@ def export_payroll_excel(): # Apply project filter if selected if project_filter and project_filter != '': query = query.filter(QRCode.project_id == int(project_filter)) - print(f"๐Ÿ“Š Applied project filter to export: {project_filter}") + logger_handler.logger.debug(f"Applied project filter to export: {project_filter}") query = query.order_by(AttendanceData.employee_id, AttendanceData.check_in_date, AttendanceData.check_in_time) @@ -235,13 +231,13 @@ def export_payroll_excel(): attendance_data.qr_code = qr_code attendance_records.append(attendance_data) - print(f"๐Ÿ“Š Export: Found {len(attendance_records)} records with QR data") + logger_handler.logger.info(f"Payroll export: found {len(attendance_records)} records with QR data") if not attendance_records: flash('No attendance records found for the selected date range and project.', 'warning') return redirect(url_for('payroll.payroll_dashboard')) - print(f"๐Ÿ“Š Exporting {len(attendance_records)} attendance records to Excel") + logger_handler.logger.info(f"Exporting {len(attendance_records)} attendance records to payroll Excel") # Get employee names using the same method as dashboard employee_names = {} @@ -264,24 +260,22 @@ def export_payroll_excel(): if row[1]: # Only add if we got a name employee_names[str(row[0])] = row[1] - print(f"๐Ÿ“Š Retrieved names for {len(employee_names)} employees for export using CAST method") + logger_handler.logger.debug(f"Retrieved names for {len(employee_names)} employees for export") except Exception as e: - print(f"โš ๏ธ Could not load employee names for export: {e}") - import traceback - print(f"โš ๏ธ Traceback: {traceback.format_exc()}") + logger_handler.logger.warning(f"Could not load employee names for export: {e}", exc_info=True) # Get project name for enhanced reports and filename project_name = None project_name_for_filename = '' if project_filter: try: - project = Project.query.get(int(project_filter)) + project = db.session.get(Project, int(project_filter)) if project: project_name = project.name project_name_for_filename = f"_{project.name.replace(' ', '_')}" except Exception as e: - print(f"โš ๏ธ Error getting project name: {e}") + logger_handler.logger.warning(f"Error getting project name for export: {e}") # Generate Excel file based on report type excel_file = None @@ -289,7 +283,7 @@ def export_payroll_excel(): if report_type == 'enhanced': # Use enhanced exporter for SP/PW reports - print("๐Ÿ“Š Creating enhanced payroll report with SP/PW support") + logger_handler.logger.debug("Creating enhanced payroll report with SP/PW support") try: from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter exporter = EnhancedPayrollExcelExporter(company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System')) @@ -297,9 +291,9 @@ def export_payroll_excel(): start_date, end_date, attendance_records, employee_names, project_name ) filename_prefix = 'enhanced_payroll_report' - print("โœ… Enhanced payroll report created successfully") + logger_handler.logger.info("Enhanced payroll report created successfully") except ImportError: - print("โš ๏ธ Enhanced exporter not available, falling back to standard exporter") + logger_handler.logger.warning("Enhanced exporter not available, falling back to standard exporter") # Fall back to standard exporter exporter = PayrollExcelExporter( company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'), @@ -310,7 +304,7 @@ def export_payroll_excel(): ) filename_prefix = 'payroll_report' except Exception as e: - print(f"โš ๏ธ Error with enhanced exporter: {e}, falling back to standard exporter") + logger_handler.logger.warning(f"Enhanced exporter error: {e} โ€” falling back to standard exporter") # Fall back to standard exporter exporter = PayrollExcelExporter( company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'), @@ -323,7 +317,7 @@ def export_payroll_excel(): elif report_type == 'detailed_sp_pw': # Detailed daily SP/PW breakdown - print("๐Ÿ“Š Creating detailed SP/PW daily breakdown report") + logger_handler.logger.debug("Creating detailed SP/PW daily breakdown report") try: from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter exporter = EnhancedPayrollExcelExporter(company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System')) @@ -331,9 +325,9 @@ def export_payroll_excel(): start_date, end_date, attendance_records, employee_names ) filename_prefix = 'detailed_sp_pw_report' - print("โœ… Detailed SP/PW report created successfully") + logger_handler.logger.info("Detailed SP/PW report created successfully") except ImportError: - print("โš ๏ธ Enhanced exporter not available, falling back to detailed hours report") + logger_handler.logger.warning("Enhanced exporter not available, falling back to detailed hours report") # Fall back to standard detailed report exporter = PayrollExcelExporter( company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'), @@ -344,7 +338,7 @@ def export_payroll_excel(): ) filename_prefix = 'detailed_hours_report' except Exception as e: - print(f"โš ๏ธ Error with enhanced exporter: {e}, falling back to detailed hours report") + logger_handler.logger.warning(f"Enhanced exporter error: {e} โ€” falling back to detailed hours report") # Fall back to standard detailed report exporter = PayrollExcelExporter( company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'), @@ -384,7 +378,7 @@ def export_payroll_excel(): timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f'{filename_prefix}_{date_from}_to_{date_to}{project_name_for_filename}_{timestamp}.xlsx' - print(f"๐Ÿ“Š Payroll Excel file generated successfully: {filename}") + logger_handler.logger.info(f"Payroll Excel file generated successfully: {filename}") # Log successful export logger_handler.logger.info(f"Payroll Excel export generated by user {session.get('username', 'unknown')}: {filename}") @@ -406,9 +400,7 @@ def export_payroll_excel(): return redirect(url_for('payroll.payroll_dashboard')) except Exception as e: - print(f"โŒ Error in export_payroll_excel route: {e}") - import traceback - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error in export_payroll_excel route: {e}", exc_info=True) logger_handler.log_flask_error( 'payroll_excel_export_error', @@ -484,7 +476,7 @@ def calculate_working_hours_api(): }) except Exception as e: - print(f"โŒ Error in calculate_working_hours_api: {e}") + logger_handler.logger.error(f"Error in calculate_working_hours_api: {e}", exc_info=True) logger_handler.log_flask_error( 'working_hours_api_error', str(e), @@ -541,11 +533,9 @@ def get_miss_punch_details(employee_id): employee_row = employee_query.fetchone() employee_name = employee_row.full_name if employee_row and employee_row.full_name else f"Employee {employee_id}" - print(f"๐Ÿ“‹ Retrieved employee name: {employee_name} for ID: {employee_id}") + logger_handler.logger.debug(f"Retrieved employee name for ID {employee_id}: {employee_name}") except Exception as e: - print(f"โš ๏ธ Could not load employee name for ID {employee_id}: {e}") - import traceback - print(f"โš ๏ธ Traceback: {traceback.format_exc()}") + logger_handler.logger.warning(f"Could not load employee name for ID {employee_id}: {e}", exc_info=True) employee_name = f"Employee {employee_id}" # Get attendance records for the employee within the period @@ -647,9 +637,7 @@ def get_miss_punch_details(employee_id): }) except Exception as e: - print(f"โŒ Error in get_miss_punch_details: {e}") - import traceback - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error in get_miss_punch_details: {e}", exc_info=True) logger_handler.log_flask_error( 'miss_punch_details_api_error', @@ -675,7 +663,7 @@ def get_employee_name(employee_id): return row[0] if row else f"Employee {employee_id}" except Exception as e: - print(f"โš ๏ธ Error getting employee name for ID {employee_id}: {e}") + logger_handler.logger.warning(f"Error getting employee name for ID {employee_id}: {e}") return f"Employee {employee_id}" def get_qr_code_checkin_count(qr_code_id): diff --git a/routes/qr_codes.py b/routes/qr_codes.py index 2d81ffc..f1a7876 100644 --- a/routes/qr_codes.py +++ b/routes/qr_codes.py @@ -86,9 +86,9 @@ def create_qr_code(): address_latitude = float(latitude) address_longitude = float(longitude) has_coordinates = True - print(f"โœ“ Coordinates received: {address_latitude}, {address_longitude}") + logger_handler.logger.debug(f"Coordinates received: {address_latitude}, {address_longitude}") except (ValueError, TypeError) as e: - print(f"โš ๏ธ Invalid coordinates format: {e}") + logger_handler.logger.warning(f"Invalid coordinates format: {e}") address_latitude = None address_longitude = None has_coordinates = False @@ -98,7 +98,7 @@ def create_qr_code(): if project_id: try: project_id = int(project_id) - project = Project.query.get(project_id) + project = db.session.get(Project, project_id) if not project or not project.active_status: flash('Selected project is not valid or inactive.', 'error') return render_template('create_qr_code.html', @@ -192,7 +192,7 @@ def create_qr_code(): db.session.rollback() logger_handler.log_database_error('qr_code_creation', e) flash('QR Code creation failed. Please try again.', 'error') - print(f"โŒ QR Code creation error: {e}") + logger_handler.logger.error(f"QR Code creation error: {e}", exc_info=True) # Get active projects and styles for dropdown projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() @@ -435,7 +435,7 @@ def edit_qr_code(qr_id): if new_project_id and new_project_id.strip(): try: new_project_id = int(new_project_id) - project = Project.query.get(new_project_id) + project = db.session.get(Project, new_project_id) if project and project.active_status: qr_code.project_id = new_project_id else: @@ -518,16 +518,16 @@ def delete_qr_code(qr_id): """Permanently delete QR code (Admin only) - Hard delete - PRESERVING EXACT ROUTE""" try: qr_code = QRCode.query.get_or_404(qr_id) - print(f"โœ… Found QR Code: {qr_code.name}") + logger_handler.logger.debug(f"Found QR Code for delete: {qr_code.name} (ID: {qr_id})") if request.method == 'POST': qr_name = qr_code.name qr_code_id = qr_code.id - print(f"๐Ÿ—‘๏ธ ATTEMPTING TO DELETE: {qr_name}") + logger_handler.logger.info(f"User {session.get('username', 'unknown')} attempting to delete QR code: {qr_name} (ID: {qr_id})") # Check if QR exists before delete before_count = QRCode.query.count() - print(f"๐Ÿ“Š QR count before delete: {before_count}") + logger_handler.logger.debug(f"QR count before delete: {before_count}") # Log QR code deletion before actual deletion logger_handler.log_qr_code_deleted( @@ -538,29 +538,27 @@ def delete_qr_code(qr_id): # Delete the QR code db.session.delete(qr_code) - print("๐Ÿ’พ Called db.session.delete()") + db.session.commit() - print("๐Ÿ’พ Called db.session.commit()") + # Check count after delete after_count = QRCode.query.count() - print(f"๐Ÿ“Š QR count after delete: {after_count}") - print(f"โœ… DELETE SUCCESS! Removed {before_count - after_count} records") + logger_handler.logger.debug(f"QR count after delete: {after_count}") + logger_handler.logger.info(f"QR code deleted successfully: {qr_name} (ID: {qr_id}), removed {before_count - after_count} records") flash(f'QR code "{qr_name}" has been permanently deleted!', 'success') return redirect(url_for('dashboard.dashboard')) # GET request - show confirmation page - print("๐Ÿ“„ Showing confirmation page") + logger_handler.logger.debug(f"Showing delete confirmation page for QR code ID: {qr_id}") return render_template('confirm_delete_qr.html', qr_code=qr_code) except Exception as e: db.session.rollback() logger_handler.log_database_error('qr_code_deletion', e) - print(f"โŒ ERROR in delete route: {e}") - print(f"โŒ Exception type: {type(e)}") - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error in QR code delete route (ID: {qr_id}): {e}", exc_info=True) flash('Error deleting QR code. Please try again.', 'error') return redirect(url_for('dashboard.dashboard')) @@ -603,23 +601,19 @@ def qr_checkin(qr_url): PRESERVES coordinate-to-address conversion functionality """ try: - print(f"\n๐Ÿš€ STARTING ENHANCED CHECK-IN PROCESS") - print(f" QR URL: {qr_url}") - print(f" Timestamp: {datetime.now()}") + logger_handler.logger.debug(f"Starting check-in process for QR URL: {qr_url}") # Find QR code by URL qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first() if not qr_code: - print(f"โŒ QR code not found or inactive: {qr_url}") + logger_handler.logger.warning(f"QR code not found or inactive: {qr_url}") return jsonify({ 'success': False, 'message': 'QR code not found or inactive.' }), 404 - print(f"โœ… Found QR code: {qr_code.name} (ID: {qr_code.id})") - print(f" Location: {qr_code.location}") - print(f" QR Address: {qr_code.location_address}") + logger_handler.logger.debug(f"Found QR code: {qr_code.name} (ID: {qr_code.id}), location: {qr_code.location}") # Get and validate employee ID employee_id = request.form.get('employee_id', '').strip() @@ -650,9 +644,7 @@ def qr_checkin(qr_url): # Check if 30 minutes have passed since the last check-in if recent_checkin_datetime > the_last_checkin_time: minutes_remaining = time_interval - int((current_time - recent_checkin_datetime).total_seconds() / 60) - print(f"โš ๏ธ Too soon for another {qr_code.location_event} for {employee_id}") - print(f" Last {qr_code.location_event}: {recent_checkin.check_in_time.strftime('%H:%M')}") - print(f" Minutes remaining: {minutes_remaining}") + logger_handler.logger.info(f"Too soon for another {qr_code.location_event} for employee {employee_id}: {minutes_remaining} minutes remaining") return jsonify({ 'success': False, @@ -660,9 +652,9 @@ def qr_checkin(qr_url): f"Puedes volver a registrarte en {minutes_remaining} minutos. El รบltimo registro fue a las {recent_checkin.check_in_time.strftime("%H:%M")}." }), 400 else: - print(f"โœ… {time_interval}-minute interval satisfied. Allowing new {qr_code.location_event} for {employee_id}") + logger_handler.logger.debug(f"{time_interval}-minute interval satisfied for employee {employee_id}") else: - print(f"โœ… First {qr_code.location_event} today for {employee_id}") + logger_handler.logger.debug(f"First {qr_code.location_event} today for employee {employee_id}") # Process location data with coordinate-to-address conversion location_data = process_location_data_enhanced(request.form) @@ -672,12 +664,10 @@ def qr_checkin(qr_url): device_info = detect_device_info(user_agent_string) client_ip = get_client_ip() - print(f"๐Ÿ“ฑ Device Info: {device_info}") - print(f"๐ŸŒ IP Address: {client_ip}") - print(f"๐Ÿ“ Location Data: {location_data}") + logger_handler.logger.debug(f"Check-in device: {device_info}, IP: {client_ip}") # Create attendance record - print(f"\n๐Ÿ’พ CREATING ATTENDANCE RECORD:") + logger_handler.logger.debug("Creating attendance record") attendance = AttendanceData( qr_code_id=qr_code.id, @@ -699,39 +689,29 @@ def qr_checkin(qr_url): verification_status=None ) - print(f"โœ… Created base attendance record") + logger_handler.logger.debug("Created base attendance record") - # ENHANCED DEBUG: Calculate location accuracy with detailed logging - print(f"\n๐ŸŽฏ CALCULATING LOCATION ACCURACY WITH ENHANCED DEBUG...") - print(f" ๐Ÿ“Š QR Code Details:") - print(f" ID: {qr_code.id}") - print(f" Name: {qr_code.name}") - print(f" Location: {qr_code.location}") - print(f" Location Address: {qr_code.location_address}") - print(f" Has location_address: {qr_code.location_address is not None}") - print(f" Location Address Length: {len(qr_code.location_address) if qr_code.location_address else 0}") - - print(f" ๐Ÿ“ Check-in Data:") - print(f" Latitude: {location_data['latitude']}") - print(f" Longitude: {location_data['longitude']}") - print(f" GPS Accuracy: {location_data['accuracy']}") - print(f" Address: {location_data['address']}") - print(f" Address Length: {len(location_data['address']) if location_data['address'] else 0}") - print(f" Source: {location_data['source']}") + # Calculate location accuracy + logger_handler.logger.debug( + f"Location accuracy check: QR='{qr_code.name}' (ID={qr_code.id}), " + f"lat={location_data['latitude']}, lng={location_data['longitude']}, " + f"source={location_data['source']}" + ) location_accuracy = None try: # Check if we have the required data if not qr_code.location_address: - print(f"โŒ QR code location_address is empty or None") - print(f" QR Code location_address value: '{qr_code.location_address}'") + logger_handler.logger.warning(f"QR code location_address is empty or None for QR ID: {qr_code.id}") elif not location_data['address'] and not (location_data['latitude'] and location_data['longitude']): - print(f"โŒ No check-in address or coordinates available") - print(f" Check-in address: '{location_data['address']}'") - print(f" Check-in coords: {location_data['latitude']}, {location_data['longitude']}") + logger_handler.logger.warning( + f"No check-in address or coordinates available: " + f"address={location_data['address']!r}, " + f"coords={location_data['latitude']}, {location_data['longitude']}" + ) else: - print(f"โœ… Required data available, proceeding with calculation...") + logger_handler.logger.debug("Required location data available, proceeding with accuracy calculation") location_accuracy = calculate_location_accuracy_enhanced( qr_address=qr_code.location_address, @@ -740,30 +720,28 @@ def qr_checkin(qr_url): checkin_lng=location_data['longitude'] ) - print(f"๐Ÿ“ Location accuracy calculation result: {location_accuracy}") + logger_handler.logger.debug(f"Location accuracy calculation result: {location_accuracy}") if location_accuracy is not None: attendance.location_accuracy = location_accuracy accuracy_level = get_location_accuracy_level_enhanced(location_accuracy) - print(f"โœ… Location accuracy set successfully: {location_accuracy:.4f} miles ({accuracy_level})") - print(f"๐Ÿ“Š Final attendance.location_accuracy value: {attendance.location_accuracy}") + logger_handler.logger.debug(f"Location accuracy set: {location_accuracy:.4f} miles ({accuracy_level})") else: - print(f"โš ๏ธ Could not calculate location accuracy - calculation returned None") + logger_handler.logger.warning("Could not calculate location accuracy โ€” calculation returned None") # CHECK DISTANCE THRESHOLD FOR PHOTO VERIFICATION - print(f"\n๐Ÿ“ธ CHECKING PHOTO VERIFICATION REQUIREMENT:") - print(f" Photo Verification Enabled: {current_app.config.get('PHOTO_VERIFICATION_ENABLED', True)}") + logger_handler.logger.debug(f"Photo verification enabled: {current_app.config.get('PHOTO_VERIFICATION_ENABLED', True)}") requires_verification = False verification_photo_data = None if current_app.config.get('PHOTO_VERIFICATION_ENABLED', True) and location_accuracy is not None and location_accuracy > current_app.config.get('DISTANCE_THRESHOLD_FOR_VERIFICATION', 0.3): - print(f"โš ๏ธ Distance ({location_accuracy:.3f} mi) exceeds threshold ({current_app.config.get('DISTANCE_THRESHOLD_FOR_VERIFICATION', 0.3)} mi)") + logger_handler.logger.info(f"Distance ({location_accuracy:.3f} mi) exceeds verification threshold for employee {employee_id}") # Check if photo was provided verification_photo_data = request.form.get('verification_photo', None) if verification_photo_data: - print(f"โœ… Verification photo provided (size: {len(verification_photo_data)} chars)") + logger_handler.logger.debug(f"Verification photo provided (size: {len(verification_photo_data)} chars)") # Validate photo data (basic validation) if verification_photo_data.startswith('data:image/'): @@ -771,16 +749,16 @@ def qr_checkin(qr_url): attendance.verification_required = True attendance.verification_status = 'pending' attendance.verification_timestamp = datetime.now() - print(f"โœ… Photo verification set to PENDING status") + logger_handler.logger.info(f"Photo verification set to PENDING for employee {employee_id}") else: - print(f"โš ๏ธ Invalid photo format provided") + logger_handler.logger.warning(f"Invalid photo format provided for employee {employee_id}") return jsonify({ 'success': False, 'message': 'Invalid photo format. Please try again.', 'requires_verification': True }), 400 else: - print(f"โŒ Photo verification REQUIRED but not provided") + logger_handler.logger.warning(f"Photo verification required but not provided for employee {employee_id}") return jsonify({ 'success': False, 'message': 'Photo verification required. Distance from location is too far.', @@ -789,22 +767,14 @@ def qr_checkin(qr_url): 'threshold': current_app.config.get('DISTANCE_THRESHOLD_FOR_VERIFICATION', 0.3) }), 400 else: - print(f"โœ… Distance within threshold - no verification needed") + logger_handler.logger.debug(f"Distance within threshold for employee {employee_id} โ€” no verification needed") except Exception as e: - print(f"โŒ Error in location accuracy calculation: {e}") - print(f"โŒ Full traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Error in location accuracy calculation: {e}", exc_info=True) # ENHANCED DEBUG: Save to database with verification try: - print(f"\n๐Ÿ’พ SAVING TO DATABASE...") - print(f" Attendance object before save:") - print(f" Employee ID: {attendance.employee_id}") - print(f" Location: {attendance.location_name}") - print(f" Latitude: {attendance.latitude}") - print(f" Longitude: {attendance.longitude}") - print(f" Address: {attendance.address}") - print(f" Location Accuracy: {attendance.location_accuracy}") + logger_handler.logger.debug(f"Saving attendance record: employee={attendance.employee_id}, location={attendance.location_name}, accuracy={attendance.location_accuracy}") db.session.add(attendance) db.session.commit() @@ -819,14 +789,11 @@ def qr_checkin(qr_url): ) # VERIFICATION: Read back from database - saved_record = AttendanceData.query.get(attendance.id) - print(f"โœ… Successfully saved attendance record with ID: {attendance.id}") - print(f"๐Ÿ“Š Verification - location accuracy in database: {saved_record.location_accuracy}") + saved_record = db.session.get(AttendanceData, attendance.id) + logger_handler.logger.info(f"Saved attendance record ID: {attendance.id}, db accuracy: {saved_record.location_accuracy}") if saved_record.location_accuracy != attendance.location_accuracy: - print(f"โš ๏ธ WARNING: Database value differs from object value!") - print(f" Object value: {attendance.location_accuracy}") - print(f" Database value: {saved_record.location_accuracy}") + logger_handler.logger.warning(f"DB accuracy mismatch: object={attendance.location_accuracy}, db={saved_record.location_accuracy}") # Add enhanced logging for location accuracy save if attendance.location_accuracy is not None: @@ -844,8 +811,7 @@ def qr_checkin(qr_url): checkin_sequence_text = f"{qr_code.location_event} details" except Exception as e: - print(f"โŒ Database error: {e}") - print(f"โŒ Full traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Database error saving attendance record: {e}", exc_info=True) db.session.rollback() logger_handler.log_database_error('checkin_save', e) return jsonify({ @@ -879,14 +845,11 @@ def qr_checkin(qr_url): response_data['data']['coordinates'] = f"{location_data['latitude']:.10f}, {location_data['longitude']:.10f}" # Enhanced logging for successful check-in with all details - print(f"โœ… Check-in completed successfully") - print(f" Employee ID: {attendance.employee_id}") - print(f" Time: {attendance.check_in_time.strftime('%I:%M %p')}") - print(f" Date: {attendance.check_in_date.strftime('%B %d, %Y')}") - print(f" Location: {attendance.location_name}") - print(f" Action: {qr_code.location_event}") - print(f" Address: {attendance.address}") - print(f" Today's count: {today_checkin_count}") + logger_handler.logger.info( + f"Check-in completed: employee={attendance.employee_id}, " + f"action={qr_code.location_event}, location={attendance.location_name}, " + f"time={attendance.check_in_time.strftime('%H:%M')}, count_today={today_checkin_count}" + ) # Log to database for audit trail logger_handler.logger.info(f"Check-in success - Employee: {attendance.employee_id}, Location: {attendance.location_name}, Time: {attendance.check_in_time}, Action: {qr_code.location_event}") @@ -894,8 +857,8 @@ def qr_checkin(qr_url): return jsonify(response_data), 200 except Exception as e: - print(f"โŒ Unexpected error in check-in process: {e}") - print(f"โŒ Traceback: {traceback.format_exc()}") + db.session.rollback() + logger_handler.logger.error(f"Unexpected error in check-in process (QR: {qr_url}): {e}", exc_info=True) return jsonify({ 'success': False, @@ -925,7 +888,7 @@ def toggle_qr_status(qr_id): except Exception as e: db.session.rollback() - print(f"Error toggling QR status: {e}") + logger_handler.logger.error(f"Error toggling QR status (ID: {qr_id}): {e}", exc_info=True) return jsonify({ 'success': False, 'message': 'Error updating QR code status. Please try again.' @@ -996,7 +959,7 @@ def activate_qr_code(qr_id): except Exception as e: db.session.rollback() - print(f"Error activating QR code: {e}") + logger_handler.logger.error(f"Error activating QR code (ID: {qr_id}): {e}", exc_info=True) return jsonify({ 'success': False, 'message': 'Error activating QR code. Please try again.' @@ -1021,7 +984,7 @@ def deactivate_qr_code(qr_id): except Exception as e: db.session.rollback() - print(f"Error deactivating QR code: {e}") + logger_handler.logger.error(f"Error deactivating QR code (ID: {qr_id}): {e}", exc_info=True) return jsonify({ 'success': False, 'message': 'Error deactivating QR code. Please try again.' diff --git a/routes/statistics.py b/routes/statistics.py index 3c30db2..5e99208 100644 --- a/routes/statistics.py +++ b/routes/statistics.py @@ -202,9 +202,6 @@ def qr_statistics(): except Exception as e: # Log the error using the correct method logger_handler.log_database_error('statistics_page_error', e) - print(f"โŒ Error loading statistics: {e}") - print(f"โŒ Traceback: {traceback.format_exc()}") - flash('Error loading statistics. Please try again.', 'error') return redirect(url_for('dashboard.dashboard')) @@ -297,15 +294,11 @@ def export_statistics(): except Exception as e: logger_handler.log_database_error('statistics_export_error', e) - print(f"โŒ Error exporting statistics: {e}") return jsonify({'error': 'Export failed'}), 500 except Exception as e: # Log the error logger_handler.log_database_error('statistics_page_error', e) - print(f"โŒ Error loading statistics: {e}") - print(f"โŒ Traceback: {traceback.format_exc()}") - flash('Error loading statistics. Please try again.', 'error') return redirect(url_for('dashboard.dashboard')) diff --git a/routes/time_attendance.py b/routes/time_attendance.py index 5b8af49..74074db 100644 --- a/routes/time_attendance.py +++ b/routes/time_attendance.py @@ -104,7 +104,7 @@ def time_attendance_dashboard(): pass except Exception as e: # Database table doesn't exist yet or other error - use defaults - print(f"TimeAttendance query error: {e}") + logger_handler.logger.error(f"TimeAttendance query error: {e}", exc_info=True) pass return render_template('time_attendance_dashboard.html', @@ -137,9 +137,10 @@ def import_time_attendance(): coming_from_invalid_review = request.form.get('from_invalid_review', 'false').lower() == 'true' coming_from_duplicate_review = request.form.get('from_duplicate_review', 'false').lower() == 'true' - print(f"\n๐Ÿ” IMPORT FLOW DEBUG:") - print(f" Coming from invalid review: {coming_from_invalid_review}") - print(f" Coming from duplicate review: {coming_from_duplicate_review}") + logger_handler.logger.debug( + f"Import flow: coming_from_invalid={coming_from_invalid_review}, " + f"coming_from_duplicate={coming_from_duplicate_review}" + ) if coming_from_invalid_review or coming_from_duplicate_review: # Retrieve file from session @@ -157,8 +158,7 @@ def import_time_attendance(): session.pop('pending_import_filename', None) return redirect(url_for('time_attendance.import_time_attendance')) - print(f"โœ… Retrieved file from session: {filename}") - print(f"โœ… Temp path exists: {os.path.exists(temp_path)}") + logger_handler.logger.debug(f"Retrieved file from session: {filename}, exists={os.path.exists(temp_path)}") else: # Normal file upload flow - now supports multiple files @@ -199,8 +199,7 @@ def import_time_attendance(): temp_paths.append(temp_path) filenames.append(filename) - print(f"โœ… Uploaded file {len(temp_paths)}: {filename}") - print(f"โœ… Saved to: {temp_path}") + logger_handler.logger.debug(f"Uploaded file {len(temp_paths)}: {filename} saved to {temp_path}") if len(temp_paths) == 0: flash('No valid files selected.', 'error') @@ -214,7 +213,7 @@ def import_time_attendance(): temp_path = temp_paths[0] if len(temp_paths) == 1 else temp_paths filename = filenames[0] if len(filenames) == 1 else ', '.join(filenames) - print(f"โœ… Total files uploaded: {len(temp_paths)}") + logger_handler.logger.debug(f"Total files uploaded: {len(temp_paths)}") # Determine if we're processing multiple files is_multiple_files = session.get('pending_import_files_multiple', False) @@ -230,7 +229,7 @@ def import_time_attendance(): # Single file mode (existing behavior) files_to_process = [(temp_path, filename)] - print(f"๐Ÿ“ Processing {len(files_to_process)} file(s)") + logger_handler.logger.info(f"Processing {len(files_to_process)} file(s) for time attendance import") try: import_service = TimeAttendanceImportService(db, logger_handler) @@ -241,12 +240,11 @@ def import_time_attendance(): analyze_duplicates = request.form.get('analyze_duplicates', 'false').lower() == 'true' analyze_invalid = request.form.get('analyze_invalid', 'false').lower() == 'true' - print(f"๐Ÿ“‹ Import Options:") - print(f" Skip duplicates: {skip_duplicates}") - print(f" Validate only: {validate_only}") - print(f" Analyze duplicates: {analyze_duplicates}") - print(f" Analyze invalid: {analyze_invalid}") - print(f" Coming from invalid review: {coming_from_invalid_review}") + logger_handler.logger.debug( + f"Import options: skip_duplicates={skip_duplicates}, validate_only={validate_only}, " + f"analyze_duplicates={analyze_duplicates}, analyze_invalid={analyze_invalid}, " + f"coming_from_invalid={coming_from_invalid_review}" + ) # Store combined results for multiple files all_results = { @@ -263,14 +261,14 @@ def import_time_attendance(): # Process each file for file_index, (current_temp_path, current_filename) in enumerate(files_to_process, 1): - print(f"\n๐Ÿ“„ Processing file {file_index}/{len(files_to_process)}: {current_filename}") + logger_handler.logger.info(f"Processing file {file_index}/{len(files_to_process)}: {current_filename}") import_result = None # Initialize to prevent reference errors try: # For multiple files, skip review screens and import directly if is_multiple_files: - print(f" ๐Ÿ“ฆ Batch mode: processing directly without review screens") + logger_handler.logger.debug("Batch mode: processing directly without review screens") # Validate the file first validation_result = import_service.validate_excel_file(current_temp_path) @@ -309,7 +307,7 @@ def import_time_attendance(): 'imported': import_result.get('imported_records', 0), 'batch_id': import_result.get('batch_id', '') }) - print(f" โœ… Imported {import_result.get('imported_records', 0)} records") + logger_handler.logger.info(f"Imported {import_result.get('imported_records', 0)} records from {current_filename}") elif import_result: # Import ran but failed all_results['failed_files'] += 1 @@ -322,7 +320,7 @@ def import_time_attendance(): }) except Exception as file_error: - print(f"โŒ Error processing file {current_filename}: {file_error}") + logger_handler.logger.error(f"Error processing file {current_filename}: {file_error}", exc_info=True) logger_handler.logger.error(f"Error processing file {current_filename}: {file_error}") all_results['failed_files'] += 1 all_results['errors'].append(f"{current_filename}: {str(file_error)}") @@ -338,9 +336,9 @@ def import_time_attendance(): if is_multiple_files and os.path.exists(current_temp_path): try: os.remove(current_temp_path) - print(f" ๐Ÿ—‘๏ธ Cleaned up temp file") + logger_handler.logger.debug(f"Cleaned up temp file: {current_temp_path}") except Exception as cleanup_error: - print(f" โš ๏ธ Failed to cleanup temp file: {cleanup_error}") + logger_handler.logger.warning(f"Failed to cleanup temp file: {cleanup_error}") # After processing all files if is_multiple_files: @@ -374,12 +372,11 @@ def import_time_attendance(): session.pop('pending_import_filename', None) session.pop('pending_import_files_multiple', None) - print(f"\n๐Ÿ“Š Batch Import Summary:") - print(f" Total files: {all_results['total_files']}") - print(f" Successful: {all_results['successful_files']}") - print(f" Failed: {all_results['failed_files']}") - print(f" Total imported: {all_results['total_imported']}") - print(f" Total duplicates: {all_results['total_duplicates']}") + logger_handler.logger.info( + f"Batch import summary: files={all_results['total_files']}, " + f"successful={all_results['successful_files']}, failed={all_results['failed_files']}, " + f"imported={all_results['total_imported']}, duplicates={all_results['total_duplicates']}" + ) return redirect(url_for('time_attendance.time_attendance_dashboard')) @@ -388,50 +385,66 @@ def import_time_attendance(): # If analyzing for duplicates, show review page (but not if coming from invalid/duplicate review) if analyze_duplicates and not force_import_hashes and not coming_from_invalid_review and not coming_from_duplicate_review: - print("๐Ÿ” Analyzing for duplicates...") - duplicate_analysis = import_service.analyze_for_duplicates(temp_path) - + logger_handler.logger.debug("Analyzing for duplicates") + project_id_for_analysis = request.form.get('project_id') + project_id_for_analysis = int(project_id_for_analysis) if project_id_for_analysis and project_id_for_analysis != '' else None + duplicate_analysis = import_service.analyze_for_duplicates(temp_path, project_id=project_id_for_analysis) + + # Surface project-mismatch errors immediately + if duplicate_analysis.get('errors'): + for err in duplicate_analysis['errors']: + flash(err, 'error') + projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() + return render_template('time_attendance_import.html', projects=projects) + if duplicate_analysis['duplicate_records'] > 0: - print(f"โš ๏ธ Found {duplicate_analysis['duplicate_records']} duplicates") - # Get project_id from form - project_id = request.form.get('project_id') + logger_handler.logger.info(f"Found {duplicate_analysis['duplicate_records']} duplicates") # Show duplicate review page return render_template('time_attendance_duplicate_review.html', analysis=duplicate_analysis, filename=filename, - project_id=project_id) + project_id=project_id_for_analysis) else: - print("โœ… No duplicates found") + logger_handler.logger.debug("No duplicates found") flash('No duplicates found. Proceeding with import.', 'info') # Check for invalid rows and show review if any (but not if coming from invalid review) if analyze_invalid and not coming_from_invalid_review: - print("๐Ÿ” Analyzing for invalid rows...") - invalid_analysis = import_service.analyze_for_invalid_rows(temp_path) - + logger_handler.logger.debug("Analyzing for invalid rows") + project_id_for_analysis = request.form.get('project_id') + project_id_for_analysis = int(project_id_for_analysis) if project_id_for_analysis and project_id_for_analysis != '' else None + invalid_analysis = import_service.analyze_for_invalid_rows(temp_path, project_id=project_id_for_analysis) + + # Surface project-mismatch errors immediately + if invalid_analysis.get('errors'): + for err in invalid_analysis['errors']: + flash(err, 'error') + projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() + return render_template('time_attendance_import.html', projects=projects) + if invalid_analysis['invalid_rows'] > 0: - print(f"โš ๏ธ Found {invalid_analysis['invalid_rows']} invalid rows") - # Get project_id from form - project_id = request.form.get('project_id') + logger_handler.logger.info(f"Found {invalid_analysis['invalid_rows']} invalid rows") # Show invalid row review page return render_template('time_attendance_invalid_review.html', analysis=invalid_analysis, filename=filename, - project_id=project_id) + project_id=project_id_for_analysis) else: - print("โœ… All rows are valid") + logger_handler.logger.debug("All rows are valid") flash('All rows are valid. Proceeding with import.', 'info') # If coming from invalid review, skip validation (already done) if not coming_from_invalid_review: - print("๐Ÿ” Validating file...") + logger_handler.logger.debug("Validating file") # Validate file validation_result = import_service.validate_excel_file(temp_path) if not validation_result['valid']: - print(f"โŒ Validation failed: {validation_result['errors']}") + logger_handler.logger.warning(f"Validation failed: {validation_result['errors']}") flash(f"File validation failed: {'; '.join(validation_result['errors'])}", 'error') - return render_template('time_attendance_import.html', + projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() + return render_template('time_attendance_import.html', + projects=projects, validation_result=validation_result) if validation_result['warnings']: @@ -439,15 +452,17 @@ def import_time_attendance(): flash(warning, 'warning') if validate_only: - print(f"โœ… Validation successful: {validation_result['valid_rows']} valid records") + logger_handler.logger.info(f"Validation successful: {validation_result['valid_rows']} valid records") flash(f"File validation successful! Found {validation_result['valid_rows']} valid records.", 'success') - return render_template('time_attendance_import.html', + projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() + return render_template('time_attendance_import.html', + projects=projects, validation_result=validation_result) else: - print("โญ๏ธ Skipping validation (already validated)") + logger_handler.logger.debug("Skipping validation (already validated)") # Proceed with import - print("๐Ÿš€ Starting import process...") + logger_handler.logger.info("Starting import process") import_source = request.form.get('import_source', f"Manual Import - {filename}") project_id = request.form.get('project_id') project_id = int(project_id) if project_id and project_id != '' else None @@ -462,11 +477,11 @@ def import_time_attendance(): ) if import_result['success']: - print(f"โœ… Import successful!") - print(f" Batch ID: {import_result['batch_id']}") - print(f" Imported: {import_result['imported_records']}/{import_result['total_records']}") - print(f" Duplicates: {import_result['duplicate_records']}") - print(f" Failed: {import_result['failed_records']}") + logger_handler.logger.info( + f"Import successful: batch_id={import_result['batch_id']}, " + f"imported={import_result['imported_records']}/{import_result['total_records']}, " + f"duplicates={import_result['duplicate_records']}, failed={import_result['failed_records']}" + ) logger_handler.logger.info( f"User {session['username']} successfully imported time attendance data - " @@ -496,36 +511,36 @@ def import_time_attendance(): os.remove(temp_path) session.pop('pending_import_file', None) session.pop('pending_import_filename', None) - print("๐Ÿ—‘๏ธ Cleaned up temp file") + logger_handler.logger.debug(f"Cleaned up temp file: {temp_path}") except Exception as cleanup_error: - print(f"โš ๏ธ Failed to cleanup temp file: {cleanup_error}") + logger_handler.logger.warning(f"Failed to cleanup temp file: {cleanup_error}") return render_template('time_attendance_import_result.html', import_result=import_result) else: - print(f"โŒ Import failed: {import_result['errors']}") + logger_handler.logger.error(f"Import failed: {import_result['errors']}") flash(f"Import failed: {'; '.join(import_result['errors'][:3])}", 'error') if len(import_result['errors']) > 3: flash(f"...and {len(import_result['errors']) - 3} more errors", 'warning') - return render_template('time_attendance_import.html', + projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() + return render_template('time_attendance_import.html', + projects=projects, import_result=import_result) except Exception as import_error: - print(f"โŒ Import exception: {import_error}") - import traceback - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Import exception: {import_error}", exc_info=True) raise except Exception as e: logger_handler.log_database_error('time_attendance_import', e) - print(f"โŒ Top-level exception: {e}") - import traceback - print(f"โŒ Traceback: {traceback.format_exc()}") + logger_handler.logger.error(f"Top-level import exception: {e}", exc_info=True) flash('Import failed due to an unexpected error.', 'error') - return render_template('time_attendance_import.html') + projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() + return render_template('time_attendance_import.html', projects=projects) # GET request - return render_template('time_attendance_import.html') + projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() + return render_template('time_attendance_import.html', projects=projects) @bp.route('/time-attendance/import/analyze-duplicates', methods=['POST'], endpoint='analyze_import_duplicates') @@ -1198,13 +1213,13 @@ def export_time_attendance(): project_name_for_filename = '' if project_filter: try: - project = Project.query.get(int(project_filter)) + project = db.session.get(Project, int(project_filter)) if project: # Replace spaces and special characters with underscores project_name_safe = project.name.replace(' ', '_').replace('/', '_').replace('\\', '_') project_name_for_filename = f"{project_name_safe}_" except Exception as e: - print(f"โš ๏ธ Error getting project name for filename: {e}") + logger_handler.logger.warning(f"Error getting project name for filename: {e}") # Log export logger_handler.logger.info( @@ -1343,13 +1358,13 @@ def export_time_attendance_by_building(): project_name_for_filename = '' if project_filter: try: - project = Project.query.get(int(project_filter)) + project = db.session.get(Project, int(project_filter)) if project: # Replace spaces and special characters with underscores project_name_safe = project.name.replace(' ', '_').replace('/', '_').replace('\\', '_') project_name_for_filename = f"{project_name_safe}_" except Exception as e: - print(f"โš ๏ธ Error getting project name for filename: {e}") + logger_handler.logger.warning(f"Error getting project name for filename: {e}") # Log export logger_handler.logger.info( diff --git a/routes/time_attendance_export.py b/routes/time_attendance_export.py index cd06463..ed85fd9 100644 --- a/routes/time_attendance_export.py +++ b/routes/time_attendance_export.py @@ -120,6 +120,180 @@ def _qtr(decimal_hours: float) -> float: base100 = convert_minutes_to_base100(rounded_minutes) return round_base100_hours(base100) +# --------------------------------------------------------------------------- +# Private export helpers โ€” shared by both export functions below. +# --------------------------------------------------------------------------- + +def _resolve_date_range(start_date_filter, end_date_filter, records, export_label='TA Excel export'): + """ + Resolve and validate the export date range. + + Returns (start_date, end_date, filtered_records) where: + - start_date / end_date are date objects + - filtered_records is the input list capped to end_date + 1 day (overnight buffer) + - end_date is capped to a maximum 14-day window + Returns None when there are no records and no date filters. + """ + MAX_EXPORT_DAYS = 14 + + if start_date_filter and end_date_filter: + if isinstance(start_date_filter, str): + start_date = datetime.strptime(start_date_filter, '%Y-%m-%d').date() + else: + start_date = start_date_filter + if isinstance(end_date_filter, str): + end_date = datetime.strptime(end_date_filter, '%Y-%m-%d').date() + else: + end_date = end_date_filter + elif records: + start_date = min(r.attendance_date for r in records) + end_date = max(r.attendance_date for r in records) + else: + return None + + if (end_date - start_date).days >= MAX_EXPORT_DAYS: + capped_end_date = start_date + timedelta(days=MAX_EXPORT_DAYS - 1) + logger_handler.logger.info( + f"{export_label}: date range [{start_date} \u2013 {end_date}] exceeds " + f"{MAX_EXPORT_DAYS} days; capping end_date to {capped_end_date}." + ) + end_date = capped_end_date + + # Preserve one extra calendar day so early-morning check-out records + # stored on Day N+1 remain available for overnight pairing detection. + # Display range is still controlled by dates_with_records (capped to end_date). + filtered_records = [r for r in records if r.attendance_date <= end_date + timedelta(days=1)] + + return start_date, end_date, filtered_records + + +def _convert_ta_records(records): + """ + Convert TimeAttendance ORM records to the lightweight anonymous-class + format expected by WorkingHoursCalculator and the Excel rendering loops. + + Returns a list of converted record objects. + """ + from working_hours_calculator import parse_employee_id_for_work_type + + converted = [] + for record in records: + distance_value = getattr(record, 'distance', None) + + record_type = 'check_in' + if hasattr(record, 'action_description') and record.action_description: + action_lower = record.action_description.lower() + if 'out' in action_lower or 'checkout' in action_lower: + record_type = 'check_out' + + _, work_type = parse_employee_id_for_work_type(str(record.employee_id)) + + base_location_name = record.location_name + if work_type and work_type in ('PT', 'SP', 'PW'): + display_location_name = f"{base_location_name} ({work_type})" + else: + display_location_name = base_location_name + + converted_record = type('Record', (), { + 'id': record.id, + 'employee_id': str(record.employee_id), + 'employee_name': getattr(record, 'employee_name', ''), + 'check_in_date': record.attendance_date, + 'check_in_time': record.attendance_time, + 'location_name': display_location_name, + 'original_location_name': base_location_name, + 'work_type': work_type, + 'latitude': None, + 'longitude': None, + 'distance': distance_value, + 'record_type': record_type, + 'action_description': record.action_description, + 'event_description': record.event_description or '', + 'recorded_address': record.recorded_address or '', + 'qr_code': type('QRCode', (), { + 'location': base_location_name, + 'location_address': record.recorded_address or '', + 'project': None + })() + })() + converted.append(converted_record) + + return converted + + +def _build_employee_name_map(records): + """ + Build a {base_employee_id: "Lastname, Firstname"} map for export headers. + + Looks up the Employee table by numeric base ID so work-type suffixes + (e.g. '3937SP') in the stored employee_name column do not pollute labels. + Falls back to the stored employee_name on lookup failure. + """ + from working_hours_calculator import parse_employee_id_for_work_type + + employee_names = {} + for record in records: + base_id, _ = parse_employee_id_for_work_type(str(record.employee_id)) + if base_id not in employee_names: + try: + emp = Employee.query.filter_by(id=int(base_id)).first() + if emp: + employee_names[base_id] = f"{emp.lastName}, {emp.firstName}" + else: + employee_names[base_id] = getattr(record, 'employee_name', f'Employee {base_id}') + logger_handler.logger.warning( + f"Employee ID {base_id} not found in employee table during export; " + f"using stored name." + ) + except Exception as e: + employee_names[base_id] = getattr(record, 'employee_name', f'Employee {base_id}') + logger_handler.logger.warning( + f"Could not lookup employee name for ID {base_id} during export: {e}" + ) + return employee_names + + +def _make_export_styles(): + """ + Return a dict of openpyxl style objects shared by both export functions. + + Keys: header_font, header_fill, data_font, bold_font, italic_bold_font, + border, missed_punch_fill, border_day_middle, border_day_last, + border_day_single, border_day_first, amber_fill + """ + header_font = Font(name='Aptos Narrow', size=11, bold=True, color='FFFFFF') + header_fill = PatternFill(start_color='000000', end_color='000000', fill_type='solid') + data_font = Font(name='Aptos Narrow', size=11) + bold_font = Font(name='Aptos Narrow', size=11, bold=True) + italic_bold_font = Font(name='Aptos Narrow', size=11, bold=True, italic=True) + border = Border( + left=Side(style='thin'), + right=Side(style='thin'), + top=Side(style='thin'), + bottom=Side(style='thin') + ) + missed_punch_fill = PatternFill(start_color='FFC000', end_color='FFC000', fill_type='solid') + amber_fill = PatternFill(start_color='FFC000', end_color='FFC000', fill_type='solid') + border_day_middle = Border() + border_day_last = Border(bottom=Side(style='thin')) + border_day_single = Border(bottom=Side(style='thin')) + border_day_first = Border() + return { + 'header_font': header_font, + 'header_fill': header_fill, + 'data_font': data_font, + 'bold_font': bold_font, + 'italic_bold_font': italic_bold_font, + 'border': border, + 'missed_punch_fill': missed_punch_fill, + 'amber_fill': amber_fill, + 'border_day_middle': border_day_middle, + 'border_day_last': border_day_last, + 'border_day_single': border_day_single, + 'border_day_first': border_day_first, + } + + def export_time_attendance_excel(records, project_name_for_filename, date_range_str, filter_str, start_date_filter=None, end_date_filter=None): """Generate Excel export with template format matching the provided template""" from openpyxl import Workbook @@ -132,105 +306,17 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ ws = wb.active ws.title = "Sheet0" - # Get date range for calculations - if start_date_filter and end_date_filter: - # Convert string dates to date objects if needed - if isinstance(start_date_filter, str): - start_date = datetime.strptime(start_date_filter, '%Y-%m-%d').date() - else: - start_date = start_date_filter - - if isinstance(end_date_filter, str): - end_date = datetime.strptime(end_date_filter, '%Y-%m-%d').date() - else: - end_date = end_date_filter - elif records: - # Fallback to calculating from records if no filter dates provided - start_date = min(r.attendance_date for r in records) - end_date = max(r.attendance_date for r in records) - else: + # Resolve date range and cap to 14-day window + result = _resolve_date_range(start_date_filter, end_date_filter, records, 'TA Excel export') + if result is None: return None - - # Enforce maximum 2-week (14-day) export window. - # If the selected range exceeds 14 days, cap end_date to start_date + 13 days. - MAX_EXPORT_DAYS = 14 - if (end_date - start_date).days >= MAX_EXPORT_DAYS: - capped_end_date = start_date + timedelta(days=MAX_EXPORT_DAYS - 1) - logger_handler.logger.info( - f"TA Excel export: date range [{start_date} โ€“ {end_date}] exceeds {MAX_EXPORT_DAYS} days; " - f"capping end_date to {capped_end_date}." - ) - end_date = capped_end_date - # Drop records that fall outside the capped window - # Preserve one extra calendar day so early-morning check-out records - # stored on Day N+1 remain available for overnight pairing detection. - # Display range is still controlled by dates_with_records (capped to end_date). - records = [r for r in records if r.attendance_date <= end_date + timedelta(days=1)] + start_date, end_date, records = result # Import parse function at the beginning for work type detection from working_hours_calculator import parse_employee_id_for_work_type # Convert TimeAttendance records to format expected by calculator - converted_records = [] - for record in records: - # Get distance value from the record - distance_value = getattr(record, 'distance', None) - - # CRITICAL: Determine record_type from action_description - record_type = 'check_in' # Default - if hasattr(record, 'action_description') and record.action_description: - action_lower = record.action_description.lower() - if 'out' in action_lower or 'checkout' in action_lower: - record_type = 'check_out' - - # Extract work type (PT, SP, PW) from employee_id for location display in Excel - _, work_type = parse_employee_id_for_work_type(str(record.employee_id)) - - # Create display location name with work type suffix if applicable - base_location_name = record.location_name - if work_type and work_type in ('PT', 'SP', 'PW'): - display_location_name = f"{base_location_name} ({work_type})" - else: - display_location_name = base_location_name - - converted_record = type('Record', (), { - 'id': record.id, - 'employee_id': str(record.employee_id), - 'check_in_date': record.attendance_date, - 'check_in_time': record.attendance_time, - 'location_name': display_location_name, # Use display name with work type for Excel export - 'original_location_name': base_location_name, # Keep original for internal grouping - 'work_type': work_type, # Store work type for reference - 'latitude': None, - 'longitude': None, - 'distance': distance_value, - 'record_type': record_type, - 'action_description': record.action_description, - 'event_description': record.event_description or '', - 'recorded_address': record.recorded_address or '', - 'qr_code': type('QRCode', (), { - 'location': base_location_name, # Keep original for QR code matching - 'location_address': record.recorded_address or '', - 'project': None - })() - })() - converted_records.append(converted_record) - - # Log count of records with work types for audit trail - work_type_counts = {'PT': 0, 'SP': 0, 'PW': 0, 'Regular': 0} - for r in converted_records: - wt = getattr(r, 'work_type', None) - if wt in work_type_counts: - work_type_counts[wt] += 1 - else: - work_type_counts['Regular'] += 1 - - if any(work_type_counts[wt] > 0 for wt in ['PT', 'SP', 'PW']): - logger_handler.logger.info( - f"Excel Export: Processing records with work types - " - f"Regular: {work_type_counts['Regular']}, PT: {work_type_counts['PT']}, " - f"SP: {work_type_counts['SP']}, PW: {work_type_counts['PW']}" - ) + converted_records = _convert_ta_records(records) # Calculate working hours using WorkingHoursCalculator calculator = WorkingHoursCalculator() @@ -240,53 +326,23 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ converted_records ) - # Get employee names - map BASE employee IDs to names for consolidated display - # Look up from Employee table using the numeric base_id to get the correct name, - # regardless of what is stored in the employee_name column (which may contain - # work type characters such as 'Employee 3937SP' if imported with a decorated ID). - from working_hours_calculator import parse_employee_id_for_work_type - employee_names = {} - for record in records: - base_id, _ = parse_employee_id_for_work_type(str(record.employee_id)) - if base_id not in employee_names: - try: - emp = Employee.query.filter_by(id=int(base_id)).first() - if emp: - employee_names[base_id] = f"{emp.lastName}, {emp.firstName}" - else: - # Fallback: use stored name if Employee table lookup fails - employee_names[base_id] = record.employee_name - logger_handler.logger.warning(f"Employee ID {base_id} not found in employee table during export; using stored name.") - except Exception as e: - employee_names[base_id] = record.employee_name - logger_handler.logger.warning(f"Could not lookup employee name for ID {base_id} during export: {e}") + # Build employee name map (Lastname, Firstname keyed by base employee ID) + employee_names = _build_employee_name_map(records) - # Setup styles - # White bold text on black background for column header row (no border) - header_font = Font(name='Aptos Narrow', size=11, bold=True, color='FFFFFF') - header_fill = PatternFill(start_color='000000', end_color='000000', fill_type='solid') - data_font = Font(name='Aptos Narrow', size=11) - bold_font = Font(name='Aptos Narrow', size=11, bold=True) - border = Border( - left=Side(style='thin'), - right=Side(style='thin'), - top=Side(style='thin'), - bottom=Side(style='thin') - ) - # CHANGED: Sample format uses ONLY a bottom border on the last row of each day group. - # Intermediate rows and first rows have no borders at all (no left/right/top). - border_day_middle = Border() # No borders on intermediate rows - - border_day_last = Border( - bottom=Side(style='thin') # Only bottom border on the last row of a day group - ) - - border_day_single = Border( - bottom=Side(style='thin') # Single-row days also get only bottom border - ) - - # border_day_first is same as middle (no borders) โ€” kept for compatibility - border_day_first = Border() + # Setup styles (shared objects) + _styles = _make_export_styles() + header_font = _styles['header_font'] + header_fill = _styles['header_fill'] + data_font = _styles['data_font'] + bold_font = _styles['bold_font'] + italic_bold_font = _styles['italic_bold_font'] + border = _styles['border'] + missed_punch_fill = _styles['missed_punch_fill'] + amber_fill = _styles['amber_fill'] + border_day_middle = _styles['border_day_middle'] + border_day_last = _styles['border_day_last'] + border_day_single = _styles['border_day_single'] + border_day_first = _styles['border_day_first'] def get_day_border(row_position, total_rows): """ @@ -511,8 +567,10 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ del daily_location_data[_ndk][_co_loc] if _ndk in daily_location_data and not daily_location_data[_ndk]: del daily_location_data[_ndk] - print(f"\U0001f319 [TA Export] Overnight: moved checkout {_co.check_in_time} " - f"from {_ndk} to {_dk} for employee {employee_id}") + logger_handler.logger.info( + f"TA Export overnight shift: moved checkout {_co.check_in_time} " + f"from {_ndk} to {_dk} for employee {employee_id}" + ) # ------------------------------------------------------------------- # END OVERNIGHT SHIFT DETECTION # ------------------------------------------------------------------- @@ -855,7 +913,7 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ 'work_type': wt, # None means regular 'used': False }) - print(f" Record at {record.check_in_time}: action='{record.action_description}', is_out={is_out}, work_type={wt}") + logger_handler.logger.debug(f"TA Export record: time={record.check_in_time}, action='{record.action_description}', is_out={is_out}, work_type={wt}") ins = [ri for ri in record_info if not ri['is_out']] outs = [ri for ri in record_info if ri['is_out']] @@ -975,7 +1033,7 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ 'effective_work_type': ri['work_type'] }) - print(f" Created {len(pairs_to_write)} pairs") + logger_handler.logger.debug(f"TA Export: created {len(pairs_to_write)} pairs for export") # Sort pairs chronologically by the anchor record's time so that # orphaned records (assembled last in Steps 2-3) appear in the @@ -1300,82 +1358,16 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename, ws = wb.active ws.title = "Sheet0" - # Get date range for calculations - if start_date_filter and end_date_filter: - if isinstance(start_date_filter, str): - start_date = datetime.strptime(start_date_filter, '%Y-%m-%d').date() - else: - start_date = start_date_filter - - if isinstance(end_date_filter, str): - end_date = datetime.strptime(end_date_filter, '%Y-%m-%d').date() - else: - end_date = end_date_filter - elif records: - start_date = min(r.attendance_date for r in records) - end_date = max(r.attendance_date for r in records) - else: + # Resolve date range and cap to 14-day window + result = _resolve_date_range(start_date_filter, end_date_filter, records, 'TA by-building Excel export') + if result is None: return None + start_date, end_date, records = result - # Enforce maximum 2-week (14-day) export window. - MAX_EXPORT_DAYS = 14 - if (end_date - start_date).days >= MAX_EXPORT_DAYS: - capped_end_date = start_date + timedelta(days=MAX_EXPORT_DAYS - 1) - logger_handler.logger.info( - f"TA by-building Excel export: date range [{start_date} โ€“ {end_date}] exceeds {MAX_EXPORT_DAYS} days; " - f"capping end_date to {capped_end_date}." - ) - end_date = capped_end_date - # Preserve one extra calendar day so early-morning check-out records - # stored on Day N+1 remain available for overnight pairing detection. - # Display range is still controlled by dates_with_records (capped to end_date). - records = [r for r in records if r.attendance_date <= end_date + timedelta(days=1)] - - # Import parse function for work type detection from working_hours_calculator import parse_employee_id_for_work_type # Convert TimeAttendance records to format expected by calculator - converted_records = [] - for record in records: - distance_value = getattr(record, 'distance', None) - - record_type = 'check_in' - if hasattr(record, 'action_description') and record.action_description: - action_lower = record.action_description.lower() - if 'out' in action_lower or 'checkout' in action_lower: - record_type = 'check_out' - - _, work_type = parse_employee_id_for_work_type(str(record.employee_id)) - - base_location_name = record.location_name - if work_type and work_type in ('PT', 'SP', 'PW'): - display_location_name = f"{base_location_name} ({work_type})" - else: - display_location_name = base_location_name - - converted_record = type('Record', (), { - 'id': record.id, - 'employee_id': str(record.employee_id), - 'employee_name': record.employee_name, - 'check_in_date': record.attendance_date, - 'check_in_time': record.attendance_time, - 'location_name': display_location_name, - 'original_location_name': base_location_name, - 'work_type': work_type, - 'latitude': None, - 'longitude': None, - 'distance': distance_value, - 'record_type': record_type, - 'action_description': record.action_description, - 'event_description': record.event_description or '', - 'recorded_address': record.recorded_address or '', - 'qr_code': type('QRCode', (), { - 'location': base_location_name, - 'location_address': record.recorded_address or '', - 'project': None - })() - })() - converted_records.append(converted_record) + converted_records = _convert_ta_records(records) # Group records by location (building) location_groups = {} @@ -1401,43 +1393,21 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename, converted_records ) - # Get employee names map - # Look up from Employee table using the numeric base_id to get the correct name, - # regardless of what is stored in the employee_name column (which may contain - # work type characters such as 'Employee 3937SP' if imported with a decorated ID). - employee_names = {} - for record in records: - base_id, _ = parse_employee_id_for_work_type(str(record.employee_id)) - if base_id not in employee_names: - try: - emp = Employee.query.filter_by(id=int(base_id)).first() - if emp: - employee_names[base_id] = f"{emp.lastName}, {emp.firstName}" - else: - # Fallback: use stored name if Employee table lookup fails - employee_names[base_id] = record.employee_name - logger_handler.logger.warning(f"Employee ID {base_id} not found in employee table during export (by-building); using stored name.") - except Exception as e: - employee_names[base_id] = record.employee_name - logger_handler.logger.warning(f"Could not lookup employee name for ID {base_id} during export (by-building): {e}") + # Build employee name map (Lastname, Firstname keyed by base employee ID) + employee_names = _build_employee_name_map(records) - # Setup styles - header_font = Font(name='Aptos Narrow', size=11, bold=True, color='FFFFFF') - header_fill = PatternFill(start_color='000000', end_color='000000', fill_type='solid') - data_font = Font(name='Aptos Narrow', size=11) - bold_font = Font(name='Aptos Narrow', size=11, bold=True) - italic_bold_font = Font(name='Aptos Narrow', size=11, bold=True, italic=True) - border = Border( - left=Side(style='thin'), - right=Side(style='thin'), - top=Side(style='thin'), - bottom=Side(style='thin') - ) - missed_punch_fill = PatternFill(start_color='FFC000', end_color='FFC000', fill_type='solid') - # Bottom-only border on the last row of each day group (matches normal TA export). - # Intermediate rows within a day have no borders. - border_day_middle = Border() # No borders on intermediate rows - border_day_last = Border(bottom=Side(style='thin')) # Bottom border on last row of day + # Setup styles (shared objects) + _styles = _make_export_styles() + header_font = _styles['header_font'] + header_fill = _styles['header_fill'] + data_font = _styles['data_font'] + bold_font = _styles['bold_font'] + italic_bold_font = _styles['italic_bold_font'] + border = _styles['border'] + missed_punch_fill = _styles['missed_punch_fill'] + amber_fill = _styles['amber_fill'] + border_day_middle = _styles['border_day_middle'] + border_day_last = _styles['border_day_last'] # Write main headers current_row = 1 diff --git a/routes/users.py b/routes/users.py index 197d0a4..c8355b6 100644 --- a/routes/users.py +++ b/routes/users.py @@ -118,7 +118,7 @@ def create_user(): try: project_id = int(pid) # Verify project exists - if Project.query.get(project_id): + if db.session.get(Project, project_id): valid_project_ids.append(project_id) except (ValueError, TypeError): logger_handler.logger.warning(f"Invalid project ID received: {pid}") @@ -206,8 +206,8 @@ def create_user(): def delete_user(user_id): """Deactivate user (Admin only) - Fixed with proper validation""" try: - user_to_delete = User.query.get(user_id) - current_user = User.query.get(session['user_id']) + user_to_delete = db.session.get(User, user_id) + current_user = db.session.get(User, session['user_id']) if not user_to_delete: flash('User not found.', 'error') @@ -247,8 +247,8 @@ def delete_user(user_id): def reactivate_user(user_id): """Reactivate a deactivated user (Admin only)""" try: - user_to_reactivate = User.query.get(user_id) - current_user = User.query.get(session['user_id']) + user_to_reactivate = db.session.get(User, user_id) + current_user = db.session.get(User, session['user_id']) if not user_to_reactivate: flash('User not found.', 'error') @@ -277,8 +277,8 @@ def reactivate_user(user_id): def promote_user(user_id): """Promote a staff user to admin (Admin only)""" try: - user_to_promote = User.query.get(user_id) - current_user = User.query.get(session['user_id']) + user_to_promote = db.session.get(User, user_id) + current_user = db.session.get(User, session['user_id']) if not user_to_promote: flash('User not found.', 'error') @@ -307,8 +307,8 @@ def promote_user(user_id): def demote_user(user_id): """Demote an admin user to staff (Admin only)""" try: - user_to_demote = User.query.get(user_id) - current_user = User.query.get(session['user_id']) + user_to_demote = db.session.get(User, user_id) + current_user = db.session.get(User, session['user_id']) if not user_to_demote: flash('User not found.', 'error') @@ -437,7 +437,7 @@ def edit_user(user_id): try: project_id = int(pid) # Verify project exists - if Project.query.get(project_id): + if db.session.get(Project, project_id): valid_project_ids.append(project_id) except (ValueError, TypeError): logger_handler.logger.warning(f"Invalid project ID received: {pid}") @@ -561,8 +561,8 @@ def edit_user(user_id): def toggle_user_status(user_id): """Toggle user active status via AJAX (Admin only)""" try: - user_to_toggle = User.query.get(user_id) - current_user = User.query.get(session['user_id']) + user_to_toggle = db.session.get(User, user_id) + current_user = db.session.get(User, session['user_id']) if not user_to_toggle: return jsonify({ @@ -619,8 +619,8 @@ def toggle_user_status(user_id): def activate_user(user_id): """Activate a user (Admin only) - Alternative route""" try: - user_to_activate = User.query.get(user_id) - current_user = User.query.get(session['user_id']) + user_to_activate = db.session.get(User, user_id) + current_user = db.session.get(User, session['user_id']) if not user_to_activate: flash('User not found.', 'error') @@ -652,8 +652,8 @@ def activate_user(user_id): def deactivate_user(user_id): """Deactivate a user (Admin only) - Alternative route""" try: - user_to_deactivate = User.query.get(user_id) - current_user = User.query.get(session['user_id']) + user_to_deactivate = db.session.get(User, user_id) + current_user = db.session.get(User, session['user_id']) if not user_to_deactivate: flash('User not found.', 'error') @@ -904,7 +904,7 @@ def permanently_delete_user(user_id): """Permanently delete user but preserve associated QR codes (Admin only)""" try: user_to_delete = User.query.get_or_404(user_id) - current_user = User.query.get(session['user_id']) + current_user = db.session.get(User, session['user_id']) # Security checks if user_to_delete.id == current_user.id: diff --git a/templates/time_attendance_import.html b/templates/time_attendance_import.html index c252e40..0457bcf 100644 --- a/templates/time_attendance_import.html +++ b/templates/time_attendance_import.html @@ -927,11 +927,17 @@ importForm.addEventListener('submit', async (e) => { const result = msg.result; setProgress(result.total_records, result.total_records, 'Import finished.'); - let summary = 'โœ… Imported ' + result.imported_records + ' of ' + result.total_records + ' records.'; - if (result.duplicate_records > 0) - summary += ' Skipped ' + result.duplicate_records + ' duplicates.'; - if (result.failed_records > 0) - summary += ' โš ๏ธ ' + result.failed_records + ' records failed.'; + let summary; + if (!result.success && result.total_records === 0) { + // Validation failed before any records were processed (e.g. project mismatch) + summary = 'โŒ Import was rejected before processing any records.'; + } else { + summary = (result.success ? 'โœ…' : 'โŒ') + ' Imported ' + result.imported_records + ' of ' + result.total_records + ' records.'; + if (result.duplicate_records > 0) + summary += ' Skipped ' + result.duplicate_records + ' duplicates.'; + if (result.failed_records > 0) + summary += ' โš ๏ธ ' + result.failed_records + ' records failed.'; + } const errors = (!result.success && result.errors && result.errors.length > 0) ? result.errors diff --git a/time_attendance_import_service.py b/time_attendance_import_service.py index 33ee223..e071bd0 100644 --- a/time_attendance_import_service.py +++ b/time_attendance_import_service.py @@ -321,13 +321,16 @@ class TimeAttendanceImportService: self.logger.logger.error(f"Failed to get existing record hashes with data: {e}") return {} - def analyze_for_duplicates(self, file_path: str) -> Dict[str, Any]: + def analyze_for_duplicates(self, file_path: str, project_id: int = None) -> Dict[str, Any]: """ - Analyze file for potential duplicates WITHOUT importing - + Analyze file for potential duplicates WITHOUT importing. + Args: - file_path: Path to the Excel file - + file_path: Path to the Excel file + project_id: Optional project ID โ€” if supplied, every Location Name + in the file is validated against that project's QR code + locations before the duplicate analysis proceeds. + Returns: Dictionary containing duplicate analysis """ @@ -355,7 +358,47 @@ class TimeAttendanceImportService: # Remove empty rows df = df.dropna(how='all') analysis_result['total_records'] = len(df) - + + # โ”€โ”€ Project-location validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if project_id: + try: + from models.qrcode import QRCode + from models.project import Project + file_locations = set( + str(loc).strip() + for loc in df['Location Name'].dropna().unique() + if str(loc).strip() + ) + project_locations = set( + qr.location + for qr in QRCode.query.filter_by(project_id=project_id) + .with_entities(QRCode.location).all() + ) + unmatched = next( + (loc for loc in sorted(file_locations) if loc not in project_locations), + None + ) + if unmatched: + project_obj = self.db.session.get(Project, project_id) + project_name = project_obj.name if project_obj else f'ID {project_id}' + error_msg = ( + f"The data in the file does not belong to the project '{project_name}'. " + f"Please verify the selected project or correct the file." + ) + analysis_result['errors'].append(error_msg) + if self.logger: + self.logger.logger.warning(f"Project-location mismatch during duplicate analysis: {error_msg}") + return analysis_result + if self.logger: + self.logger.logger.info( + f"Project-location validation passed (duplicate analysis): " + f"all {len(file_locations)} location(s) belong to project ID {project_id}." + ) + except Exception as e: + if self.logger: + self.logger.logger.warning(f"Could not perform project-location validation (duplicate analysis): {e}") + # โ”€โ”€ End project-location validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + # Get existing record hashes existing_hashes = self._get_existing_record_hashes_with_data() @@ -445,13 +488,16 @@ class TimeAttendanceImportService: return analysis_result - def analyze_for_invalid_rows(self, file_path: str) -> Dict[str, Any]: + def analyze_for_invalid_rows(self, file_path: str, project_id: int = None) -> Dict[str, Any]: """ - Analyze file for invalid rows with detailed error information - + Analyze file for invalid rows with detailed error information. + Args: - file_path: Path to the Excel file - + file_path: Path to the Excel file + project_id: Optional project ID โ€” if supplied, every Location Name + in the file is validated against that project's QR code + locations before the row analysis proceeds. + Returns: Dictionary containing invalid row analysis """ @@ -479,6 +525,46 @@ class TimeAttendanceImportService: # Remove empty rows df = df.dropna(how='all') analysis_result['total_rows'] = len(df) + + # โ”€โ”€ Project-location validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if project_id: + try: + from models.qrcode import QRCode + from models.project import Project + file_locations = set( + str(loc).strip() + for loc in df['Location Name'].dropna().unique() + if str(loc).strip() + ) + project_locations = set( + qr.location + for qr in QRCode.query.filter_by(project_id=project_id) + .with_entities(QRCode.location).all() + ) + unmatched = next( + (loc for loc in sorted(file_locations) if loc not in project_locations), + None + ) + if unmatched: + project_obj = self.db.session.get(Project, project_id) + project_name = project_obj.name if project_obj else f'ID {project_id}' + error_msg = ( + f"The data in the file does not belong to the project '{project_name}'. " + f"Please verify the selected project or correct the file." + ) + analysis_result['errors'].append(error_msg) + if self.logger: + self.logger.logger.warning(f"Project-location mismatch during invalid-row analysis: {error_msg}") + return analysis_result + if self.logger: + self.logger.logger.info( + f"Project-location validation passed (invalid-row analysis): " + f"all {len(file_locations)} location(s) belong to project ID {project_id}." + ) + except Exception as e: + if self.logger: + self.logger.logger.warning(f"Could not perform project-location validation (invalid-row analysis): {e}") + # โ”€โ”€ End project-location validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # Analyze each row invalid_list = [] @@ -663,13 +749,22 @@ class TimeAttendanceImportService: if str(loc).strip() ) - # Fetch all location names that belong to the selected project + # Fetch all location names that belong to the selected project. + # Strip each value โ€” DB entries may have trailing whitespace. project_locations = set( - qr.location + qr.location.strip() for qr in QRCode.query.filter_by(project_id=project_id) .with_entities(QRCode.location).all() + if qr.location ) + if self.logger: + self.logger.logger.info( + f"Project-location validation: project_id={project_id}, " + f"file_locations={sorted(file_locations)}, " + f"project_locations={sorted(project_locations)}" + ) + # Find the first location in the file that is not in the project unmatched = next( (loc for loc in sorted(file_locations) if loc not in project_locations), @@ -677,10 +772,11 @@ class TimeAttendanceImportService: ) if unmatched: - project_obj = Project.query.get(project_id) + project_obj = self.db.session.get(Project, project_id) project_name = project_obj.name if project_obj else f'ID {project_id}' error_msg = ( f"The data in the file does not belong to the project '{project_name}'. " + f"Location '{unmatched}' was not found in this project. " f"Please verify the selected project or correct the file." ) import_results['errors'].append(error_msg) @@ -697,10 +793,16 @@ class TimeAttendanceImportService: ) except Exception as e: + # Fail closed: if validation cannot be performed, block the import. + # This prevents a DB or import error from silently bypassing the check. + error_msg = f"Project-location validation could not be completed: {e}" + import_results['errors'].append(error_msg) if self.logger: - self.logger.logger.warning( - f"Could not perform project-location validation: {e}" + self.logger.logger.error( + f"Project-location validation error (failing closed): {e}", + exc_info=True ) + return import_results # โ”€โ”€ End project-location validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # Track duplicates using hash