Mar. 23 2026 patch code 2

This commit is contained in:
2026-03-23 12:46:35 -04:00
parent 3c36dbc15f
commit 28bcd13c7f
15 changed files with 783 additions and 760 deletions
+3 -4
View File
@@ -326,7 +326,6 @@ def update_existing_qr_codes():
lh.logger.info(f"Updated {updated_count} existing QR codes with missing URLs/images") lh.logger.info(f"Updated {updated_count} existing QR codes with missing URLs/images")
except Exception as e: except Exception as e:
lh.log_database_error('update_existing_qr_codes', 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): def log_slow_query_performance(app_instance):
@@ -368,15 +367,15 @@ if __name__ == '__main__':
create_tables() create_tables()
log_slow_query_performance(app) log_slow_query_performance(app)
print("🚀 Initializing performance optimizations...")
from extensions import logger_handler from extensions import logger_handler
logger_handler.logger.info("Initializing performance optimizations")
cached_query = initialize_performance_optimizations(app, db, logger_handler) cached_query = initialize_performance_optimizations(app, db, logger_handler)
performance_monitor = PerformanceMonitor(app, db, logger_handler) performance_monitor = PerformanceMonitor(app, db, logger_handler)
if cached_query: if cached_query:
print("Performance optimizations completed successfully") logger_handler.logger.info("Performance optimizations completed successfully")
else: 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") logger_handler.logger.info("QR Attendance Management System started successfully")
+59 -49
View File
@@ -25,7 +25,7 @@ import os
import traceback import traceback
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from functools import wraps 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 import text
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
import uuid import uuid
@@ -169,7 +169,7 @@ class AppLogger:
self.db.session.commit() self.db.session.commit()
except Exception as e: 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): def _register_error_handlers(self):
"""Register Flask error handlers for automatic logging""" """Register Flask error handlers for automatic logging"""
@@ -258,7 +258,7 @@ class AppLogger:
except Exception as e: except Exception as e:
# Don't let logging errors break the application # 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: try:
self.db.session.rollback() self.db.session.rollback()
except: except:
@@ -618,10 +618,10 @@ class AppLogger:
try: try:
table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone() table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone()
if not table_check: 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 return stats
except Exception as table_error: 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 return stats
# Get total events count # Get total events count
@@ -635,9 +635,9 @@ class AppLogger:
total_result = self.db.session.execute(text(total_sql), {'cutoff_date': cutoff_date}).fetchone() total_result = self.db.session.execute(text(total_sql), {'cutoff_date': cutoff_date}).fetchone()
if total_result: if total_result:
stats['total_events'] = total_result.total_events 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: 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 # Get events by category
try: try:
@@ -655,7 +655,7 @@ class AppLogger:
for row in category_result: for row in category_result:
category = row.event_category category = row.event_category
count = row.event_count 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 # Map categories to stats keys
if category == 'security': if category == 'security':
@@ -672,13 +672,13 @@ class AppLogger:
stats['system_events'] = count stats['system_events'] = count
except Exception as category_error: 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 return stats
except Exception as e: 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) self.log_database_error('get_log_statistics', e)
return { return {
'total_events': 0, 'total_events': 0,
@@ -695,16 +695,16 @@ class AppLogger:
try: try:
from datetime import datetime, timedelta # Import here as backup from datetime import datetime, timedelta # Import here as backup
cutoff_date = datetime.now() - timedelta(days=days_to_keep) 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 # Check if table exists first
try: try:
table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone() table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone()
if not table_check: 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 return 0
except Exception as table_error: 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 return 0
# First, count how many records will be deleted # 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_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 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: 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 return 0
except Exception as count_error: 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 return 0
# Perform the cleanup - exclude critical logs # Perform the cleanup - exclude critical logs
@@ -741,7 +741,7 @@ class AppLogger:
deleted_count = result.rowcount deleted_count = result.rowcount
self.db.session.commit() 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 # Log the cleanup operation
self.logger.info(f"Log cleanup completed: {deleted_count} entries removed (keeping entries newer than {days_to_keep} days)") 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 return deleted_count
except Exception as delete_error: 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() self.db.session.rollback()
return 0 return 0
except Exception as e: 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.db.session.rollback()
self.log_database_error('cleanup_old_logs', e) self.log_database_error('cleanup_old_logs', e)
return 0 return 0
@@ -819,7 +819,7 @@ class AppLogger:
except Exception as e: except Exception as e:
self.log_database_error('get_recent_logs', 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 [] return []
def log_system_event(self, event_type, description, severity='INFO', additional_data=None): 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() result = self.db.session.execute(text(check_table_sql)).fetchone()
if result.table_exists == 0: 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() self._create_log_table()
return True return True
count_sql = "SELECT COUNT(*) as record_count FROM log_events" count_sql = "SELECT COUNT(*) as record_count FROM log_events"
count_result = self.db.session.execute(text(count_sql)).fetchone() 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 return True
except Exception as e: 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 return False
def log_modal_interaction(self, event_type, description, additional_data=None): def log_modal_interaction(self, event_type, description, additional_data=None):
@@ -904,64 +904,74 @@ class AppLogger:
severity='INFO' severity='INFO'
) )
except Exception as e: 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 # DECORATOR FUNCTIONS FOR AUTOMATIC LOGGING
def log_user_activity(activity_type): 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): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
try: try:
result = f(*args, **kwargs) result = f(*args, **kwargs)
# Log successful activity # Log successful activity via the AppLogger instance on current_app
if hasattr(g, 'app') and hasattr(g.app, 'logger_handler'): try:
logger = g.app.logger_handler lh = current_app.logger_handler
logger.logger.info(json.dumps({ lh.log_user_activity(
'event': f'user_activity_{activity_type}', activity_type=activity_type,
'data': { description=(
'user_id': session.get('user_id'), f"User '{session.get('username', 'anonymous')}' "
'username': session.get('username'), f"completed activity: {activity_type}"
'activity': activity_type, )
'timestamp': datetime.now().isoformat() )
} 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 return result
except Exception as e: except Exception as e:
# Log error # Log the error, then re-raise so Flask handles it normally
if hasattr(g, 'app') and hasattr(g.app, 'logger_handler'): try:
logger = g.app.logger_handler lh = current_app.logger_handler
logger.log_flask_error( lh.log_flask_error(
error_type=f"activity_error_{activity_type}", error_type=f"activity_error_{activity_type}",
error_message=str(e), error_message=str(e),
stack_trace=traceback.format_exc() 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 raise
return decorated_function return decorated_function
return decorator return decorator
def log_database_operations(operation_name): def log_database_operations(operation_name):
"""Decorator to automatically log database operations""" """Decorator to automatically log database operation errors."""
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
try: try:
result = f(*args, **kwargs) return f(*args, **kwargs)
return result
except Exception as e: except Exception as e:
# Log database error # Log the database error via the AppLogger instance on current_app
if hasattr(g, 'app') and hasattr(g.app, 'logger_handler'): try:
logger = g.app.logger_handler lh = current_app.logger_handler
logger.log_database_error( lh.log_database_error(
operation=operation_name, operation=operation_name,
error=e 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 raise
return decorated_function return decorated_function
+26 -33
View File
@@ -77,8 +77,10 @@ def api_recent_logs():
severity = request.args.get('severity', '') severity = request.args.get('severity', '')
search = request.args.get('search', '') search = request.args.get('search', '')
print(f"📊 API request - Days: {days}, Limit: {limit}, Page: {page}") logger_handler.logger.debug(
print(f"📊 Filters - Category: {category}, Severity: {severity}, Search: {search}") 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) cutoff_date = datetime.now() - timedelta(days=days)
@@ -164,7 +166,7 @@ def api_recent_logs():
'ip_address': row.ip_address or '-' '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({ return jsonify({
'success': True, 'success': True,
@@ -179,7 +181,6 @@ def api_recent_logs():
except Exception as e: except Exception as e:
logger_handler.log_database_error('api_recent_logs', e) logger_handler.log_database_error('api_recent_logs', e)
print(f"Error in api_recent_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to fetch recent logs: {str(e)}' 'error': f'Failed to fetch recent logs: {str(e)}'
@@ -191,11 +192,11 @@ def api_log_stats():
"""API endpoint to get logging statistics""" """API endpoint to get logging statistics"""
try: try:
days = request.args.get('days', 7, type=int) 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 # Get statistics from logger handler
stats = logger_handler.get_log_statistics(days=days) stats = logger_handler.get_log_statistics(days=days)
print(f"📈 Retrieved stats: {stats}")
# Ensure all expected keys exist with updated categories # Ensure all expected keys exist with updated categories
expected_stats = { expected_stats = {
@@ -217,7 +218,6 @@ def api_log_stats():
except Exception as e: except Exception as e:
logger_handler.log_database_error('api_log_stats', e) logger_handler.log_database_error('api_log_stats', e)
print(f"❌ Error in api_log_stats: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to fetch log statistics: {str(e)}', 'error': f'Failed to fetch log statistics: {str(e)}',
@@ -240,25 +240,21 @@ def api_cleanup_logs():
# Get JSON data # Get JSON data
data = request.get_json() data = request.get_json()
if not data: if not data:
print("❌ No JSON data provided")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'No JSON data provided' 'error': 'No JSON data provided'
}), 400 }), 400
days_to_keep = data.get('days_to_keep', 90) days_to_keep = data.get('days_to_keep', 90)
print(f"🧹 Cleanup request: keep last {days_to_keep} days")
# Validate input # Validate input
if not isinstance(days_to_keep, int) or days_to_keep < 7: if not isinstance(days_to_keep, int) or days_to_keep < 7:
print(f"❌ Invalid days_to_keep: {days_to_keep}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'days_to_keep must be an integer >= 7' 'error': 'days_to_keep must be an integer >= 7'
}), 400 }), 400
if days_to_keep > 365: if days_to_keep > 365:
print(f"❌ days_to_keep too large: {days_to_keep}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'days_to_keep cannot exceed 365 days' '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) deleted_count = logger_handler.cleanup_old_logs(days_to_keep=days_to_keep)
admin_username = session.get('username', 'unknown') 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 # Log the admin action
logger_handler.log_security_event( logger_handler.log_security_event(
@@ -294,7 +293,6 @@ def api_cleanup_logs():
except Exception as e: except Exception as e:
logger_handler.log_database_error('api_cleanup_logs', e) logger_handler.log_database_error('api_cleanup_logs', e)
print(f"❌ Error in api_cleanup_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to cleanup old logs: {str(e)}' 'error': f'Failed to cleanup old logs: {str(e)}'
@@ -306,7 +304,7 @@ def api_clear_logs():
"""API endpoint to clear ALL log entries""" """API endpoint to clear ALL log entries"""
try: try:
admin_username = session.get('username', 'unknown') 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 # Count existing logs before deletion
try: try:
@@ -314,10 +312,7 @@ def api_clear_logs():
count_result = db.session.execute(text(count_sql)).fetchone() count_result = db.session.execute(text(count_sql)).fetchone()
total_logs = count_result.total_logs if count_result else 0 total_logs = count_result.total_logs if count_result else 0
print(f"📊 Total logs to be cleared: {total_logs}")
if total_logs == 0: if total_logs == 0:
print("✅ No logs found to clear")
return jsonify({ return jsonify({
'success': True, 'success': True,
'deleted_count': 0, 'deleted_count': 0,
@@ -325,7 +320,7 @@ def api_clear_logs():
}) })
except Exception as count_error: 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 total_logs = 0
# Perform the clear operation # Perform the clear operation
@@ -335,7 +330,9 @@ def api_clear_logs():
deleted_count = result.rowcount deleted_count = result.rowcount
db.session.commit() 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) # Log the clear operation (this will be the first entry in the new log)
logger_handler.log_security_event( logger_handler.log_security_event(
@@ -358,7 +355,7 @@ def api_clear_logs():
}) })
except Exception as delete_error: 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() db.session.rollback()
return jsonify({ return jsonify({
'success': False, 'success': False,
@@ -367,7 +364,6 @@ def api_clear_logs():
except Exception as e: except Exception as e:
logger_handler.log_database_error('api_clear_logs', e) logger_handler.log_database_error('api_clear_logs', e)
print(f"❌ Error in api_clear_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to clear logs: {str(e)}' 'error': f'Failed to clear logs: {str(e)}'
@@ -381,7 +377,6 @@ def api_clear_old_logs():
# Get JSON data # Get JSON data
data = request.get_json() data = request.get_json()
if not data: if not data:
print("❌ No JSON data provided")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'No JSON data provided' 'error': 'No JSON data provided'
@@ -389,11 +384,9 @@ def api_clear_old_logs():
days_threshold = data.get('days_threshold', 90) days_threshold = data.get('days_threshold', 90)
admin_username = session.get('username', 'unknown') admin_username = session.get('username', 'unknown')
print(f"🧹 Clear old logs request by admin: {admin_username}, threshold: {days_threshold} days")
# Validate input # Validate input
if not isinstance(days_threshold, int) or days_threshold not in [30, 60, 90]: if not isinstance(days_threshold, int) or days_threshold not in [30, 60, 90]:
print(f"❌ Invalid days_threshold: {days_threshold}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'days_threshold must be 30, 60, or 90' '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() count_result = db.session.execute(text(count_sql), {'cutoff_date': cutoff_date}).fetchone()
total_logs = count_result.total_logs if count_result else 0 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: if total_logs == 0:
print("✅ No old logs found to clear")
return jsonify({ return jsonify({
'success': True, 'success': True,
'deleted_count': 0, 'deleted_count': 0,
@@ -419,7 +409,7 @@ def api_clear_old_logs():
}) })
except Exception as count_error: 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 total_logs = 0
# Perform the clear operation # Perform the clear operation
@@ -429,7 +419,9 @@ def api_clear_old_logs():
deleted_count = result.rowcount deleted_count = result.rowcount
db.session.commit() 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 # Log the clear operation
logger_handler.log_security_event( logger_handler.log_security_event(
@@ -455,7 +447,7 @@ def api_clear_old_logs():
}) })
except Exception as delete_error: 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() db.session.rollback()
return jsonify({ return jsonify({
'success': False, 'success': False,
@@ -464,7 +456,6 @@ def api_clear_old_logs():
except Exception as e: except Exception as e:
logger_handler.log_database_error('api_clear_old_logs', e) logger_handler.log_database_error('api_clear_old_logs', e)
print(f"❌ Error in api_clear_old_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to clear old logs: {str(e)}' 'error': f'Failed to clear old logs: {str(e)}'
@@ -481,7 +472,10 @@ def api_export_logs():
search = request.args.get('search', '') search = request.args.get('search', '')
admin_username = session.get('username', 'unknown') 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) cutoff_date = datetime.now() - timedelta(days=days)
@@ -578,7 +572,6 @@ def api_export_logs():
except Exception as e: except Exception as e:
logger_handler.log_database_error('api_export_logs', e) logger_handler.log_database_error('api_export_logs', e)
print(f"❌ Error in api_export_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to export logs: {str(e)}' 'error': f'Failed to export logs: {str(e)}'
+129 -145
View File
@@ -44,18 +44,18 @@ bp = Blueprint('attendance', __name__)
def attendance_report(): def attendance_report():
"""Safe attendance report with backward compatibility for location_accuracy and fixed datetime handling""" """Safe attendance report with backward compatibility for location_accuracy and fixed datetime handling"""
try: try:
print("📊 Loading attendance report...") logger_handler.logger.debug("Loading attendance report")
# Log attendance report access # Log attendance report access
try: try:
user_role = session.get('role', 'unknown') user_role = session.get('role', 'unknown')
logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed attendance report") logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed attendance report")
except Exception as log_error: except Exception:
print(f"⚠️ Logging error (non-critical): {log_error}") pass
# Check if location_accuracy column exists # Check if location_accuracy column exists
has_location_accuracy = check_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 # Get filter parameters
date_from = request.args.get('date_from', '') date_from = request.args.get('date_from', '')
@@ -98,7 +98,7 @@ def attendance_report():
# Check if user is Project Manager and get their permissions # Check if user is Project Manager and get their permissions
if user_role == 'project_manager': 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: try:
# Get assigned projects # Get assigned projects
@@ -115,10 +115,9 @@ def attendance_report():
f"Projects: {allowed_project_ids}, Locations: {allowed_location_names}" f"Projects: {allowed_project_ids}, Locations: {allowed_location_names}"
) )
print(f"🔒 Allowed projects: {allowed_project_ids}") logger_handler.logger.debug(f"PM allowed projects: {allowed_project_ids}, locations: {allowed_location_names}")
print(f"🔒 Allowed locations: {allowed_location_names}")
except Exception as perm_error: 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}") logger_handler.logger.error(f"Error loading Project Manager permissions: {perm_error}")
# If no permissions assigned, user cannot view anything # 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" 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 # Execute query
result = db.session.execute(text(base_query), query_params) result = db.session.execute(text(base_query), query_params)
records = result.fetchall() records = result.fetchall()
print(f"Loaded {len(records)} attendance records") logger_handler.logger.debug(f"Loaded {len(records)} attendance records")
# Process records # Process records
processed_records = [] processed_records = []
@@ -322,7 +321,7 @@ def attendance_report():
record_dict['accuracy_level'] = 'unknown' record_dict['accuracy_level'] = 'unknown'
processed_records.append(record_dict) processed_records.append(record_dict)
except Exception as rec_error: except Exception as rec_error:
print(f"⚠️ Error processing record: {rec_error}") logger_handler.logger.warning(f"Error processing attendance record: {rec_error}")
continue continue
# Get unique locations for filter dropdown # Get unique locations for filter dropdown
@@ -333,7 +332,7 @@ def attendance_report():
if user_role == 'project_manager' and allowed_location_names: if user_role == 'project_manager' and allowed_location_names:
# Only show locations the PM has access to # Only show locations the PM has access to
locations = sorted(allowed_location_names) 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: else:
# Show all locations for Admin/Staff/Payroll # Show all locations for Admin/Staff/Payroll
locations_query = db.session.execute(text(""" locations_query = db.session.execute(text("""
@@ -343,12 +342,12 @@ def attendance_report():
ORDER BY location_name ORDER BY location_name
""")) """))
locations = [row[0] for row in locations_query.fetchall()] 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 # END: FILTER LOCATIONS FOR PROJECT MANAGER
# ============================================================ # ============================================================
except Exception as e: except Exception as e:
print(f"⚠️ Error loading locations: {e}") logger_handler.logger.warning(f"Error loading locations filter: {e}")
locations = [] locations = []
# Get projects for filter dropdown # Get projects for filter dropdown
@@ -369,7 +368,7 @@ def attendance_report():
ORDER BY p.name ORDER BY p.name
""")) """))
projects = projects_query.fetchall() 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: else:
# Show all projects for Admin/Staff/Payroll # Show all projects for Admin/Staff/Payroll
projects = db.session.execute(text(""" projects = db.session.execute(text("""
@@ -382,18 +381,18 @@ def attendance_report():
HAVING COUNT(DISTINCT ad.id) > 0 HAVING COUNT(DISTINCT ad.id) > 0
ORDER BY p.name ORDER BY p.name
""")).fetchall() """)).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 # END: FILTER PROJECTS FOR PROJECT MANAGER
# ============================================================ # ============================================================
except Exception as e: except Exception as e:
print(f"⚠️ Error loading projects: {e}") logger_handler.logger.warning(f"Error loading projects filter: {e}")
projects = [] projects = []
# ============================================================ # ============================================================
# STATISTICS - COMPLETELY REWRITTEN FOR SAFETY # STATISTICS - COMPLETELY REWRITTEN FOR SAFETY
# ============================================================ # ============================================================
print("📊 Loading statistics...") logger_handler.logger.debug("Loading attendance statistics")
# Create simple dict for stats (most compatible approach) # Create simple dict for stats (most compatible approach)
stats_dict = { stats_dict = {
@@ -455,15 +454,13 @@ def attendance_report():
if stats_conditions: if stats_conditions:
stats_query_text += " AND " + " AND ".join(stats_conditions) stats_query_text += " AND " + " AND ".join(stats_conditions)
print(f"📊 Executing stats query...") logger_handler.logger.debug(f"Executing stats query with params: {list(stats_params.keys())}")
print(f"📊 Stats params: {list(stats_params.keys())}")
# Execute stats query # Execute stats query
stats_result = db.session.execute(text(stats_query_text), stats_params) stats_result = db.session.execute(text(stats_query_text), stats_params)
stats_row = stats_result.fetchone() stats_row = stats_result.fetchone()
print(f"📊 Stats row type: {type(stats_row)}") logger_handler.logger.debug(f"Stats row type: {type(stats_row).__name__}")
print(f"📊 Stats row value: {stats_row}")
# Safely extract stats from row # Safely extract stats from row
if stats_row is not None and len(stats_row) >= 7: 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_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['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 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: 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 # stats_dict already has default values
else: 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: except Exception as stats_error:
print(f"Error loading statistics: {stats_error}") logger_handler.logger.error(f"Error loading statistics: {stats_error}", exc_info=True)
import traceback
print(f"❌ Stats error traceback: {traceback.format_exc()}")
# stats_dict already has default values # stats_dict already has default values
# Convert dict to object-like for template compatibility # Convert dict to object-like for template compatibility
@@ -495,7 +490,7 @@ def attendance_report():
setattr(self, key, value) setattr(self, key, value)
stats = StatsObject(stats_dict) 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 # END: STATISTICS
@@ -505,8 +500,7 @@ def attendance_report():
today_date = datetime.now().strftime('%Y-%m-%d') today_date = datetime.now().strftime('%Y-%m-%d')
current_date_formatted = datetime.now().strftime('%B %d') current_date_formatted = datetime.now().strftime('%B %d')
print("Rendering attendance report template") logger_handler.logger.debug("Rendering attendance report template")
print(f"✅ Stats object: {stats}")
return render_template('attendance_report.html', return render_template('attendance_report.html',
attendance_records=processed_records, attendance_records=processed_records,
@@ -527,18 +521,17 @@ def attendance_report():
user_role=user_role) user_role=user_role)
except Exception as e: except Exception as e:
print(f"Error loading attendance report: {e}") logger_handler.logger.error(f"Error loading attendance report: {e}", exc_info=True)
print(f"❌ Exception type: {type(e)}")
import traceback import traceback
error_traceback = traceback.format_exc() error_traceback = traceback.format_exc()
print(f"❌ Traceback: {error_traceback}")
# Log the error # Log the error
try: try:
logger_handler.log_database_error('attendance_report', e) logger_handler.log_database_error('attendance_report', e)
except Exception as log_error: 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') flash('Error loading attendance report. Please check the server logs for details.', 'error')
return redirect(url_for('dashboard.dashboard')) 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()) qr_codes=QRCode.query.filter_by(active_status=True).all())
# Validate the QR code exists # 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: if not new_qr_code:
flash('Selected location event not found.', 'error') flash('Selected location event not found.', 'error')
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() 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') 'editor_username': session.get('username')
} }
) )
print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} updated attendance record {record_id}: {changes}") logger_handler.logger.info(
print(f"[LOG] Edit reason: {edit_note}") f"User {session.get('username')} ({session.get('role', 'unknown')}) "
f"updated attendance record {record_id}: {changes}, reason: {edit_note}"
)
else: else:
# Log even if no changes were made (for audit purposes) # Log even if no changes were made (for audit purposes)
logger_handler.log_security_event( logger_handler.log_security_event(
@@ -672,8 +667,10 @@ def edit_attendance(record_id):
'editor_username': session.get('username') 'editor_username': session.get('username')
} }
) )
print(f"[LOG] {session.get('role', 'unknown').title()} {session.get('username')} edited record {record_id} with no changes") logger_handler.logger.info(
print(f"[LOG] Edit reason: {edit_note}") 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') flash(f'Attendance record for {new_employee_id} updated successfully! Edit reason logged for audit.', 'success')
return redirect(url_for('attendance.attendance_report')) return redirect(url_for('attendance.attendance_report'))
@@ -693,7 +690,7 @@ def edit_attendance(record_id):
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('attendance_update', e) 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') flash('Error updating attendance record. Please try again.', 'error')
return redirect(url_for('attendance.attendance_report')) return redirect(url_for('attendance.attendance_report'))
@@ -770,7 +767,7 @@ def save_manual_attendance():
return redirect(url_for('attendance.add_manual_attendance')) return redirect(url_for('attendance.add_manual_attendance'))
# Get QR code (location) # 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: if not qr_code:
flash('Selected location not found.', 'error') flash('Selected location not found.', 'error')
return redirect(url_for('attendance.add_manual_attendance')) return redirect(url_for('attendance.add_manual_attendance'))
@@ -1093,7 +1090,10 @@ def delete_attendance(record_id):
db.session.delete(attendance_record) db.session.delete(attendance_record)
db.session.commit() 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 # Return JSON response for AJAX requests
if request.headers.get('X-Requested-With') == 'XMLHttpRequest': if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
@@ -1108,7 +1108,7 @@ def delete_attendance(record_id):
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('attendance_delete', e) 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': if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({ return jsonify({
@@ -1191,7 +1191,7 @@ def verification_review():
project_id = record.qr_code.project_id project_id = record.qr_code.project_id
if project_id not in project_names: if project_id not in project_names:
try: try:
project = Project.query.get(project_id) project = db.session.get(Project, project_id)
if project: if project:
project_names[project_id] = project.name project_names[project_id] = project.name
else: else:
@@ -1307,16 +1307,11 @@ def get_verification_details(record_id):
record = AttendanceData.query.get_or_404(record_id) record = AttendanceData.query.get_or_404(record_id)
# DEBUG: Log record details # DEBUG: Log record details
print(f"=== VERIFICATION DETAILS DEBUG ===") logger_handler.logger.debug(
print(f"Record ID: {record.id}") f"Verification details: record={record.id}, employee={record.employee_id}, "
print(f"Employee: {record.employee_id}") f"date={record.check_in_date}, time={record.check_in_time}, "
print(f"check_in_date type: {type(record.check_in_date)}") f"has_photo={record.verification_photo is not None}, status={record.verification_status}"
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"==================================")
# Check if user has permission to view # Check if user has permission to view
# Allow admin and payroll staff to view verification details # Allow admin and payroll staff to view verification details
@@ -1333,13 +1328,13 @@ def get_verification_details(record_id):
try: try:
check_in_date_str = record.check_in_date.strftime('%Y-%m-%d') if record.check_in_date else 'N/A' check_in_date_str = record.check_in_date.strftime('%Y-%m-%d') if record.check_in_date else 'N/A'
except Exception as e: 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' check_in_date_str = str(record.check_in_date) if record.check_in_date else 'N/A'
try: try:
check_in_time_str = record.check_in_time.strftime('%I:%M %p') if record.check_in_time else 'N/A' check_in_time_str = record.check_in_time.strftime('%I:%M %p') if record.check_in_time else 'N/A'
except Exception as e: 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' check_in_time_str = str(record.check_in_time) if record.check_in_time else 'N/A'
# Prepare record data with safe formatting # Prepare record data with safe formatting
@@ -1373,10 +1368,7 @@ def get_verification_details(record_id):
}) })
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error getting verification details for record {record_id}: {e}") logger_handler.logger.error(f"Error in get_verification_details for record {record_id}: {e}", exc_info=True)
print(f"❌ Error in get_verification_details for record {record_id}: {e}")
import traceback
print(f"❌ Traceback: {traceback.format_exc()}")
return jsonify({ return jsonify({
'success': False, 'success': False,
@@ -1402,7 +1394,7 @@ def verification_review_detail(record_id):
return redirect(url_for('attendance.attendance_report')) return redirect(url_for('attendance.attendance_report'))
# Get the QR code information for additional context # 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 # Get employee name from Employee table
employee_name = None employee_name = None
@@ -1497,7 +1489,7 @@ def attendance_stats_api():
}) })
except Exception as e: 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 return jsonify({'error': 'Failed to fetch attendance statistics'}), 500
@bp.route('/export-configuration', endpoint='export_configuration') @bp.route('/export-configuration', endpoint='export_configuration')
@@ -1515,8 +1507,8 @@ def export_configuration():
try: 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')} (role: {user_role}) accessed export configuration")
logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed export configuration page") logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed export configuration page")
except Exception as log_error: except Exception:
print(f"⚠️ Logging error (non-critical): {log_error}") pass
# Get current filters from session or request args # Get current filters from session or request args
filters = { filters = {
@@ -1527,24 +1519,24 @@ def export_configuration():
'project_filter': request.args.get('project', '') '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 # Get project name if project filter is applied
project_name = None project_name = None
if filters.get('project_filter'): if filters.get('project_filter'):
try: try:
project = Project.query.get(int(filters['project_filter'])) project = db.session.get(Project, int(filters['project_filter']))
if project: if project:
project_name = project.name 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: 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 # Check if location accuracy feature exists
try: try:
has_location_accuracy = check_location_accuracy_column_exists() has_location_accuracy = check_location_accuracy_column_exists()
except Exception as e: 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 has_location_accuracy = False
# Define all available columns with their default settings # Define all available columns with their default settings
@@ -1574,7 +1566,7 @@ def export_configuration():
'enabled': True # Changed from False to True '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', return render_template('export_configuration.html',
available_columns=available_columns, available_columns=available_columns,
@@ -1583,8 +1575,7 @@ def export_configuration():
has_location_accuracy_feature=has_location_accuracy) has_location_accuracy_feature=has_location_accuracy)
except Exception as e: except Exception as e:
print(f"Error in export_configuration route: {e}") logger_handler.logger.error(f"Error in export_configuration route: {e}", exc_info=True)
print(f"❌ Traceback: {traceback.format_exc()}")
# Use your existing logger error method with correct parameters # Use your existing logger error method with correct parameters
try: try:
@@ -1594,7 +1585,7 @@ def export_configuration():
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
except Exception as log_error: 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') flash('Error loading export configuration page.', 'error')
return redirect(url_for('attendance.attendance_report')) 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') flash('Access denied. Only administrators and payroll staff can export data.', 'error')
return redirect(url_for('attendance.attendance_report')) 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 # Log export action using your existing logger
try: try:
logger_handler.logger.info(f"User {session.get('username', 'unknown')} generated Excel export") logger_handler.logger.info(f"User {session.get('username', 'unknown')} generated Excel export")
except Exception as log_error: except Exception:
print(f"⚠️ Logging error (non-critical): {log_error}") pass
# Get selected columns and custom names from form # Get selected columns and custom names from form
selected_columns_raw = request.form.getlist('selected_columns') 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 # Get column order from form
column_order_json = request.form.get('column_order', '[]') column_order_json = request.form.get('column_order', '[]')
@@ -1629,7 +1620,7 @@ def generate_excel_export():
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
column_order = [] 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 # Determine final column order
if column_order: if column_order:
@@ -1643,7 +1634,7 @@ def generate_excel_export():
# Fallback to raw selection order # Fallback to raw selection order
selected_columns = selected_columns_raw 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: if not selected_columns:
flash('Please select at least one column to export.', 'error') 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') 'project_filter': request.form.get('project_filter')
} }
print(f"📊 Export filters: {filters}") logger_handler.logger.debug(f"Export filters: {filters}")
print(f"📊 Column names: {column_names}")
# Save user preferences in session for next time # Save user preferences in session for next time
session['export_preferences'] = { session['export_preferences'] = {
@@ -1680,13 +1670,13 @@ def generate_excel_export():
project_name_for_filename = '' project_name_for_filename = ''
if filters.get('project_filter'): if filters.get('project_filter'):
try: try:
project = Project.query.get(int(filters['project_filter'])) project = db.session.get(Project, int(filters['project_filter']))
if project: if project:
# Replace spaces and special characters with underscores # Replace spaces and special characters with underscores
project_name_safe = project.name.replace(' ', '_').replace('/', '_').replace('\\', '_') project_name_safe = project.name.replace(' ', '_').replace('/', '_').replace('\\', '_')
project_name_for_filename = f"{project_name_safe}_" project_name_for_filename = f"{project_name_safe}_"
except Exception as e: 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) # Format dates for filename (MMDDYYYY format)
date_from_formatted = '' date_from_formatted = ''
@@ -1717,14 +1707,13 @@ def generate_excel_export():
filename = f'{project_name_for_filename}attendance_report_{date_range_str}.xlsx' filename = f'{project_name_for_filename}attendance_report_{date_range_str}.xlsx'
print(f"📊 Excel file generated successfully: {filename}") logger_handler.logger.info(f"Excel export generated successfully: {filename}")
print(f"📊 Column order in export: {selected_columns}")
# Log successful export using your existing logger # Log successful export using your existing logger
try: 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}") 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: except Exception:
print(f"⚠️ Logging error (non-critical): {log_error}") pass
return send_file( return send_file(
excel_file, excel_file,
@@ -1737,8 +1726,7 @@ def generate_excel_export():
return redirect(url_for('attendance.export_configuration')) return redirect(url_for('attendance.export_configuration'))
except Exception as e: except Exception as e:
print(f"Error in generate_excel_export route: {e}") logger_handler.logger.error(f"Error in generate_excel_export route: {e}", exc_info=True)
print(f"❌ Traceback: {traceback.format_exc()}")
# Use your existing logger error method with correct parameters # Use your existing logger error method with correct parameters
try: try:
@@ -1748,7 +1736,7 @@ def generate_excel_export():
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
except Exception as log_error: 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') flash('Error generating Excel export.', 'error')
return redirect(url_for('attendance.export_configuration')) return redirect(url_for('attendance.export_configuration'))
@@ -1756,7 +1744,7 @@ def generate_excel_export():
def create_excel_export(selected_columns, column_names, filters): def create_excel_export(selected_columns, column_names, filters):
"""Create Excel file with selected attendance data - Updated to include employee names""" """Create Excel file with selected attendance data - Updated to include employee names"""
try: 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 # Import openpyxl modules
try: try:
@@ -1764,8 +1752,7 @@ def create_excel_export(selected_columns, column_names, filters):
from openpyxl.styles import Font, Alignment, PatternFill from openpyxl.styles import Font, Alignment, PatternFill
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
except ImportError as e: except ImportError as e:
print(f"openpyxl import error: {e}") logger_handler.logger.error(f"openpyxl import error: {e}. Run: pip install openpyxl")
print("💡 Install openpyxl: pip install openpyxl")
return None return None
# Build query based on filters - JOIN with QRCode to get location_event and location_address # 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: try:
date_from = datetime.strptime(filters['date_from'], '%Y-%m-%d').date() date_from = datetime.strptime(filters['date_from'], '%Y-%m-%d').date()
query = query.filter(AttendanceData.check_in_date >= date_from) 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: 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'): if filters.get('date_to'):
try: try:
date_to = datetime.strptime(filters['date_to'], '%Y-%m-%d').date() date_to = datetime.strptime(filters['date_to'], '%Y-%m-%d').date()
query = query.filter(AttendanceData.check_in_date <= date_to) 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: except ValueError as e:
print(f"⚠️ Invalid date_to format: {e}") logger_handler.logger.warning(f"Invalid date_to format: {e}")
# Apply location filter # Apply location filter
if filters.get('location_filter'): if filters.get('location_filter'):
query = query.filter(AttendanceData.location_name.like(f"%{filters['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 # Apply employee filter — supports comma-separated multi-employee values
if filters.get('employee_filter'): 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]) query = query.filter(AttendanceData.employee_id == emp_ids[0])
elif len(emp_ids) > 1: elif len(emp_ids) > 1:
query = query.filter(AttendanceData.employee_id.in_(emp_ids)) 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 # Apply project filter
if filters.get('project_filter'): if filters.get('project_filter'):
try: try:
project_id = int(filters['project_filter']) project_id = int(filters['project_filter'])
query = query.filter(QRCode.project_id == project_id) 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: 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 # Order by date and time
query = query.order_by(AttendanceData.check_in_date.desc(), AttendanceData.check_in_time.desc()) query = query.order_by(AttendanceData.check_in_date.desc(), AttendanceData.check_in_time.desc())
# Execute query # Execute query
results = query.all() 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: if not results:
print("⚠️ No records found for export") logger_handler.logger.warning("No records found for export")
return None return None
# Create workbook # Create workbook
@@ -1890,10 +1877,10 @@ def create_excel_export(selected_columns, column_names, filters):
lng_formatted = f"{float(qr_record.address_longitude):.10f}" 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()}")' hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
cell.value = hyperlink_formula 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: else:
cell.value = address_text 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: else:
# Lower accuracy - use actual check-in address with hyperlink # Lower accuracy - use actual check-in address with hyperlink
address_text = attendance_record.address or '' 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}" 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()}")' hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
cell.value = hyperlink_formula 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: else:
cell.value = address_text 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): except (ValueError, TypeError):
# If accuracy can't be converted to float, use check-in address with hyperlink # If accuracy can't be converted to float, use check-in address with hyperlink
address_text = attendance_record.address or '' 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}" 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()}")' hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
cell.value = hyperlink_formula 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: else:
cell.value = address_text cell.value = address_text
else: else:
@@ -1928,7 +1915,7 @@ def create_excel_export(selected_columns, column_names, filters):
lng_formatted = f"{float(attendance_record.longitude):.10f}" 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()}")' hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
cell.value = hyperlink_formula 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: else:
cell.value = address_text cell.value = address_text
elif column_key == 'device_info': elif column_key == 'device_info':
@@ -1948,7 +1935,7 @@ def create_excel_export(selected_columns, column_names, filters):
else: else:
cell.value = '' cell.value = ''
except Exception as cell_error: 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 = '' cell.value = ''
# Auto-adjust column widths based on content and header # 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 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 # Save to BytesIO
excel_buffer = io.BytesIO() excel_buffer = io.BytesIO()
wb.save(excel_buffer) wb.save(excel_buffer)
excel_buffer.seek(0) 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 # Log export action with employee name column
try: try:
logger_handler.logger.info(f"Excel export with employee names generated by user {session.get('username', 'unknown')}") logger_handler.logger.info(f"Excel export with employee names generated by user {session.get('username', 'unknown')}")
except Exception as log_error: except Exception:
print(f"⚠️ Logging error (non-critical): {log_error}") pass
return excel_buffer return excel_buffer
except Exception as e: except Exception as e:
print(f"Error creating Excel export: {e}") logger_handler.logger.error(f"Error creating Excel export: {e}", exc_info=True)
print(f"❌ Traceback: {traceback.format_exc()}")
# Log error # Log error
try: try:
@@ -2036,7 +2022,7 @@ def create_excel_export(selected_columns, column_names, filters):
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
except Exception as log_error: 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 return None
@@ -2052,7 +2038,7 @@ def format_employee_id_for_excel(employee_id):
def create_excel_export_ordered(selected_columns, column_names, filters): def create_excel_export_ordered(selected_columns, column_names, filters):
"""Create Excel file with selected attendance data in specified column order""" """Create Excel file with selected attendance data in specified column order"""
try: 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 # Import openpyxl modules
try: try:
@@ -2060,8 +2046,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
from openpyxl.styles import Font, Alignment, PatternFill from openpyxl.styles import Font, Alignment, PatternFill
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
except ImportError as e: except ImportError as e:
print(f"openpyxl import error: {e}") logger_handler.logger.error(f"openpyxl import error: {e}. Run: pip install openpyxl")
print("💡 Install openpyxl: pip install openpyxl")
return None return None
# Build query based on filters - JOIN with QRCode to get location_event and location_address # 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: try:
date_from = datetime.strptime(filters['date_from'], '%Y-%m-%d').date() date_from = datetime.strptime(filters['date_from'], '%Y-%m-%d').date()
query = query.filter(AttendanceData.check_in_date >= date_from) 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: 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'): if filters.get('date_to'):
try: try:
date_to = datetime.strptime(filters['date_to'], '%Y-%m-%d').date() date_to = datetime.strptime(filters['date_to'], '%Y-%m-%d').date()
query = query.filter(AttendanceData.check_in_date <= date_to) 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: except ValueError as e:
print(f"⚠️ Invalid date_to format: {e}") logger_handler.logger.warning(f"Invalid date_to format: {e}")
# Apply location filter # Apply location filter
if filters.get('location_filter'): if filters.get('location_filter'):
query = query.filter(AttendanceData.location_name.like(f"%{filters['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 # Apply employee filter — supports comma-separated multi-employee values
if filters.get('employee_filter'): 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]) query = query.filter(AttendanceData.employee_id == emp_ids[0])
elif len(emp_ids) > 1: elif len(emp_ids) > 1:
query = query.filter(AttendanceData.employee_id.in_(emp_ids)) 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 # Apply project filter
if filters.get('project_filter'): if filters.get('project_filter'):
try: try:
project_id = int(filters['project_filter']) project_id = int(filters['project_filter'])
query = query.filter(QRCode.project_id == project_id) 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: 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 # Order by date and time
query = query.order_by(AttendanceData.check_in_date.desc(), AttendanceData.check_in_time.desc()) query = query.order_by(AttendanceData.check_in_date.desc(), AttendanceData.check_in_time.desc())
# Execute query # Execute query
results = query.all() 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: if not results:
print("⚠️ No records found for export") logger_handler.logger.warning("No records found for export")
return None return None
# Create workbook # 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}" 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()}")' hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
cell.value = hyperlink_formula 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: else:
cell.value = address_text 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: else:
# Lower accuracy - use actual check-in address with hyperlink # Lower accuracy - use actual check-in address with hyperlink
address_text = attendance_record.address or '' 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}" 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()}")' hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
cell.value = hyperlink_formula 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: else:
cell.value = address_text 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): except (ValueError, TypeError):
# If accuracy can't be converted to float, use check-in address with hyperlink # If accuracy can't be converted to float, use check-in address with hyperlink
address_text = attendance_record.address or '' 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}" 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()}")' hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
cell.value = hyperlink_formula 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: else:
cell.value = address_text cell.value = address_text
else: else:
@@ -2230,7 +2215,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
lng_formatted = f"{float(attendance_record.longitude):.10f}" 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()}")' hyperlink_formula = f'=HYPERLINK("http://maps.google.com/maps?q={lat_formatted},{lng_formatted}","{address_text.strip()}")'
cell.value = hyperlink_formula 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: else:
cell.value = address_text cell.value = address_text
elif column_key == 'device_info': elif column_key == 'device_info':
@@ -2259,7 +2244,7 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
else: else:
cell.value = '' cell.value = ''
except Exception as cell_error: 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 = '' cell.value = ''
# Auto-adjust column widths based on content and header # 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 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 # Save to BytesIO
excel_buffer = io.BytesIO() excel_buffer = io.BytesIO()
wb.save(excel_buffer) wb.save(excel_buffer)
excel_buffer.seek(0) 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 # Log export action with employee name column and verification status coloring
try: try:
logger_handler.logger.info(f"Excel export with employee names and verification status coloring generated by user {session.get('username', 'unknown')}") 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: except Exception:
print(f"⚠️ Logging error (non-critical): {log_error}") pass
return excel_buffer return excel_buffer
except Exception as e: except Exception as e:
print(f"Error creating Excel export: {e}") logger_handler.logger.error(f"Error creating Excel export: {e}", exc_info=True)
print(f"❌ Traceback: {traceback.format_exc()}")
# Log error # Log error
try: try:
@@ -2347,6 +2331,6 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
stack_trace=traceback.format_exc() stack_trace=traceback.format_exc()
) )
except Exception as log_error: 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 return None
+3 -1
View File
@@ -161,6 +161,7 @@ def login():
logger_handler.logger.warning(f"Failed login attempt for username: {username}") logger_handler.logger.warning(f"Failed login attempt for username: {username}")
except Exception as e: except Exception as e:
db.session.rollback()
logger_handler.log_database_error('user_login', e) logger_handler.log_database_error('user_login', e)
logger_handler.logger.error(f"Login error for username '{username}': {e}") logger_handler.logger.error(f"Login error for username '{username}': {e}")
flash('Login error. Please try again.', 'error') flash('Login error. Please try again.', 'error')
@@ -201,7 +202,7 @@ def logout():
def profile(): def profile():
"""User profile management with logging""" """User profile management with logging"""
try: try:
user = User.query.get(session['user_id']) user = db.session.get(User, session['user_id'])
if request.method == 'POST': if request.method == 'POST':
form_type = request.form.get('form_type') form_type = request.form.get('form_type')
@@ -259,6 +260,7 @@ def profile():
return render_template('profile.html', user=user) return render_template('profile.html', user=user)
except Exception as e: except Exception as e:
db.session.rollback()
logger_handler.log_database_error('profile_update', e) logger_handler.log_database_error('profile_update', e)
flash('Profile update failed. Please try again.', 'error') flash('Profile update failed. Please try again.', 'error')
return redirect(url_for('dashboard.dashboard')) return redirect(url_for('dashboard.dashboard'))
+1 -3
View File
@@ -26,7 +26,7 @@ bp = Blueprint('dashboard', __name__)
def dashboard(): def dashboard():
"""Enhanced project-centric dashboard with search filters""" """Enhanced project-centric dashboard with search filters"""
try: try:
user = User.query.get(session['user_id']) user = db.session.get(User, session['user_id'])
# Get search parameters from URL # Get search parameters from URL
search_name = request.args.get('search_name', '').strip() search_name = request.args.get('search_name', '').strip()
@@ -71,7 +71,6 @@ def dashboard():
except Exception as e: except Exception as e:
logger_handler.log_database_error('dashboard_load', e) logger_handler.log_database_error('dashboard_load', e)
print(f"Error loading dashboard: {e}")
flash('Error loading dashboard. Please try again.', 'error') flash('Error loading dashboard. Please try again.', 'error')
return redirect(url_for('auth.login')) return redirect(url_for('auth.login'))
@@ -127,7 +126,6 @@ def project_qr_codes(project_id):
except Exception as e: except Exception as e:
logger_handler.log_database_error('project_qr_codes_view', 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') flash('Error loading project QR codes. Please try again.', 'error')
return redirect(url_for('dashboard.dashboard')) return redirect(url_for('dashboard.dashboard'))
+2 -2
View File
@@ -134,7 +134,7 @@ def create_employee():
db.session.commit() db.session.commit()
# Log employee creation with project info # 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}" project_name = project.name if project else f"Project {contract_id_int}"
logger_handler.logger.info( logger_handler.logger.info(
f"User {session['username']} created new employee: " f"User {session['username']} created new employee: "
@@ -213,7 +213,7 @@ def edit_employee(employee_index):
db.session.commit() db.session.commit()
# Log employee update with project info # 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}" project_name = project.name if project else f"Project {contract_id_int}"
logger_handler.logger.info( logger_handler.logger.info(
f"User {session['username']} updated employee: " f"User {session['username']} updated employee: "
+36 -48
View File
@@ -44,7 +44,7 @@ def payroll_dashboard():
flash('Access denied. Only administrators and payroll staff can access payroll features.', 'error') flash('Access denied. Only administrators and payroll staff can access payroll features.', 'error')
return redirect(url_for('dashboard.dashboard')) return redirect(url_for('dashboard.dashboard'))
print("📊 Loading payroll dashboard") logger_handler.logger.debug("Loading payroll dashboard")
# Log payroll dashboard access # Log payroll dashboard access
logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed payroll dashboard") logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed payroll dashboard")
@@ -65,9 +65,9 @@ def payroll_dashboard():
projects = [] projects = []
try: try:
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() 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: 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 # Get attendance records for the period
attendance_records = [] attendance_records = []
@@ -90,12 +90,12 @@ def payroll_dashboard():
# Apply project filter if selected # Apply project filter if selected
if project_filter and project_filter != '': if project_filter and project_filter != '':
query = query.filter(QRCode.project_id == int(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) query = query.order_by(AttendanceData.employee_id, AttendanceData.check_in_date, AttendanceData.check_in_time)
attendance_records = query.all() 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 # Calculate working hours if we have records
if attendance_records: if attendance_records:
@@ -103,13 +103,13 @@ def payroll_dashboard():
working_hours_data = calculator.calculate_all_employees_hours( working_hours_data = calculator.calculate_all_employees_hours(
start_date, end_date, attendance_records 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: 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') flash('Invalid date format. Please use YYYY-MM-DD format.', 'error')
except Exception as e: 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) logger_handler.log_database_error('payroll_calculation', e)
flash('Error calculating working hours. Please check the server logs.', 'error') 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 if row[1]: # Only add if we got a name
employee_names[str(row[0])] = row[1] 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: except Exception as e:
print(f"⚠️ Could not load employee names: {e}") logger_handler.logger.warning(f"Could not load employee names: {e}", exc_info=True)
import traceback
print(f"⚠️ Traceback: {traceback.format_exc()}")
# Continue without names - will use employee IDs # Continue without names - will use employee IDs
# Get selected project name for display # Get selected project name for display
selected_project_name = '' selected_project_name = ''
if project_filter: if project_filter:
try: try:
selected_project = Project.query.get(int(project_filter)) selected_project = db.session.get(Project, int(project_filter))
if selected_project: if selected_project:
selected_project_name = selected_project.name selected_project_name = selected_project.name
except Exception as e: 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', return render_template('payroll_dashboard.html',
working_hours_data=working_hours_data, working_hours_data=working_hours_data,
@@ -165,9 +163,7 @@ def payroll_dashboard():
user_role=user_role) user_role=user_role)
except Exception as e: except Exception as e:
print(f"Error loading payroll dashboard: {e}") logger_handler.logger.error(f"Error loading payroll dashboard: {e}", exc_info=True)
import traceback
print(f"❌ Traceback: {traceback.format_exc()}")
logger_handler.log_flask_error( logger_handler.log_flask_error(
'payroll_dashboard_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') flash('Access denied. Only administrators and payroll staff can export payroll data.', 'error')
return redirect(url_for('payroll.payroll_dashboard')) 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 # Get parameters from form
date_from = request.form.get('date_from') date_from = request.form.get('date_from')
@@ -222,7 +218,7 @@ def export_payroll_excel():
# Apply project filter if selected # Apply project filter if selected
if project_filter and project_filter != '': if project_filter and project_filter != '':
query = query.filter(QRCode.project_id == int(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) 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_data.qr_code = qr_code
attendance_records.append(attendance_data) 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: if not attendance_records:
flash('No attendance records found for the selected date range and project.', 'warning') flash('No attendance records found for the selected date range and project.', 'warning')
return redirect(url_for('payroll.payroll_dashboard')) 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 # Get employee names using the same method as dashboard
employee_names = {} employee_names = {}
@@ -264,24 +260,22 @@ def export_payroll_excel():
if row[1]: # Only add if we got a name if row[1]: # Only add if we got a name
employee_names[str(row[0])] = row[1] 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: except Exception as e:
print(f"⚠️ Could not load employee names for export: {e}") logger_handler.logger.warning(f"Could not load employee names for export: {e}", exc_info=True)
import traceback
print(f"⚠️ Traceback: {traceback.format_exc()}")
# Get project name for enhanced reports and filename # Get project name for enhanced reports and filename
project_name = None project_name = None
project_name_for_filename = '' project_name_for_filename = ''
if project_filter: if project_filter:
try: try:
project = Project.query.get(int(project_filter)) project = db.session.get(Project, int(project_filter))
if project: if project:
project_name = project.name project_name = project.name
project_name_for_filename = f"_{project.name.replace(' ', '_')}" project_name_for_filename = f"_{project.name.replace(' ', '_')}"
except Exception as e: 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 # Generate Excel file based on report type
excel_file = None excel_file = None
@@ -289,7 +283,7 @@ def export_payroll_excel():
if report_type == 'enhanced': if report_type == 'enhanced':
# Use enhanced exporter for SP/PW reports # 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: try:
from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter
exporter = EnhancedPayrollExcelExporter(company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System')) 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 start_date, end_date, attendance_records, employee_names, project_name
) )
filename_prefix = 'enhanced_payroll_report' filename_prefix = 'enhanced_payroll_report'
print("Enhanced payroll report created successfully") logger_handler.logger.info("Enhanced payroll report created successfully")
except ImportError: 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 # Fall back to standard exporter
exporter = PayrollExcelExporter( exporter = PayrollExcelExporter(
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'), company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'),
@@ -310,7 +304,7 @@ def export_payroll_excel():
) )
filename_prefix = 'payroll_report' filename_prefix = 'payroll_report'
except Exception as e: 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 # Fall back to standard exporter
exporter = PayrollExcelExporter( exporter = PayrollExcelExporter(
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'), 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': elif report_type == 'detailed_sp_pw':
# Detailed daily SP/PW breakdown # 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: try:
from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter
exporter = EnhancedPayrollExcelExporter(company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System')) 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 start_date, end_date, attendance_records, employee_names
) )
filename_prefix = 'detailed_sp_pw_report' 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: 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 # Fall back to standard detailed report
exporter = PayrollExcelExporter( exporter = PayrollExcelExporter(
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'), 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' filename_prefix = 'detailed_hours_report'
except Exception as e: 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 # Fall back to standard detailed report
exporter = PayrollExcelExporter( exporter = PayrollExcelExporter(
company_name=current_app.config.get('COMPANY_NAME', 'QR Code Management System'), 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') 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' 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 # Log successful export
logger_handler.logger.info(f"Payroll Excel export generated by user {session.get('username', 'unknown')}: {filename}") 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')) return redirect(url_for('payroll.payroll_dashboard'))
except Exception as e: except Exception as e:
print(f"Error in export_payroll_excel route: {e}") logger_handler.logger.error(f"Error in export_payroll_excel route: {e}", exc_info=True)
import traceback
print(f"❌ Traceback: {traceback.format_exc()}")
logger_handler.log_flask_error( logger_handler.log_flask_error(
'payroll_excel_export_error', 'payroll_excel_export_error',
@@ -484,7 +476,7 @@ def calculate_working_hours_api():
}) })
except Exception as e: 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( logger_handler.log_flask_error(
'working_hours_api_error', 'working_hours_api_error',
str(e), str(e),
@@ -541,11 +533,9 @@ def get_miss_punch_details(employee_id):
employee_row = employee_query.fetchone() employee_row = employee_query.fetchone()
employee_name = employee_row.full_name if employee_row and employee_row.full_name else f"Employee {employee_id}" 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: except Exception as e:
print(f"⚠️ Could not load employee name for ID {employee_id}: {e}") logger_handler.logger.warning(f"Could not load employee name for ID {employee_id}: {e}", exc_info=True)
import traceback
print(f"⚠️ Traceback: {traceback.format_exc()}")
employee_name = f"Employee {employee_id}" employee_name = f"Employee {employee_id}"
# Get attendance records for the employee within the period # Get attendance records for the employee within the period
@@ -647,9 +637,7 @@ def get_miss_punch_details(employee_id):
}) })
except Exception as e: except Exception as e:
print(f"Error in get_miss_punch_details: {e}") logger_handler.logger.error(f"Error in get_miss_punch_details: {e}", exc_info=True)
import traceback
print(f"❌ Traceback: {traceback.format_exc()}")
logger_handler.log_flask_error( logger_handler.log_flask_error(
'miss_punch_details_api_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}" return row[0] if row else f"Employee {employee_id}"
except Exception as e: 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}" return f"Employee {employee_id}"
def get_qr_code_checkin_count(qr_code_id): def get_qr_code_checkin_count(qr_code_id):
+62 -99
View File
@@ -86,9 +86,9 @@ def create_qr_code():
address_latitude = float(latitude) address_latitude = float(latitude)
address_longitude = float(longitude) address_longitude = float(longitude)
has_coordinates = True 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: except (ValueError, TypeError) as e:
print(f"⚠️ Invalid coordinates format: {e}") logger_handler.logger.warning(f"Invalid coordinates format: {e}")
address_latitude = None address_latitude = None
address_longitude = None address_longitude = None
has_coordinates = False has_coordinates = False
@@ -98,7 +98,7 @@ def create_qr_code():
if project_id: if project_id:
try: try:
project_id = int(project_id) 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: if not project or not project.active_status:
flash('Selected project is not valid or inactive.', 'error') flash('Selected project is not valid or inactive.', 'error')
return render_template('create_qr_code.html', return render_template('create_qr_code.html',
@@ -192,7 +192,7 @@ def create_qr_code():
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('qr_code_creation', e) logger_handler.log_database_error('qr_code_creation', e)
flash('QR Code creation failed. Please try again.', 'error') 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 # Get active projects and styles for dropdown
projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() 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(): if new_project_id and new_project_id.strip():
try: try:
new_project_id = int(new_project_id) 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: if project and project.active_status:
qr_code.project_id = new_project_id qr_code.project_id = new_project_id
else: else:
@@ -518,16 +518,16 @@ def delete_qr_code(qr_id):
"""Permanently delete QR code (Admin only) - Hard delete - PRESERVING EXACT ROUTE""" """Permanently delete QR code (Admin only) - Hard delete - PRESERVING EXACT ROUTE"""
try: try:
qr_code = QRCode.query.get_or_404(qr_id) 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': if request.method == 'POST':
qr_name = qr_code.name qr_name = qr_code.name
qr_code_id = qr_code.id 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 # Check if QR exists before delete
before_count = QRCode.query.count() 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 # Log QR code deletion before actual deletion
logger_handler.log_qr_code_deleted( logger_handler.log_qr_code_deleted(
@@ -538,29 +538,27 @@ def delete_qr_code(qr_id):
# Delete the QR code # Delete the QR code
db.session.delete(qr_code) db.session.delete(qr_code)
print("💾 Called db.session.delete()")
db.session.commit() db.session.commit()
print("💾 Called db.session.commit()")
# Check count after delete # Check count after delete
after_count = QRCode.query.count() after_count = QRCode.query.count()
print(f"📊 QR count after delete: {after_count}") logger_handler.logger.debug(f"QR count after delete: {after_count}")
print(f"✅ DELETE SUCCESS! Removed {before_count - after_count} records") 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') flash(f'QR code "{qr_name}" has been permanently deleted!', 'success')
return redirect(url_for('dashboard.dashboard')) return redirect(url_for('dashboard.dashboard'))
# GET request - show confirmation page # 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) return render_template('confirm_delete_qr.html', qr_code=qr_code)
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('qr_code_deletion', e) logger_handler.log_database_error('qr_code_deletion', e)
print(f"❌ ERROR in delete route: {e}") logger_handler.logger.error(f"Error in QR code delete route (ID: {qr_id}): {e}", exc_info=True)
print(f"❌ Exception type: {type(e)}")
print(f"❌ Traceback: {traceback.format_exc()}")
flash('Error deleting QR code. Please try again.', 'error') flash('Error deleting QR code. Please try again.', 'error')
return redirect(url_for('dashboard.dashboard')) return redirect(url_for('dashboard.dashboard'))
@@ -603,23 +601,19 @@ def qr_checkin(qr_url):
PRESERVES coordinate-to-address conversion functionality PRESERVES coordinate-to-address conversion functionality
""" """
try: try:
print(f"\n🚀 STARTING ENHANCED CHECK-IN PROCESS") logger_handler.logger.debug(f"Starting check-in process for QR URL: {qr_url}")
print(f" QR URL: {qr_url}")
print(f" Timestamp: {datetime.now()}")
# Find QR code by URL # Find QR code by URL
qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first() qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first()
if not qr_code: 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({ return jsonify({
'success': False, 'success': False,
'message': 'QR code not found or inactive.' 'message': 'QR code not found or inactive.'
}), 404 }), 404
print(f"Found QR code: {qr_code.name} (ID: {qr_code.id})") logger_handler.logger.debug(f"Found QR code: {qr_code.name} (ID: {qr_code.id}), location: {qr_code.location}")
print(f" Location: {qr_code.location}")
print(f" QR Address: {qr_code.location_address}")
# Get and validate employee ID # Get and validate employee ID
employee_id = request.form.get('employee_id', '').strip() 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 # Check if 30 minutes have passed since the last check-in
if recent_checkin_datetime > the_last_checkin_time: if recent_checkin_datetime > the_last_checkin_time:
minutes_remaining = time_interval - int((current_time - recent_checkin_datetime).total_seconds() / 60) 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}") logger_handler.logger.info(f"Too soon for another {qr_code.location_event} for employee {employee_id}: {minutes_remaining} minutes remaining")
print(f" Last {qr_code.location_event}: {recent_checkin.check_in_time.strftime('%H:%M')}")
print(f" Minutes remaining: {minutes_remaining}")
return jsonify({ return jsonify({
'success': False, '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")}." f"Puedes volver a registrarte en {minutes_remaining} minutos. El último registro fue a las {recent_checkin.check_in_time.strftime("%H:%M")}."
}), 400 }), 400
else: 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: 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 # Process location data with coordinate-to-address conversion
location_data = process_location_data_enhanced(request.form) 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) device_info = detect_device_info(user_agent_string)
client_ip = get_client_ip() client_ip = get_client_ip()
print(f"📱 Device Info: {device_info}") logger_handler.logger.debug(f"Check-in device: {device_info}, IP: {client_ip}")
print(f"🌐 IP Address: {client_ip}")
print(f"📍 Location Data: {location_data}")
# Create attendance record # Create attendance record
print(f"\n💾 CREATING ATTENDANCE RECORD:") logger_handler.logger.debug("Creating attendance record")
attendance = AttendanceData( attendance = AttendanceData(
qr_code_id=qr_code.id, qr_code_id=qr_code.id,
@@ -699,39 +689,29 @@ def qr_checkin(qr_url):
verification_status=None 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 # Calculate location accuracy
print(f"\n🎯 CALCULATING LOCATION ACCURACY WITH ENHANCED DEBUG...") logger_handler.logger.debug(
print(f" 📊 QR Code Details:") f"Location accuracy check: QR='{qr_code.name}' (ID={qr_code.id}), "
print(f" ID: {qr_code.id}") f"lat={location_data['latitude']}, lng={location_data['longitude']}, "
print(f" Name: {qr_code.name}") f"source={location_data['source']}"
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']}")
location_accuracy = None location_accuracy = None
try: try:
# Check if we have the required data # Check if we have the required data
if not qr_code.location_address: if not qr_code.location_address:
print(f"QR code location_address is empty or None") logger_handler.logger.warning(f"QR code location_address is empty or None for QR ID: {qr_code.id}")
print(f" QR Code location_address value: '{qr_code.location_address}'")
elif not location_data['address'] and not (location_data['latitude'] and location_data['longitude']): elif not location_data['address'] and not (location_data['latitude'] and location_data['longitude']):
print(f"❌ No check-in address or coordinates available") logger_handler.logger.warning(
print(f" Check-in address: '{location_data['address']}'") f"No check-in address or coordinates available: "
print(f" Check-in coords: {location_data['latitude']}, {location_data['longitude']}") f"address={location_data['address']!r}, "
f"coords={location_data['latitude']}, {location_data['longitude']}"
)
else: 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( location_accuracy = calculate_location_accuracy_enhanced(
qr_address=qr_code.location_address, qr_address=qr_code.location_address,
@@ -740,30 +720,28 @@ def qr_checkin(qr_url):
checkin_lng=location_data['longitude'] 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: if location_accuracy is not None:
attendance.location_accuracy = location_accuracy attendance.location_accuracy = location_accuracy
accuracy_level = get_location_accuracy_level_enhanced(location_accuracy) accuracy_level = get_location_accuracy_level_enhanced(location_accuracy)
print(f"Location accuracy set successfully: {location_accuracy:.4f} miles ({accuracy_level})") logger_handler.logger.debug(f"Location accuracy set: {location_accuracy:.4f} miles ({accuracy_level})")
print(f"📊 Final attendance.location_accuracy value: {attendance.location_accuracy}")
else: 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 # CHECK DISTANCE THRESHOLD FOR PHOTO VERIFICATION
print(f"\n📸 CHECKING PHOTO VERIFICATION REQUIREMENT:") logger_handler.logger.debug(f"Photo verification enabled: {current_app.config.get('PHOTO_VERIFICATION_ENABLED', True)}")
print(f" Photo Verification Enabled: {current_app.config.get('PHOTO_VERIFICATION_ENABLED', True)}")
requires_verification = False requires_verification = False
verification_photo_data = None 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): 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 # Check if photo was provided
verification_photo_data = request.form.get('verification_photo', None) verification_photo_data = request.form.get('verification_photo', None)
if verification_photo_data: 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) # Validate photo data (basic validation)
if verification_photo_data.startswith('data:image/'): if verification_photo_data.startswith('data:image/'):
@@ -771,16 +749,16 @@ def qr_checkin(qr_url):
attendance.verification_required = True attendance.verification_required = True
attendance.verification_status = 'pending' attendance.verification_status = 'pending'
attendance.verification_timestamp = datetime.now() 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: else:
print(f"⚠️ Invalid photo format provided") logger_handler.logger.warning(f"Invalid photo format provided for employee {employee_id}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'message': 'Invalid photo format. Please try again.', 'message': 'Invalid photo format. Please try again.',
'requires_verification': True 'requires_verification': True
}), 400 }), 400
else: 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({ return jsonify({
'success': False, 'success': False,
'message': 'Photo verification required. Distance from location is too far.', '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) 'threshold': current_app.config.get('DISTANCE_THRESHOLD_FOR_VERIFICATION', 0.3)
}), 400 }), 400
else: 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: except Exception as e:
print(f"Error in location accuracy calculation: {e}") logger_handler.logger.error(f"Error in location accuracy calculation: {e}", exc_info=True)
print(f"❌ Full traceback: {traceback.format_exc()}")
# ENHANCED DEBUG: Save to database with verification # ENHANCED DEBUG: Save to database with verification
try: try:
print(f"\n💾 SAVING TO DATABASE...") logger_handler.logger.debug(f"Saving attendance record: employee={attendance.employee_id}, location={attendance.location_name}, accuracy={attendance.location_accuracy}")
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}")
db.session.add(attendance) db.session.add(attendance)
db.session.commit() db.session.commit()
@@ -819,14 +789,11 @@ def qr_checkin(qr_url):
) )
# VERIFICATION: Read back from database # VERIFICATION: Read back from database
saved_record = AttendanceData.query.get(attendance.id) saved_record = db.session.get(AttendanceData, attendance.id)
print(f"✅ Successfully saved attendance record with ID: {attendance.id}") logger_handler.logger.info(f"Saved attendance record ID: {attendance.id}, db accuracy: {saved_record.location_accuracy}")
print(f"📊 Verification - location accuracy in database: {saved_record.location_accuracy}")
if saved_record.location_accuracy != attendance.location_accuracy: if saved_record.location_accuracy != attendance.location_accuracy:
print(f"⚠️ WARNING: Database value differs from object value!") logger_handler.logger.warning(f"DB accuracy mismatch: object={attendance.location_accuracy}, db={saved_record.location_accuracy}")
print(f" Object value: {attendance.location_accuracy}")
print(f" Database value: {saved_record.location_accuracy}")
# Add enhanced logging for location accuracy save # Add enhanced logging for location accuracy save
if attendance.location_accuracy is not None: 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" checkin_sequence_text = f"{qr_code.location_event} details"
except Exception as e: except Exception as e:
print(f"Database error: {e}") logger_handler.logger.error(f"Database error saving attendance record: {e}", exc_info=True)
print(f"❌ Full traceback: {traceback.format_exc()}")
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('checkin_save', e) logger_handler.log_database_error('checkin_save', e)
return jsonify({ return jsonify({
@@ -879,14 +845,11 @@ def qr_checkin(qr_url):
response_data['data']['coordinates'] = f"{location_data['latitude']:.10f}, {location_data['longitude']:.10f}" response_data['data']['coordinates'] = f"{location_data['latitude']:.10f}, {location_data['longitude']:.10f}"
# Enhanced logging for successful check-in with all details # Enhanced logging for successful check-in with all details
print(f"✅ Check-in completed successfully") logger_handler.logger.info(
print(f" Employee ID: {attendance.employee_id}") f"Check-in completed: employee={attendance.employee_id}, "
print(f" Time: {attendance.check_in_time.strftime('%I:%M %p')}") f"action={qr_code.location_event}, location={attendance.location_name}, "
print(f" Date: {attendance.check_in_date.strftime('%B %d, %Y')}") f"time={attendance.check_in_time.strftime('%H:%M')}, count_today={today_checkin_count}"
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}")
# Log to database for audit trail # 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}") 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 return jsonify(response_data), 200
except Exception as e: except Exception as e:
print(f"❌ Unexpected error in check-in process: {e}") db.session.rollback()
print(f"❌ Traceback: {traceback.format_exc()}") logger_handler.logger.error(f"Unexpected error in check-in process (QR: {qr_url}): {e}", exc_info=True)
return jsonify({ return jsonify({
'success': False, 'success': False,
@@ -925,7 +888,7 @@ def toggle_qr_status(qr_id):
except Exception as e: except Exception as e:
db.session.rollback() 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({ return jsonify({
'success': False, 'success': False,
'message': 'Error updating QR code status. Please try again.' 'message': 'Error updating QR code status. Please try again.'
@@ -996,7 +959,7 @@ def activate_qr_code(qr_id):
except Exception as e: except Exception as e:
db.session.rollback() 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({ return jsonify({
'success': False, 'success': False,
'message': 'Error activating QR code. Please try again.' 'message': 'Error activating QR code. Please try again.'
@@ -1021,7 +984,7 @@ def deactivate_qr_code(qr_id):
except Exception as e: except Exception as e:
db.session.rollback() 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({ return jsonify({
'success': False, 'success': False,
'message': 'Error deactivating QR code. Please try again.' 'message': 'Error deactivating QR code. Please try again.'
-7
View File
@@ -202,9 +202,6 @@ def qr_statistics():
except Exception as e: except Exception as e:
# Log the error using the correct method # Log the error using the correct method
logger_handler.log_database_error('statistics_page_error', e) 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') flash('Error loading statistics. Please try again.', 'error')
return redirect(url_for('dashboard.dashboard')) return redirect(url_for('dashboard.dashboard'))
@@ -297,15 +294,11 @@ def export_statistics():
except Exception as e: except Exception as e:
logger_handler.log_database_error('statistics_export_error', e) logger_handler.log_database_error('statistics_export_error', e)
print(f"❌ Error exporting statistics: {e}")
return jsonify({'error': 'Export failed'}), 500 return jsonify({'error': 'Export failed'}), 500
except Exception as e: except Exception as e:
# Log the error # Log the error
logger_handler.log_database_error('statistics_page_error', e) 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') flash('Error loading statistics. Please try again.', 'error')
return redirect(url_for('dashboard.dashboard')) return redirect(url_for('dashboard.dashboard'))
+82 -67
View File
@@ -104,7 +104,7 @@ def time_attendance_dashboard():
pass pass
except Exception as e: except Exception as e:
# Database table doesn't exist yet or other error - use defaults # 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 pass
return render_template('time_attendance_dashboard.html', 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_invalid_review = request.form.get('from_invalid_review', 'false').lower() == 'true'
coming_from_duplicate_review = request.form.get('from_duplicate_review', 'false').lower() == 'true' coming_from_duplicate_review = request.form.get('from_duplicate_review', 'false').lower() == 'true'
print(f"\n🔍 IMPORT FLOW DEBUG:") logger_handler.logger.debug(
print(f" Coming from invalid review: {coming_from_invalid_review}") f"Import flow: coming_from_invalid={coming_from_invalid_review}, "
print(f" Coming from duplicate review: {coming_from_duplicate_review}") f"coming_from_duplicate={coming_from_duplicate_review}"
)
if coming_from_invalid_review or coming_from_duplicate_review: if coming_from_invalid_review or coming_from_duplicate_review:
# Retrieve file from session # Retrieve file from session
@@ -157,8 +158,7 @@ def import_time_attendance():
session.pop('pending_import_filename', None) session.pop('pending_import_filename', None)
return redirect(url_for('time_attendance.import_time_attendance')) return redirect(url_for('time_attendance.import_time_attendance'))
print(f"Retrieved file from session: {filename}") logger_handler.logger.debug(f"Retrieved file from session: {filename}, exists={os.path.exists(temp_path)}")
print(f"✅ Temp path exists: {os.path.exists(temp_path)}")
else: else:
# Normal file upload flow - now supports multiple files # Normal file upload flow - now supports multiple files
@@ -199,8 +199,7 @@ def import_time_attendance():
temp_paths.append(temp_path) temp_paths.append(temp_path)
filenames.append(filename) filenames.append(filename)
print(f"Uploaded file {len(temp_paths)}: {filename}") logger_handler.logger.debug(f"Uploaded file {len(temp_paths)}: {filename} saved to {temp_path}")
print(f"✅ Saved to: {temp_path}")
if len(temp_paths) == 0: if len(temp_paths) == 0:
flash('No valid files selected.', 'error') 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 temp_path = temp_paths[0] if len(temp_paths) == 1 else temp_paths
filename = filenames[0] if len(filenames) == 1 else ', '.join(filenames) 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 # Determine if we're processing multiple files
is_multiple_files = session.get('pending_import_files_multiple', False) is_multiple_files = session.get('pending_import_files_multiple', False)
@@ -230,7 +229,7 @@ def import_time_attendance():
# Single file mode (existing behavior) # Single file mode (existing behavior)
files_to_process = [(temp_path, filename)] 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: try:
import_service = TimeAttendanceImportService(db, logger_handler) 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_duplicates = request.form.get('analyze_duplicates', 'false').lower() == 'true'
analyze_invalid = request.form.get('analyze_invalid', 'false').lower() == 'true' analyze_invalid = request.form.get('analyze_invalid', 'false').lower() == 'true'
print(f"📋 Import Options:") logger_handler.logger.debug(
print(f" Skip duplicates: {skip_duplicates}") f"Import options: skip_duplicates={skip_duplicates}, validate_only={validate_only}, "
print(f" Validate only: {validate_only}") f"analyze_duplicates={analyze_duplicates}, analyze_invalid={analyze_invalid}, "
print(f" Analyze duplicates: {analyze_duplicates}") f"coming_from_invalid={coming_from_invalid_review}"
print(f" Analyze invalid: {analyze_invalid}") )
print(f" Coming from invalid review: {coming_from_invalid_review}")
# Store combined results for multiple files # Store combined results for multiple files
all_results = { all_results = {
@@ -263,14 +261,14 @@ def import_time_attendance():
# Process each file # Process each file
for file_index, (current_temp_path, current_filename) in enumerate(files_to_process, 1): 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 import_result = None # Initialize to prevent reference errors
try: try:
# For multiple files, skip review screens and import directly # For multiple files, skip review screens and import directly
if is_multiple_files: 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 # Validate the file first
validation_result = import_service.validate_excel_file(current_temp_path) 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), 'imported': import_result.get('imported_records', 0),
'batch_id': import_result.get('batch_id', '') '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: elif import_result:
# Import ran but failed # Import ran but failed
all_results['failed_files'] += 1 all_results['failed_files'] += 1
@@ -322,7 +320,7 @@ def import_time_attendance():
}) })
except Exception as file_error: 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}") logger_handler.logger.error(f"Error processing file {current_filename}: {file_error}")
all_results['failed_files'] += 1 all_results['failed_files'] += 1
all_results['errors'].append(f"{current_filename}: {str(file_error)}") 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): if is_multiple_files and os.path.exists(current_temp_path):
try: try:
os.remove(current_temp_path) 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: 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 # After processing all files
if is_multiple_files: if is_multiple_files:
@@ -374,12 +372,11 @@ def import_time_attendance():
session.pop('pending_import_filename', None) session.pop('pending_import_filename', None)
session.pop('pending_import_files_multiple', None) session.pop('pending_import_files_multiple', None)
print(f"\n📊 Batch Import Summary:") logger_handler.logger.info(
print(f" Total files: {all_results['total_files']}") f"Batch import summary: files={all_results['total_files']}, "
print(f" Successful: {all_results['successful_files']}") f"successful={all_results['successful_files']}, failed={all_results['failed_files']}, "
print(f" Failed: {all_results['failed_files']}") f"imported={all_results['total_imported']}, duplicates={all_results['total_duplicates']}"
print(f" Total imported: {all_results['total_imported']}") )
print(f" Total duplicates: {all_results['total_duplicates']}")
return redirect(url_for('time_attendance.time_attendance_dashboard')) 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 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: if analyze_duplicates and not force_import_hashes and not coming_from_invalid_review and not coming_from_duplicate_review:
print("🔍 Analyzing for duplicates...") logger_handler.logger.debug("Analyzing for duplicates")
duplicate_analysis = import_service.analyze_for_duplicates(temp_path) 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: if duplicate_analysis['duplicate_records'] > 0:
print(f"⚠️ Found {duplicate_analysis['duplicate_records']} duplicates") logger_handler.logger.info(f"Found {duplicate_analysis['duplicate_records']} duplicates")
# Get project_id from form
project_id = request.form.get('project_id')
# Show duplicate review page # Show duplicate review page
return render_template('time_attendance_duplicate_review.html', return render_template('time_attendance_duplicate_review.html',
analysis=duplicate_analysis, analysis=duplicate_analysis,
filename=filename, filename=filename,
project_id=project_id) project_id=project_id_for_analysis)
else: else:
print("No duplicates found") logger_handler.logger.debug("No duplicates found")
flash('No duplicates found. Proceeding with import.', 'info') flash('No duplicates found. Proceeding with import.', 'info')
# Check for invalid rows and show review if any (but not if coming from invalid review) # 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: if analyze_invalid and not coming_from_invalid_review:
print("🔍 Analyzing for invalid rows...") logger_handler.logger.debug("Analyzing for invalid rows")
invalid_analysis = import_service.analyze_for_invalid_rows(temp_path) 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: if invalid_analysis['invalid_rows'] > 0:
print(f"⚠️ Found {invalid_analysis['invalid_rows']} invalid rows") logger_handler.logger.info(f"Found {invalid_analysis['invalid_rows']} invalid rows")
# Get project_id from form
project_id = request.form.get('project_id')
# Show invalid row review page # Show invalid row review page
return render_template('time_attendance_invalid_review.html', return render_template('time_attendance_invalid_review.html',
analysis=invalid_analysis, analysis=invalid_analysis,
filename=filename, filename=filename,
project_id=project_id) project_id=project_id_for_analysis)
else: else:
print("All rows are valid") logger_handler.logger.debug("All rows are valid")
flash('All rows are valid. Proceeding with import.', 'info') flash('All rows are valid. Proceeding with import.', 'info')
# If coming from invalid review, skip validation (already done) # If coming from invalid review, skip validation (already done)
if not coming_from_invalid_review: if not coming_from_invalid_review:
print("🔍 Validating file...") logger_handler.logger.debug("Validating file")
# Validate file # Validate file
validation_result = import_service.validate_excel_file(temp_path) validation_result = import_service.validate_excel_file(temp_path)
if not validation_result['valid']: 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') flash(f"File validation failed: {'; '.join(validation_result['errors'])}", 'error')
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
return render_template('time_attendance_import.html', return render_template('time_attendance_import.html',
projects=projects,
validation_result=validation_result) validation_result=validation_result)
if validation_result['warnings']: if validation_result['warnings']:
@@ -439,15 +452,17 @@ def import_time_attendance():
flash(warning, 'warning') flash(warning, 'warning')
if validate_only: 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') flash(f"File validation successful! Found {validation_result['valid_rows']} valid records.", 'success')
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
return render_template('time_attendance_import.html', return render_template('time_attendance_import.html',
projects=projects,
validation_result=validation_result) validation_result=validation_result)
else: else:
print("⏭️ Skipping validation (already validated)") logger_handler.logger.debug("Skipping validation (already validated)")
# Proceed with import # 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}") import_source = request.form.get('import_source', f"Manual Import - {filename}")
project_id = request.form.get('project_id') project_id = request.form.get('project_id')
project_id = int(project_id) if project_id and project_id != '' else None 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']: if import_result['success']:
print(f"✅ Import successful!") logger_handler.logger.info(
print(f" Batch ID: {import_result['batch_id']}") f"Import successful: batch_id={import_result['batch_id']}, "
print(f" Imported: {import_result['imported_records']}/{import_result['total_records']}") f"imported={import_result['imported_records']}/{import_result['total_records']}, "
print(f" Duplicates: {import_result['duplicate_records']}") f"duplicates={import_result['duplicate_records']}, failed={import_result['failed_records']}"
print(f" Failed: {import_result['failed_records']}") )
logger_handler.logger.info( logger_handler.logger.info(
f"User {session['username']} successfully imported time attendance data - " f"User {session['username']} successfully imported time attendance data - "
@@ -496,36 +511,36 @@ def import_time_attendance():
os.remove(temp_path) os.remove(temp_path)
session.pop('pending_import_file', None) session.pop('pending_import_file', None)
session.pop('pending_import_filename', 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: 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', return render_template('time_attendance_import_result.html',
import_result=import_result) import_result=import_result)
else: 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') flash(f"Import failed: {'; '.join(import_result['errors'][:3])}", 'error')
if len(import_result['errors']) > 3: if len(import_result['errors']) > 3:
flash(f"...and {len(import_result['errors']) - 3} more errors", 'warning') flash(f"...and {len(import_result['errors']) - 3} more errors", 'warning')
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
return render_template('time_attendance_import.html', return render_template('time_attendance_import.html',
projects=projects,
import_result=import_result) import_result=import_result)
except Exception as import_error: except Exception as import_error:
print(f"Import exception: {import_error}") logger_handler.logger.error(f"Import exception: {import_error}", exc_info=True)
import traceback
print(f"❌ Traceback: {traceback.format_exc()}")
raise raise
except Exception as e: except Exception as e:
logger_handler.log_database_error('time_attendance_import', e) logger_handler.log_database_error('time_attendance_import', e)
print(f"Top-level exception: {e}") logger_handler.logger.error(f"Top-level import exception: {e}", exc_info=True)
import traceback
print(f"❌ Traceback: {traceback.format_exc()}")
flash('Import failed due to an unexpected error.', 'error') 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 # 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') @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 = '' project_name_for_filename = ''
if project_filter: if project_filter:
try: try:
project = Project.query.get(int(project_filter)) project = db.session.get(Project, int(project_filter))
if project: if project:
# Replace spaces and special characters with underscores # Replace spaces and special characters with underscores
project_name_safe = project.name.replace(' ', '_').replace('/', '_').replace('\\', '_') project_name_safe = project.name.replace(' ', '_').replace('/', '_').replace('\\', '_')
project_name_for_filename = f"{project_name_safe}_" project_name_for_filename = f"{project_name_safe}_"
except Exception as e: 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 # Log export
logger_handler.logger.info( logger_handler.logger.info(
@@ -1343,13 +1358,13 @@ def export_time_attendance_by_building():
project_name_for_filename = '' project_name_for_filename = ''
if project_filter: if project_filter:
try: try:
project = Project.query.get(int(project_filter)) project = db.session.get(Project, int(project_filter))
if project: if project:
# Replace spaces and special characters with underscores # Replace spaces and special characters with underscores
project_name_safe = project.name.replace(' ', '_').replace('/', '_').replace('\\', '_') project_name_safe = project.name.replace(' ', '_').replace('/', '_').replace('\\', '_')
project_name_for_filename = f"{project_name_safe}_" project_name_for_filename = f"{project_name_safe}_"
except Exception as e: 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 # Log export
logger_handler.logger.info( logger_handler.logger.info(
+220 -250
View File
@@ -120,6 +120,180 @@ def _qtr(decimal_hours: float) -> float:
base100 = convert_minutes_to_base100(rounded_minutes) base100 = convert_minutes_to_base100(rounded_minutes)
return round_base100_hours(base100) 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): 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""" """Generate Excel export with template format matching the provided template"""
from openpyxl import Workbook from openpyxl import Workbook
@@ -132,105 +306,17 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
ws = wb.active ws = wb.active
ws.title = "Sheet0" ws.title = "Sheet0"
# Get date range for calculations # Resolve date range and cap to 14-day window
if start_date_filter and end_date_filter: result = _resolve_date_range(start_date_filter, end_date_filter, records, 'TA Excel export')
# Convert string dates to date objects if needed if result is None:
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:
return None return None
start_date, end_date, records = result
# 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)]
# Import parse function at the beginning for work type detection # Import parse function at the beginning for work type detection
from working_hours_calculator import parse_employee_id_for_work_type from working_hours_calculator import parse_employee_id_for_work_type
# Convert TimeAttendance records to format expected by calculator # Convert TimeAttendance records to format expected by calculator
converted_records = [] converted_records = _convert_ta_records(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']}"
)
# Calculate working hours using WorkingHoursCalculator # Calculate working hours using WorkingHoursCalculator
calculator = WorkingHoursCalculator() calculator = WorkingHoursCalculator()
@@ -240,53 +326,23 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_
converted_records converted_records
) )
# Get employee names - map BASE employee IDs to names for consolidated display # Build employee name map (Lastname, Firstname keyed by base employee ID)
# Look up from Employee table using the numeric base_id to get the correct name, employee_names = _build_employee_name_map(records)
# 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}")
# Setup styles # Setup styles (shared objects)
# White bold text on black background for column header row (no border) _styles = _make_export_styles()
header_font = Font(name='Aptos Narrow', size=11, bold=True, color='FFFFFF') header_font = _styles['header_font']
header_fill = PatternFill(start_color='000000', end_color='000000', fill_type='solid') header_fill = _styles['header_fill']
data_font = Font(name='Aptos Narrow', size=11) data_font = _styles['data_font']
bold_font = Font(name='Aptos Narrow', size=11, bold=True) bold_font = _styles['bold_font']
border = Border( italic_bold_font = _styles['italic_bold_font']
left=Side(style='thin'), border = _styles['border']
right=Side(style='thin'), missed_punch_fill = _styles['missed_punch_fill']
top=Side(style='thin'), amber_fill = _styles['amber_fill']
bottom=Side(style='thin') border_day_middle = _styles['border_day_middle']
) border_day_last = _styles['border_day_last']
# CHANGED: Sample format uses ONLY a bottom border on the last row of each day group. border_day_single = _styles['border_day_single']
# Intermediate rows and first rows have no borders at all (no left/right/top). border_day_first = _styles['border_day_first']
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()
def get_day_border(row_position, total_rows): 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] del daily_location_data[_ndk][_co_loc]
if _ndk in daily_location_data and not daily_location_data[_ndk]: if _ndk in daily_location_data and not daily_location_data[_ndk]:
del daily_location_data[_ndk] del daily_location_data[_ndk]
print(f"\U0001f319 [TA Export] Overnight: moved checkout {_co.check_in_time} " logger_handler.logger.info(
f"from {_ndk} to {_dk} for employee {employee_id}") f"TA Export overnight shift: moved checkout {_co.check_in_time} "
f"from {_ndk} to {_dk} for employee {employee_id}"
)
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# END OVERNIGHT SHIFT DETECTION # 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 'work_type': wt, # None means regular
'used': False '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']] ins = [ri for ri in record_info if not ri['is_out']]
outs = [ri for ri in record_info if 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'] '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 # Sort pairs chronologically by the anchor record's time so that
# orphaned records (assembled last in Steps 2-3) appear in the # 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 = wb.active
ws.title = "Sheet0" ws.title = "Sheet0"
# Get date range for calculations # Resolve date range and cap to 14-day window
if start_date_filter and end_date_filter: result = _resolve_date_range(start_date_filter, end_date_filter, records, 'TA by-building Excel export')
if isinstance(start_date_filter, str): if result is None:
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 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 from working_hours_calculator import parse_employee_id_for_work_type
# Convert TimeAttendance records to format expected by calculator # Convert TimeAttendance records to format expected by calculator
converted_records = [] converted_records = _convert_ta_records(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)
# Group records by location (building) # Group records by location (building)
location_groups = {} location_groups = {}
@@ -1401,43 +1393,21 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename,
converted_records converted_records
) )
# Get employee names map # Build employee name map (Lastname, Firstname keyed by base employee ID)
# Look up from Employee table using the numeric base_id to get the correct name, employee_names = _build_employee_name_map(records)
# 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}")
# Setup styles # Setup styles (shared objects)
header_font = Font(name='Aptos Narrow', size=11, bold=True, color='FFFFFF') _styles = _make_export_styles()
header_fill = PatternFill(start_color='000000', end_color='000000', fill_type='solid') header_font = _styles['header_font']
data_font = Font(name='Aptos Narrow', size=11) header_fill = _styles['header_fill']
bold_font = Font(name='Aptos Narrow', size=11, bold=True) data_font = _styles['data_font']
italic_bold_font = Font(name='Aptos Narrow', size=11, bold=True, italic=True) bold_font = _styles['bold_font']
border = Border( italic_bold_font = _styles['italic_bold_font']
left=Side(style='thin'), border = _styles['border']
right=Side(style='thin'), missed_punch_fill = _styles['missed_punch_fill']
top=Side(style='thin'), amber_fill = _styles['amber_fill']
bottom=Side(style='thin') border_day_middle = _styles['border_day_middle']
) border_day_last = _styles['border_day_last']
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
# Write main headers # Write main headers
current_row = 1 current_row = 1
+17 -17
View File
@@ -118,7 +118,7 @@ def create_user():
try: try:
project_id = int(pid) project_id = int(pid)
# Verify project exists # Verify project exists
if Project.query.get(project_id): if db.session.get(Project, project_id):
valid_project_ids.append(project_id) valid_project_ids.append(project_id)
except (ValueError, TypeError): except (ValueError, TypeError):
logger_handler.logger.warning(f"Invalid project ID received: {pid}") logger_handler.logger.warning(f"Invalid project ID received: {pid}")
@@ -206,8 +206,8 @@ def create_user():
def delete_user(user_id): def delete_user(user_id):
"""Deactivate user (Admin only) - Fixed with proper validation""" """Deactivate user (Admin only) - Fixed with proper validation"""
try: try:
user_to_delete = User.query.get(user_id) user_to_delete = db.session.get(User, user_id)
current_user = User.query.get(session['user_id']) current_user = db.session.get(User, session['user_id'])
if not user_to_delete: if not user_to_delete:
flash('User not found.', 'error') flash('User not found.', 'error')
@@ -247,8 +247,8 @@ def delete_user(user_id):
def reactivate_user(user_id): def reactivate_user(user_id):
"""Reactivate a deactivated user (Admin only)""" """Reactivate a deactivated user (Admin only)"""
try: try:
user_to_reactivate = User.query.get(user_id) user_to_reactivate = db.session.get(User, user_id)
current_user = User.query.get(session['user_id']) current_user = db.session.get(User, session['user_id'])
if not user_to_reactivate: if not user_to_reactivate:
flash('User not found.', 'error') flash('User not found.', 'error')
@@ -277,8 +277,8 @@ def reactivate_user(user_id):
def promote_user(user_id): def promote_user(user_id):
"""Promote a staff user to admin (Admin only)""" """Promote a staff user to admin (Admin only)"""
try: try:
user_to_promote = User.query.get(user_id) user_to_promote = db.session.get(User, user_id)
current_user = User.query.get(session['user_id']) current_user = db.session.get(User, session['user_id'])
if not user_to_promote: if not user_to_promote:
flash('User not found.', 'error') flash('User not found.', 'error')
@@ -307,8 +307,8 @@ def promote_user(user_id):
def demote_user(user_id): def demote_user(user_id):
"""Demote an admin user to staff (Admin only)""" """Demote an admin user to staff (Admin only)"""
try: try:
user_to_demote = User.query.get(user_id) user_to_demote = db.session.get(User, user_id)
current_user = User.query.get(session['user_id']) current_user = db.session.get(User, session['user_id'])
if not user_to_demote: if not user_to_demote:
flash('User not found.', 'error') flash('User not found.', 'error')
@@ -437,7 +437,7 @@ def edit_user(user_id):
try: try:
project_id = int(pid) project_id = int(pid)
# Verify project exists # Verify project exists
if Project.query.get(project_id): if db.session.get(Project, project_id):
valid_project_ids.append(project_id) valid_project_ids.append(project_id)
except (ValueError, TypeError): except (ValueError, TypeError):
logger_handler.logger.warning(f"Invalid project ID received: {pid}") 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): def toggle_user_status(user_id):
"""Toggle user active status via AJAX (Admin only)""" """Toggle user active status via AJAX (Admin only)"""
try: try:
user_to_toggle = User.query.get(user_id) user_to_toggle = db.session.get(User, user_id)
current_user = User.query.get(session['user_id']) current_user = db.session.get(User, session['user_id'])
if not user_to_toggle: if not user_to_toggle:
return jsonify({ return jsonify({
@@ -619,8 +619,8 @@ def toggle_user_status(user_id):
def activate_user(user_id): def activate_user(user_id):
"""Activate a user (Admin only) - Alternative route""" """Activate a user (Admin only) - Alternative route"""
try: try:
user_to_activate = User.query.get(user_id) user_to_activate = db.session.get(User, user_id)
current_user = User.query.get(session['user_id']) current_user = db.session.get(User, session['user_id'])
if not user_to_activate: if not user_to_activate:
flash('User not found.', 'error') flash('User not found.', 'error')
@@ -652,8 +652,8 @@ def activate_user(user_id):
def deactivate_user(user_id): def deactivate_user(user_id):
"""Deactivate a user (Admin only) - Alternative route""" """Deactivate a user (Admin only) - Alternative route"""
try: try:
user_to_deactivate = User.query.get(user_id) user_to_deactivate = db.session.get(User, user_id)
current_user = User.query.get(session['user_id']) current_user = db.session.get(User, session['user_id'])
if not user_to_deactivate: if not user_to_deactivate:
flash('User not found.', 'error') 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)""" """Permanently delete user but preserve associated QR codes (Admin only)"""
try: try:
user_to_delete = User.query.get_or_404(user_id) 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 # Security checks
if user_to_delete.id == current_user.id: if user_to_delete.id == current_user.id:
+7 -1
View File
@@ -927,11 +927,17 @@ importForm.addEventListener('submit', async (e) => {
const result = msg.result; const result = msg.result;
setProgress(result.total_records, result.total_records, 'Import finished.'); setProgress(result.total_records, result.total_records, 'Import finished.');
let summary = '✅ Imported ' + result.imported_records + ' of ' + result.total_records + ' records.'; 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) if (result.duplicate_records > 0)
summary += ' Skipped ' + result.duplicate_records + ' duplicates.'; summary += ' Skipped ' + result.duplicate_records + ' duplicates.';
if (result.failed_records > 0) if (result.failed_records > 0)
summary += ' ⚠️ ' + result.failed_records + ' records failed.'; summary += ' ⚠️ ' + result.failed_records + ' records failed.';
}
const errors = (!result.success && result.errors && result.errors.length > 0) const errors = (!result.success && result.errors && result.errors.length > 0)
? result.errors ? result.errors
+111 -9
View File
@@ -321,12 +321,15 @@ class TimeAttendanceImportService:
self.logger.logger.error(f"Failed to get existing record hashes with data: {e}") self.logger.logger.error(f"Failed to get existing record hashes with data: {e}")
return {} 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: 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: Returns:
Dictionary containing duplicate analysis Dictionary containing duplicate analysis
@@ -356,6 +359,46 @@ class TimeAttendanceImportService:
df = df.dropna(how='all') df = df.dropna(how='all')
analysis_result['total_records'] = len(df) 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 # Get existing record hashes
existing_hashes = self._get_existing_record_hashes_with_data() existing_hashes = self._get_existing_record_hashes_with_data()
@@ -445,12 +488,15 @@ class TimeAttendanceImportService:
return analysis_result 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: 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: Returns:
Dictionary containing invalid row analysis Dictionary containing invalid row analysis
@@ -480,6 +526,46 @@ class TimeAttendanceImportService:
df = df.dropna(how='all') df = df.dropna(how='all')
analysis_result['total_rows'] = len(df) 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 # Analyze each row
invalid_list = [] invalid_list = []
valid_count = 0 valid_count = 0
@@ -663,11 +749,20 @@ class TimeAttendanceImportService:
if str(loc).strip() 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( project_locations = set(
qr.location qr.location.strip()
for qr in QRCode.query.filter_by(project_id=project_id) for qr in QRCode.query.filter_by(project_id=project_id)
.with_entities(QRCode.location).all() .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 # Find the first location in the file that is not in the project
@@ -677,10 +772,11 @@ class TimeAttendanceImportService:
) )
if unmatched: 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}' project_name = project_obj.name if project_obj else f'ID {project_id}'
error_msg = ( error_msg = (
f"The data in the file does not belong to the project '{project_name}'. " 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." f"Please verify the selected project or correct the file."
) )
import_results['errors'].append(error_msg) import_results['errors'].append(error_msg)
@@ -697,10 +793,16 @@ class TimeAttendanceImportService:
) )
except Exception as e: 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: if self.logger:
self.logger.logger.warning( self.logger.logger.error(
f"Could not perform project-location validation: {e}" f"Project-location validation error (failing closed): {e}",
exc_info=True
) )
return import_results
# ── End project-location validation ────────────────────────────────── # ── End project-location validation ──────────────────────────────────
# Track duplicates using hash # Track duplicates using hash