Mar. 23 2026 patch code 2
This commit is contained in:
@@ -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")
|
||||||
|
|
||||||
|
|||||||
+65
-55
@@ -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,66 +904,76 @@ 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
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|||||||
+26
-33
@@ -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)}'
|
||||||
|
|||||||
+130
-146
@@ -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,11 +1368,8 @@ 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,
|
||||||
'message': 'Error loading verification details'
|
'message': 'Error loading verification details'
|
||||||
@@ -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
|
||||||
|
|||||||
+4
-2
@@ -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
@@ -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'))
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -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: "
|
||||||
@@ -372,4 +372,4 @@ def employee_detail(employee_index):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger_handler.log_database_error('employee_detail', e)
|
logger_handler.log_database_error('employee_detail', e)
|
||||||
flash('Error loading employee details. Please try again.', 'error')
|
flash('Error loading employee details. Please try again.', 'error')
|
||||||
return redirect(url_for('employees.employees'))
|
return redirect(url_for('employees.employees'))
|
||||||
|
|||||||
+36
-48
@@ -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
@@ -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.'
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|
||||||
|
|||||||
+87
-72
@@ -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')
|
||||||
return render_template('time_attendance_import.html',
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
return render_template('time_attendance_import.html',
|
||||||
|
projects=projects,
|
||||||
validation_result=validation_result)
|
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')
|
||||||
return render_template('time_attendance_import.html',
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
return render_template('time_attendance_import.html',
|
||||||
|
projects=projects,
|
||||||
validation_result=validation_result)
|
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')
|
||||||
return render_template('time_attendance_import.html',
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
|
||||||
|
return render_template('time_attendance_import.html',
|
||||||
|
projects=projects,
|
||||||
import_result=import_result)
|
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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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.duplicate_records > 0)
|
if (!result.success && result.total_records === 0) {
|
||||||
summary += ' Skipped ' + result.duplicate_records + ' duplicates.';
|
// Validation failed before any records were processed (e.g. project mismatch)
|
||||||
if (result.failed_records > 0)
|
summary = '❌ Import was rejected before processing any records.';
|
||||||
summary += ' ⚠️ ' + result.failed_records + ' records failed.';
|
} else {
|
||||||
|
summary = (result.success ? '✅' : '❌') + ' Imported ' + result.imported_records + ' of ' + result.total_records + ' records.';
|
||||||
|
if (result.duplicate_records > 0)
|
||||||
|
summary += ' Skipped ' + result.duplicate_records + ' duplicates.';
|
||||||
|
if (result.failed_records > 0)
|
||||||
|
summary += ' ⚠️ ' + result.failed_records + ' records failed.';
|
||||||
|
}
|
||||||
|
|
||||||
const errors = (!result.success && result.errors && result.errors.length > 0)
|
const errors = (!result.success && result.errors && result.errors.length > 0)
|
||||||
? result.errors
|
? result.errors
|
||||||
|
|||||||
@@ -321,13 +321,16 @@ 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
|
||||||
"""
|
"""
|
||||||
@@ -355,7 +358,47 @@ class TimeAttendanceImportService:
|
|||||||
# Remove empty rows
|
# Remove empty rows
|
||||||
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,13 +488,16 @@ 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
|
||||||
"""
|
"""
|
||||||
@@ -479,6 +525,46 @@ class TimeAttendanceImportService:
|
|||||||
# Remove empty rows
|
# Remove empty rows
|
||||||
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 = []
|
||||||
@@ -663,13 +749,22 @@ 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
|
||||||
unmatched = next(
|
unmatched = next(
|
||||||
(loc for loc in sorted(file_locations) if loc not in project_locations),
|
(loc for loc in sorted(file_locations) if loc not in project_locations),
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user