From f6ad3bd930beab32ce2879677a14423f14c48916 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 20 Mar 2026 17:14:58 -0400 Subject: [PATCH] Mar 20 2026: refactor 2 --- app.py | 19 +- routes/admin.py | 10 +- routes/attendance.py | 88 +- routes/auth.py | 26 +- routes/dashboard.py | 23 +- routes/employees.py | 37 +- routes/payroll.py | 35 +- routes/projects.py | 25 +- routes/qr_codes.py | 46 +- routes/statistics.py | 17 +- routes/time_attendance.py | 82 +- routes/users.py | 98 +- templates/add_manual_attendance.html | 4 +- templates/attendance_report.html | 2168 ++++----- templates/base_authenticated.html | 38 +- templates/bulk_qr_import.html | 10 +- templates/confirm_delete_qr.html | 1218 ++--- templates/create_employee.html | 4 +- templates/create_project.html | 4 +- templates/create_qr_code.html | 2814 +++++------ templates/create_user.html | 1338 +++--- templates/dashboard.html | 4140 ++++++++--------- templates/edit_attendance.html | 4 +- templates/edit_employee.html | 4 +- templates/edit_project.html | 4 +- templates/edit_qr_code.html | 2906 ++++++------ templates/edit_user.html | 1456 +++--- templates/employee_detail.html | 2 +- templates/employees.html | 20 +- templates/export_configuration.html | 4 +- templates/payroll_dashboard.html | 14 +- templates/project_qr_codes.html | 14 +- templates/projects.html | 10 +- templates/qr_not_found.html | 158 +- templates/register.html | 362 +- templates/statistics.html | 4 +- templates/time_attendance_batch_detail.html | 4 +- templates/time_attendance_dashboard.html | 16 +- .../time_attendance_duplicate_review.html | 8 +- templates/time_attendance_import.html | 16 +- .../time_attendance_import_progress.html | 6 +- templates/time_attendance_import_result.html | 14 +- templates/time_attendance_invalid_review.html | 8 +- templates/time_attendance_record_detail.html | 10 +- templates/time_attendance_records.html | 16 +- templates/users.html | 1146 ++--- templates/verification_review.html | 6 +- templates/verification_review_detail.html | 8 +- utils/helpers.py | 41 +- 49 files changed, 9173 insertions(+), 9332 deletions(-) diff --git a/app.py b/app.py index a30ff18..5930e8d 100644 --- a/app.py +++ b/app.py @@ -70,13 +70,7 @@ def create_app() -> Flask: Employee, TimeAttendance, UserProjectPermission, UserLocationPermission) = set_db(db) - app.config['_models'] = { - 'User': User, 'QRCode': QRCode, 'QRCodeStyle': QRCodeStyle, - 'Project': Project, 'AttendanceData': AttendanceData, - 'Employee': Employee, 'TimeAttendance': TimeAttendance, - 'UserProjectPermission': UserProjectPermission, - 'UserLocationPermission': UserLocationPermission, - } + # ------------------------------------------------------------------ # Logger initialization @@ -108,9 +102,7 @@ def create_app() -> Flask: from extensions import logger_handler as _lh create_location_logging_routes(app, db, _lh) - # Patch Jinja2's url_for global so templates also use the compatibility shim - from utils.helpers import url_for as _compat_url_for - app.jinja_env.globals['url_for'] = _compat_url_for + # ------------------------------------------------------------------ # Template filters (global — must be on app, not blueprints) @@ -155,9 +147,8 @@ def create_app() -> Flask: def get_qr_code_checkin_count(qr_code_id): """Helper function to get total check-ins count for a QR code""" - from flask import current_app + from models.attendance import AttendanceData try: - AttendanceData = current_app.config['_models']['AttendanceData'] count = AttendanceData.query.filter_by(qr_code_id=qr_code_id).count() return count except Exception as e: @@ -325,7 +316,7 @@ def create_tables(): try: _db.create_all() from flask import current_app - User = current_app.config['_models']['User'] + from models.user import User admin = User.query.filter_by(username='admin').first() if not admin: default_password = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123') @@ -359,7 +350,7 @@ def update_existing_qr_codes(): from utils.helpers import generate_qr_code, get_qr_styling, generate_qr_url from flask import current_app, request try: - QRCode = current_app.config['_models']['QRCode'] + from models.qrcode import QRCode qr_codes = QRCode.query.filter_by(active_status=True).all() updated_count = 0 for qr_code in qr_codes: diff --git a/routes/admin.py b/routes/admin.py index a3b5356..5049aef 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -5,7 +5,7 @@ Admin panel and log management routes. Routes: /admin/logs, /admin/health/google-maps, /api/logs/* """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, url_for from datetime import datetime, timedelta import json, math @@ -13,14 +13,10 @@ from extensions import db, logger_handler from sqlalchemy import text from utils.geocoding import gmaps_client from logger_handler import log_user_activity, log_database_operations -from utils.helpers import url_for, admin_required, login_required +from utils.helpers import admin_required, login_required bp = Blueprint('admin', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/admin/logs', endpoint='admin_logs') @@ -34,7 +30,7 @@ def admin_logs(): except Exception as e: logger_handler.log_database_error('admin_logs_load', e) flash('Error loading log statistics.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) def check_google_maps_health(): """Check if Google Maps services are working properly""" diff --git a/routes/attendance.py b/routes/attendance.py index 074d186..c0147d3 100644 --- a/routes/attendance.py +++ b/routes/attendance.py @@ -9,14 +9,20 @@ Routes: /attendance, /attendance//edit, /attendance/add, /api/get_project_locations, /verification-review/*, /export-configuration, /generate-excel-export """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, url_for from datetime import datetime, date, timedelta, time import io, os, json, re, traceback from extensions import db, logger_handler +from models.attendance import AttendanceData +from models.employee import Employee +from models.permissions import UserLocationPermission, UserProjectPermission +from models.project import Project +from models.qrcode import QRCode +from models.user import User from sqlalchemy import text from logger_handler import log_user_activity, log_database_operations -from utils.helpers import (url_for, +from utils.helpers import ( admin_required, get_client_ip, has_admin_privileges, @@ -31,17 +37,12 @@ from openpyxl.utils import get_column_letter bp = Blueprint('attendance', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/attendance', endpoint='attendance_report') @login_required def attendance_report(): """Safe attendance report with backward compatibility for location_accuracy and fixed datetime handling""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: print("📊 Loading attendance report...") @@ -540,18 +541,17 @@ def attendance_report(): print(f"⚠️ Additional logging error: {log_error}") flash('Error loading attendance report. Please check the server logs for details.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/attendance//edit', methods=['GET', 'POST'], endpoint='edit_attendance') @login_required @log_database_operations('attendance_update') def edit_attendance(record_id): """Edit attendance record (Admin and Payroll only)""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] # Check if user has permission to edit attendance records if session.get('role') not in ['admin', 'payroll', 'accounting']: flash('Access denied. Only administrators and accounting staff can edit attendance records.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) try: attendance_record = AttendanceData.query.get_or_404(record_id) @@ -676,7 +676,7 @@ def edit_attendance(record_id): print(f"[LOG] Edit reason: {edit_note}") flash(f'Attendance record for {new_employee_id} updated successfully! Edit reason logged for audit.', 'success') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) # GET request - show edit form # Get available projects for the dropdown @@ -695,7 +695,7 @@ def edit_attendance(record_id): logger_handler.log_database_error('attendance_update', e) print(f"[LOG] Error updating attendance record {record_id}: {e}") flash('Error updating attendance record. Please try again.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) @bp.route('/attendance/add', methods=['GET'], endpoint='add_manual_attendance') @login_required @@ -705,14 +705,13 @@ def add_manual_attendance(): Display form to manually add attendance record Only accessible by admin and accounting roles """ - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: user_role = session.get('role') # Check authorization if user_role not in ['admin', 'accounting']: flash('You do not have permission to manually add attendance records.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) # Get all active projects projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() @@ -731,7 +730,7 @@ def add_manual_attendance(): except Exception as e: logger_handler.logger.error(f"Error loading manual attendance form: {e}") flash('Error loading form. Please try again.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) @bp.route('/attendance/save_manual', methods=['POST'], endpoint='save_manual_attendance') @@ -743,7 +742,6 @@ def save_manual_attendance(): Save manually created attendance record Only accessible by admin and accounting roles """ - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: user_role = session.get('role') @@ -763,19 +761,19 @@ def save_manual_attendance(): # Validate required fields if not all([employee_id, location_id, check_date, check_time]): flash('All fields are required.', 'error') - return redirect(url_for('add_manual_attendance')) + return redirect(url_for('attendance.add_manual_attendance')) # Validate employee exists employee = Employee.query.filter_by(id=int(employee_id)).first() if not employee: flash(f'Employee with ID {employee_id} not found.', 'error') - return redirect(url_for('add_manual_attendance')) + return redirect(url_for('attendance.add_manual_attendance')) # Get QR code (location) qr_code = QRCode.query.get(int(location_id)) if not qr_code: flash('Selected location not found.', 'error') - return redirect(url_for('add_manual_attendance')) + return redirect(url_for('attendance.add_manual_attendance')) # Parse date and time try: @@ -784,7 +782,7 @@ def save_manual_attendance(): except ValueError as e: flash('Invalid date or time format.', 'error') logger_handler.logger.error(f"Date/time parsing error: {e}") - return redirect(url_for('add_manual_attendance')) + return redirect(url_for('attendance.add_manual_attendance')) # Check if record already exists for this employee, location, date, and time existing_record = AttendanceData.query.filter_by( @@ -796,7 +794,7 @@ def save_manual_attendance(): if existing_record: flash('An attendance record already exists for this employee at this location, date, and time.', 'warning') - return redirect(url_for('add_manual_attendance')) + return redirect(url_for('attendance.add_manual_attendance')) # Create new attendance record # Use QR code's location address for both QR address and check-in address @@ -839,14 +837,14 @@ def save_manual_attendance(): ) flash(f'Attendance record successfully created for {employee.firstName} {employee.lastName}.', 'success') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) except Exception as e: db.session.rollback() logger_handler.logger.error(f"Error saving manual attendance record: {e}") logger_handler.logger.error(f"Traceback: {traceback.format_exc()}") flash('Error saving attendance record. Please try again.', 'error') - return redirect(url_for('add_manual_attendance')) + return redirect(url_for('attendance.add_manual_attendance')) @bp.route('/api/time-attendance/locations', endpoint='time_attendance_locations_api') @@ -854,7 +852,6 @@ def save_manual_attendance(): def time_attendance_locations_api(): """Return distinct location_name values from time_attendance, optionally filtered by project_id. Used by the time attendance records page to dynamically scope the location dropdown.""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: project_id = request.args.get('project_id', '').strip() @@ -896,7 +893,6 @@ def time_attendance_locations_api(): def attendance_locations_api(): """Return distinct location_name values from attendance_data, optionally filtered by project_id. Used by the attendance report page to dynamically scope the location dropdown when a project is selected.""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: project_id = request.args.get('project_id', '').strip() @@ -943,7 +939,6 @@ def search_employees_api(): in attendance_data that have no Employee record — so unregistered IDs that have attendance records can still be filtered on the attendance page. """ - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: search_query = request.args.get('q', '').strip() @@ -1016,7 +1011,6 @@ def get_project_locations_api(): API endpoint to get locations for a specific project Returns JSON with location list """ - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: project_id = request.args.get('project_id', '').strip() @@ -1062,7 +1056,6 @@ def get_project_locations_api(): @log_database_operations('attendance_delete') def delete_attendance(record_id): """Delete attendance record (Admin and Payroll only)""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] # Check if user has permission to delete attendance records if session.get('role') not in ['admin', 'payroll', 'accounting']: if request.headers.get('X-Requested-With') == 'XMLHttpRequest': @@ -1072,7 +1065,7 @@ def delete_attendance(record_id): }), 403 else: flash('Access denied. Only administrators and payroll staff can delete attendance records.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) try: attendance_record = AttendanceData.query.get_or_404(record_id) @@ -1110,7 +1103,7 @@ def delete_attendance(record_id): }) else: flash(f'Attendance record for {employee_id} deleted successfully!', 'success') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) except Exception as e: db.session.rollback() @@ -1124,18 +1117,17 @@ def delete_attendance(record_id): }), 500 else: flash('Error deleting attendance record. Please try again.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) @bp.route('/verification-review', endpoint='verification_review') @login_required def verification_review(): """Admin page to review pending photo verifications""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: # Only admins can access if session.get('role') not in ['admin', 'payroll', 'accounting']: flash('Unauthorized access.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) # Get filter parameters status_filter = request.args.get('status', 'pending') @@ -1253,14 +1245,13 @@ def verification_review(): except Exception as e: logger_handler.logger.error(f"Error in verification review: {e}") flash('Error loading verification review.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/verification-review//update', methods=['POST'], endpoint='update_verification_status') @login_required @log_database_operations('verification_update') def update_verification_status(record_id): """Update verification status (approve/reject)""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: # Only admins can update if session.get('role') not in ['admin', 'payroll', 'accounting']: @@ -1311,7 +1302,6 @@ def update_verification_status(record_id): @login_required def get_verification_details(record_id): """API endpoint to get verification details for a specific record""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: # Get the attendance record with verification data record = AttendanceData.query.get_or_404(record_id) @@ -1397,12 +1387,11 @@ def get_verification_details(record_id): @login_required def verification_review_detail(record_id): """Review a single verification photo on a dedicated page""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: # Check permissions if session.get('role') not in ['admin', 'payroll', 'accounting']: flash('Access denied. Only administrators, payroll, and accounting staff can review verification photos.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) # Get the attendance record record = AttendanceData.query.get_or_404(record_id) @@ -1410,7 +1399,7 @@ def verification_review_detail(record_id): # Check if this record has verification if not record.verification_required: flash('This record does not require verification.', 'warning') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) # Get the QR code information for additional context qr_code = QRCode.query.get(record.qr_code_id) if record.qr_code_id else None @@ -1459,13 +1448,12 @@ def verification_review_detail(record_id): except Exception as e: logger_handler.logger.error(f"Error loading verification review detail: {e}") flash('Error loading verification details.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) @bp.route('/api/attendance/stats', endpoint='attendance_stats_api') @admin_required def attendance_stats_api(): """API endpoint for attendance statistics""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: # Daily stats for the last 7 days daily_stats = db.session.execute(text(""" @@ -1516,13 +1504,12 @@ def attendance_stats_api(): @login_required def export_configuration(): """Display export configuration page for customizing Excel exports""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: user_role = session.get('role') if user_role not in ['admin', 'payroll', 'accounting']: logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted unauthorized access to export configuration") flash('Access denied. Only administrators and payroll staff can access export configuration.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) # Log export configuration access using your existing logger try: @@ -1546,7 +1533,6 @@ def export_configuration(): project_name = None if filters.get('project_filter'): try: - from models.project import Project project = Project.query.get(int(filters['project_filter'])) if project: project_name = project.name @@ -1611,19 +1597,18 @@ def export_configuration(): print(f"⚠️ Could not log error: {log_error}") flash('Error loading export configuration page.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) @bp.route('/generate-excel-export', methods=['POST'], endpoint='generate_excel_export') @login_required def generate_excel_export(): """Generate and download Excel file with selected columns in specified order""" - AttendanceData, QRCode, Employee, Project, User, UserProjectPermission, UserLocationPermission = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Employee"], _get_models()["Project"], _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"] try: user_role = session.get('role') if user_role not in ['admin', 'payroll', 'accounting']: logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted unauthorized Excel export") flash('Access denied. Only administrators and payroll staff can export data.', 'error') - return redirect(url_for('attendance_report')) + return redirect(url_for('attendance.attendance_report')) print("📊 Excel export generation started") @@ -1662,7 +1647,7 @@ def generate_excel_export(): if not selected_columns: flash('Please select at least one column to export.', 'error') - return redirect(url_for('export_configuration')) + return redirect(url_for('attendance.export_configuration')) column_names = {} for column in selected_columns: @@ -1695,7 +1680,6 @@ def generate_excel_export(): project_name_for_filename = '' if filters.get('project_filter'): try: - from models.project import Project project = Project.query.get(int(filters['project_filter'])) if project: # Replace spaces and special characters with underscores @@ -1750,7 +1734,7 @@ def generate_excel_export(): ) else: flash('Error generating Excel file.', 'error') - return redirect(url_for('export_configuration')) + return redirect(url_for('attendance.export_configuration')) except Exception as e: print(f"❌ Error in generate_excel_export route: {e}") @@ -1767,11 +1751,10 @@ def generate_excel_export(): print(f"⚠️ Could not log error: {log_error}") flash('Error generating Excel export.', 'error') - return redirect(url_for('export_configuration')) + return redirect(url_for('attendance.export_configuration')) def create_excel_export(selected_columns, column_names, filters): """Create Excel file with selected attendance data - Updated to include employee names""" - AttendanceData, Employee, QRCode = _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["QRCode"] try: print(f"📊 Creating Excel export with {len(selected_columns)} columns") @@ -2068,7 +2051,6 @@ def format_employee_id_for_excel(employee_id): def create_excel_export_ordered(selected_columns, column_names, filters): """Create Excel file with selected attendance data in specified column order""" - AttendanceData, Employee, QRCode = _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["QRCode"] try: print(f"📊 Creating Excel export with {len(selected_columns)} columns in order: {selected_columns}") diff --git a/routes/auth.py b/routes/auth.py index abf2ec3..e2560cd 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -5,36 +5,31 @@ Authentication and user-profile routes. Routes: /, /register, /login, /logout, /profile """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, url_for from datetime import datetime import json from extensions import db, logger_handler +from models.user import User from logger_handler import log_user_activity, log_database_operations -from utils.helpers import url_for, admin_required, login_required, staff_or_admin_required +from utils.helpers import admin_required, login_required, staff_or_admin_required from turnstile_utils import turnstile_utils bp = Blueprint('auth', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/', endpoint='index') def index(): """Home page - redirect to login if not authenticated""" - User = _get_models()["User"] if 'user_id' in session: - return redirect(url_for('dashboard')) - return redirect(url_for('login')) + return redirect(url_for('dashboard.dashboard')) + return redirect(url_for('auth.login')) @bp.route('/register', methods=['GET', 'POST'], endpoint='register') @log_user_activity('user_registration') def register(): """User registration endpoint""" - User = _get_models()["User"] if request.method == 'POST': try: full_name = request.form['full_name'] @@ -67,7 +62,7 @@ def register(): logger_handler.logger.info(f"New user registered: {username} ({email})") flash('Registration successful! Please log in.', 'success') - return redirect(url_for('login')) + return redirect(url_for('auth.login')) except Exception as e: db.session.rollback() @@ -79,7 +74,6 @@ def register(): @bp.route('/login', methods=['GET', 'POST'], endpoint='login') def login(): """Enhanced user authentication with Turnstile and comprehensive logging""" - User = _get_models()["User"] if request.method == 'POST': username = request.form.get('username', '').strip() password = request.form.get('password', '') @@ -151,7 +145,7 @@ def login(): # Redirect to intended page or dashboard next_page = request.args.get('next') - return redirect(next_page) if next_page else redirect(url_for('attendance_report')) + return redirect(next_page) if next_page else redirect(url_for('attendance.attendance_report')) else: # Invalid credentials - log failed attempt @@ -176,7 +170,6 @@ def login(): @bp.route('/logout', endpoint='logout') def logout(): """User logout endpoint with session duration logging""" - User = _get_models()["User"] user_id = session.get('user_id') username = session.get('username') login_time_str = session.get('login_time') @@ -200,14 +193,13 @@ def logout(): session.clear() flash('You have been logged out.', 'info') - return redirect(url_for('login')) + return redirect(url_for('auth.login')) @bp.route('/profile', methods=['GET', 'POST'], endpoint='profile') @login_required @log_user_activity('profile_update') def profile(): """User profile management with logging""" - User = _get_models()["User"] try: user = User.query.get(session['user_id']) @@ -269,4 +261,4 @@ def profile(): except Exception as e: logger_handler.log_database_error('profile_update', e) flash('Profile update failed. Please try again.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) diff --git a/routes/dashboard.py b/routes/dashboard.py index b25913a..65659cc 100644 --- a/routes/dashboard.py +++ b/routes/dashboard.py @@ -6,26 +6,25 @@ Dashboard and related API routes. Routes: /dashboard, /project//qr-codes, /dashboard/search, /api/dashboard/stats, /api/dashboard/realtime """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, url_for from datetime import datetime, timedelta, date, time from extensions import db, logger_handler +from models.attendance import AttendanceData +from models.project import Project +from models.qrcode import QRCode +from models.user import User from logger_handler import log_user_activity, log_database_operations -from utils.helpers import url_for, login_required +from utils.helpers import login_required bp = Blueprint('dashboard', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/dashboard', endpoint='dashboard') @login_required def dashboard(): """Enhanced project-centric dashboard with search filters""" - User, QRCode, Project, AttendanceData = _get_models()["User"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["AttendanceData"] try: user = User.query.get(session['user_id']) @@ -74,7 +73,7 @@ def dashboard(): logger_handler.log_database_error('dashboard_load', e) print(f"Error loading dashboard: {e}") flash('Error loading dashboard. Please try again.', 'error') - return redirect(url_for('login')) + return redirect(url_for('auth.login')) @bp.route('/project//qr-codes', endpoint='project_qr_codes') @login_required @@ -83,7 +82,6 @@ def project_qr_codes(project_id): View all QR codes for a specific project with search filters Allows filtering by name and status within the project """ - User, QRCode, Project, AttendanceData = _get_models()["User"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["AttendanceData"] try: # Get the project project = Project.query.get_or_404(project_id) @@ -131,13 +129,12 @@ def project_qr_codes(project_id): logger_handler.log_database_error('project_qr_codes_view', e) print(f"Error loading project QR codes: {e}") flash('Error loading project QR codes. Please try again.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/dashboard/search', methods=['GET'], endpoint='search_qr_codes') @login_required def search_qr_codes(): """Search QR codes - redirect to dashboard with filters""" - User, QRCode, Project, AttendanceData = _get_models()["User"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["AttendanceData"] search_name = request.args.get('search_name', '').strip() search_status = request.args.get('search_status', '').strip() @@ -148,13 +145,12 @@ def search_qr_codes(): ) # Redirect to dashboard with search parameters - return redirect(url_for('dashboard', search_name=search_name, search_status=search_status)) + return redirect(url_for('dashboard.dashboard', search_name=search_name, search_status=search_status)) @bp.route('/api/dashboard/stats', endpoint='dashboard_stats_api') @login_required def dashboard_stats_api(): """API endpoint for dashboard statistics""" - User, QRCode, Project, AttendanceData = _get_models()["User"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["AttendanceData"] try: # Get current stats total_qr_codes = QRCode.query.filter_by(active_status=True).count() @@ -213,7 +209,6 @@ def dashboard_stats_api(): @login_required def dashboard_realtime_api(): """API endpoint for real-time dashboard data""" - User, QRCode, Project, AttendanceData = _get_models()["User"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["AttendanceData"] try: # Get recent activity (last 10 check-ins) recent_activity = db.session.query( diff --git a/routes/employees.py b/routes/employees.py index 4c5c254..52dc667 100644 --- a/routes/employees.py +++ b/routes/employees.py @@ -6,12 +6,17 @@ Employee CRUD and search routes. Routes: /employees, /employees/create, /employees//edit, /employees//delete, /api/employees/search, /employees/ """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, url_for from datetime import datetime, date from extensions import db, logger_handler +from models.attendance import AttendanceData +from models.employee import Employee +from models.project import Project +from models.qrcode import QRCode +from models.user import User from logger_handler import log_user_activity, log_database_operations -from utils.helpers import (url_for, +from utils.helpers import ( admin_required, has_admin_privileges, has_staff_level_access, @@ -20,17 +25,12 @@ from utils.helpers import (url_for, bp = Blueprint('employees', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/employees', endpoint='employees') @login_required def employees(): """Display employee management page with search and pagination""" - Employee, AttendanceData, Project, QRCode, User = _get_models()["Employee"], _get_models()["AttendanceData"], _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] try: # Log user accessing employee management try: @@ -87,14 +87,13 @@ def employees(): except Exception as e: logger_handler.log_database_error('employee_list', e) flash('Error loading employee list. Please try again.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/employees/create', methods=['GET', 'POST'], endpoint='create_employee') @login_required @log_database_operations('employee_creation') def create_employee(): """Create new employee (Admin only)""" - Employee, AttendanceData, Project, QRCode, User = _get_models()["Employee"], _get_models()["AttendanceData"], _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] if request.method == 'POST': try: # Get form data @@ -147,7 +146,7 @@ def create_employee(): print(f"⚠️ Logging error (non-critical): {log_error}") flash(f'Employee "{first_name} {last_name}" (ID: {employee_id}) created successfully.', 'success') - return redirect(url_for('employees')) + return redirect(url_for('employees.employees')) except Exception as e: db.session.rollback() @@ -165,7 +164,6 @@ def create_employee(): @log_database_operations('employee_update') def edit_employee(employee_index): """Edit existing employee (Admin only)""" - Employee, AttendanceData, Project, QRCode, User = _get_models()["Employee"], _get_models()["AttendanceData"], _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] try: # Get employee by index (primary key) employee = Employee.query.get_or_404(employee_index) @@ -227,7 +225,7 @@ def edit_employee(employee_index): print(f"⚠️ Logging error (non-critical): {log_error}") flash(f'Employee "{first_name} {last_name}" updated successfully.', 'success') - return redirect(url_for('employees')) + return redirect(url_for('employees.employees')) # GET request - load the form with projects projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() @@ -237,14 +235,13 @@ def edit_employee(employee_index): db.session.rollback() logger_handler.log_database_error('employee_update', e) flash('Error updating employee. Please try again.', 'error') - return redirect(url_for('employees')) + return redirect(url_for('employees.employees')) @bp.route('/employees//delete', methods=['POST'], endpoint='delete_employee') @login_required @log_database_operations('employee_deletion') def delete_employee(employee_index): """Delete employee (Admin only) - Enhanced with better logging""" - Employee, AttendanceData, Project, QRCode, User = _get_models()["Employee"], _get_models()["AttendanceData"], _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] try: print(f"🗑️ DELETE REQUEST: Employee index {employee_index}") print(f"📋 Request method: {request.method}") @@ -265,7 +262,6 @@ def delete_employee(employee_index): } # Check if employee has attendance records - from models.attendance import AttendanceData attendance_count = AttendanceData.query.filter_by(employee_id=str(employee.id)).count() print(f"📊 Attendance records found: {attendance_count}") @@ -273,7 +269,7 @@ def delete_employee(employee_index): error_msg = f'Cannot delete employee "{employee.full_name}". Employee has {attendance_count} attendance records. Please contact system administrator.' print(f"❌ DELETION BLOCKED: {error_msg}") flash(error_msg, 'error') - return redirect(url_for('employees')) + return redirect(url_for('employees.employees')) # Proceed with deletion print(f"🗑️ Proceeding with deletion of employee: {employee_data['firstName']} {employee_data['lastName']}") @@ -293,7 +289,7 @@ def delete_employee(employee_index): flash(success_msg, 'success') print(f"✅ SUCCESS: {success_msg}") - return redirect(url_for('employees')) + return redirect(url_for('employees.employees')) except Exception as e: db.session.rollback() @@ -302,13 +298,12 @@ def delete_employee(employee_index): print(f"❌ ERROR in delete_employee: {e}") print(f"❌ Exception type: {type(e)}") flash(error_msg, 'error') - return redirect(url_for('employees')) + return redirect(url_for('employees.employees')) @bp.route('/api/employees/search', endpoint='api_employees_search') @login_required def api_employees_search(): """API endpoint for employee search (for AJAX)""" - Employee, AttendanceData, Project, QRCode, User = _get_models()["Employee"], _get_models()["AttendanceData"], _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] try: search = request.args.get('q', '').strip() limit = request.args.get('limit', 10, type=int) @@ -332,13 +327,11 @@ def api_employees_search(): @login_required def employee_detail(employee_index): """View employee details with attendance summary""" - Employee, AttendanceData, Project, QRCode, User = _get_models()["Employee"], _get_models()["AttendanceData"], _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] try: # Get employee by index (primary key) employee = Employee.query.outerjoin(Project, Employee.contractId == Project.id).filter(Employee.index == employee_index).first_or_404() # Get attendance statistics for this employee - from models.attendance import AttendanceData # Total attendance records total_attendance = AttendanceData.query.filter_by(employee_id=str(employee.id)).count() @@ -387,4 +380,4 @@ def employee_detail(employee_index): except Exception as e: logger_handler.log_database_error('employee_detail', e) flash('Error loading employee details. Please try again.', 'error') - return redirect(url_for('employees')) + return redirect(url_for('employees.employees')) diff --git a/routes/payroll.py b/routes/payroll.py index 95c6a7a..ca655f9 100644 --- a/routes/payroll.py +++ b/routes/payroll.py @@ -6,14 +6,19 @@ Payroll dashboard and Excel export routes. Routes: /payroll, /payroll/export-excel, /api/working-hours/calculate, /api/employee//miss-punch-details """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, url_for from datetime import datetime, date, timedelta, time import io, json, traceback, os from extensions import db, logger_handler +from models.attendance import AttendanceData +from models.employee import Employee +from models.project import Project +from models.qrcode import QRCode +from models.user import User from sqlalchemy import text from logger_handler import log_user_activity, log_database_operations -from utils.helpers import (url_for, +from utils.helpers import ( admin_required, has_admin_privileges, has_staff_level_access, @@ -25,24 +30,19 @@ from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter bp = Blueprint('payroll', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/payroll', endpoint='payroll_dashboard') @login_required def payroll_dashboard(): """Payroll dashboard for calculating and exporting working hours""" - AttendanceData, Employee, Project, TimeAttendance, QRCode, User = _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["Project"], _get_models()["TimeAttendance"], _get_models()["QRCode"], _get_models()["User"] try: # Check if user has payroll access user_role = session.get('role') if user_role not in ['admin', 'payroll', 'accounting']: logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted to access payroll dashboard without permissions") flash('Access denied. Only administrators and payroll staff can access payroll features.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) print("📊 Loading payroll dashboard") @@ -176,21 +176,20 @@ def payroll_dashboard(): ) flash('Error loading payroll dashboard. Please check the server logs.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/payroll/export-excel', methods=['POST'], endpoint='export_payroll_excel') @login_required @log_database_operations('payroll_excel_export') def export_payroll_excel(): """Export payroll report to Excel with working hours calculations including SP/PW support""" - AttendanceData, Employee, Project, TimeAttendance, QRCode, User = _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["Project"], _get_models()["TimeAttendance"], _get_models()["QRCode"], _get_models()["User"] try: # Check permissions user_role = session.get('role') if user_role not in ['admin', 'payroll', 'accounting']: logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted unauthorized payroll Excel export") flash('Access denied. Only administrators and payroll staff can export payroll data.', 'error') - return redirect(url_for('payroll_dashboard')) + return redirect(url_for('payroll.payroll_dashboard')) print("📊 Payroll Excel export started") @@ -202,14 +201,14 @@ def export_payroll_excel(): if not date_from or not date_to: flash('Please provide both start and end dates for the export.', 'error') - return redirect(url_for('payroll_dashboard')) + return redirect(url_for('payroll.payroll_dashboard')) try: start_date = datetime.strptime(date_from, '%Y-%m-%d') end_date = datetime.strptime(date_to, '%Y-%m-%d') except ValueError: flash('Invalid date format. Please use YYYY-MM-DD format.', 'error') - return redirect(url_for('payroll_dashboard')) + return redirect(url_for('payroll.payroll_dashboard')) # Get attendance records with project filter and QR code data query = db.session.query(AttendanceData, QRCode).join(QRCode, AttendanceData.qr_code_id == QRCode.id) @@ -240,7 +239,7 @@ def export_payroll_excel(): if not attendance_records: flash('No attendance records found for the selected date range and project.', 'warning') - return redirect(url_for('payroll_dashboard')) + return redirect(url_for('payroll.payroll_dashboard')) print(f"📊 Exporting {len(attendance_records)} attendance records to Excel") @@ -404,7 +403,7 @@ def export_payroll_excel(): ) else: flash('Error generating payroll Excel file.', 'error') - return redirect(url_for('payroll_dashboard')) + return redirect(url_for('payroll.payroll_dashboard')) except Exception as e: print(f"❌ Error in export_payroll_excel route: {e}") @@ -418,14 +417,13 @@ def export_payroll_excel(): ) flash('Error generating payroll Excel export. Please check the server logs.', 'error') - return redirect(url_for('payroll_dashboard')) + return redirect(url_for('payroll.payroll_dashboard')) @bp.route('/api/working-hours/calculate', methods=['POST'], endpoint='calculate_working_hours_api') @login_required @log_database_operations('working_hours_api_calculation') def calculate_working_hours_api(): """API endpoint for calculating working hours""" - AttendanceData, Employee, Project, TimeAttendance, QRCode, User = _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["Project"], _get_models()["TimeAttendance"], _get_models()["QRCode"], _get_models()["User"] try: # Check permissions user_role = session.get('role') @@ -503,7 +501,6 @@ def calculate_working_hours_api(): @log_database_operations('miss_punch_details_api') def get_miss_punch_details(employee_id): """API endpoint to get detailed miss punch information for an employee""" - AttendanceData, Employee, Project, TimeAttendance, QRCode, User = _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["Project"], _get_models()["TimeAttendance"], _get_models()["QRCode"], _get_models()["User"] try: # Check permissions user_role = session.get('role') @@ -667,7 +664,6 @@ def get_miss_punch_details(employee_id): def get_employee_name(employee_id): """Helper function to get employee full name by ID""" - Employee = _get_models()["Employee"] try: result = db.session.execute(text(""" SELECT CONCAT(firstName, ' ', lastName) as full_name @@ -684,7 +680,6 @@ def get_employee_name(employee_id): def get_qr_code_checkin_count(qr_code_id): """Helper function to get total check-ins count for a QR code""" - AttendanceData = _get_models()["AttendanceData"] try: count = AttendanceData.query.filter_by(qr_code_id=qr_code_id).count() logger_handler.logger.info(f"QR Code {qr_code_id} total check-ins: {count}") diff --git a/routes/projects.py b/routes/projects.py index f76b641..921ccbb 100644 --- a/routes/projects.py +++ b/routes/projects.py @@ -6,41 +6,37 @@ Project CRUD and related API routes. Routes: /projects, /projects/create, /projects//edit, /projects//toggle, /api/projects/active """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, url_for import json from datetime import datetime from extensions import db, logger_handler +from models.project import Project +from models.user import User from logger_handler import log_user_activity, log_database_operations -from utils.helpers import url_for, admin_required, login_required, staff_or_admin_required +from utils.helpers import admin_required, login_required, staff_or_admin_required bp = Blueprint('projects', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/projects', endpoint='projects') @admin_required def projects(): """Display all projects""" - Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] try: projects = Project.query.order_by(Project.created_date.desc()).all() return render_template('projects.html', projects=projects) except Exception as e: logger_handler.log_database_error('projects_list', e) flash('Error loading projects list.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/projects/create', methods=['GET', 'POST'], endpoint='create_project') @admin_required @log_database_operations('project_creation') def create_project(): """Create new project""" - Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] if request.method == 'POST': try: name = request.form['name'] @@ -65,7 +61,7 @@ def create_project(): logger_handler.logger.info(f"User {session['username']} created new project: {name}") flash(f'Project "{name}" created successfully.', 'success') - return redirect(url_for('projects')) + return redirect(url_for('projects.projects')) except Exception as e: db.session.rollback() @@ -79,7 +75,6 @@ def create_project(): @log_database_operations('project_edit') def edit_project(project_id): """Edit existing project""" - Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] try: project = Project.query.get_or_404(project_id) @@ -103,7 +98,7 @@ def edit_project(project_id): logger_handler.logger.info(f"User {session['username']} updated project {project_id}: {json.dumps(changes)}") flash(f'Project "{project.name}" updated successfully.', 'success') - return redirect(url_for('projects')) + return redirect(url_for('projects.projects')) return render_template('edit_project.html', project=project) @@ -111,14 +106,13 @@ def edit_project(project_id): db.session.rollback() logger_handler.log_database_error('project_edit', e) flash('Project update failed. Please try again.', 'error') - return redirect(url_for('projects')) + return redirect(url_for('projects.projects')) @bp.route('/projects//toggle', methods=['POST'], endpoint='toggle_project') @admin_required @log_database_operations('project_toggle') def toggle_project(project_id): """Toggle project active status""" - Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] try: project = Project.query.get_or_404(project_id) old_status = project.active_status @@ -137,14 +131,13 @@ def toggle_project(project_id): logger_handler.log_database_error('project_toggle', e) flash('Failed to update project status.', 'error') - return redirect(url_for('projects')) + return redirect(url_for('projects.projects')) # API ENDPOINTS FOR DROPDOWN FUNCTIONALITY @bp.route('/api/projects/active', endpoint='api_active_projects') @login_required def api_active_projects(): """Get active projects for dropdown""" - Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"] try: projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() diff --git a/routes/qr_codes.py b/routes/qr_codes.py index dd3871c..f074e29 100644 --- a/routes/qr_codes.py +++ b/routes/qr_codes.py @@ -6,15 +6,19 @@ QR code management and destination handler routes. Routes: /qr-codes/create, /qr-codes/bulk-import, /qr-codes//*, /qr/ """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, current_app +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, current_app, url_for from datetime import datetime, date, timedelta, time import io, os, base64, re, uuid, json, traceback from extensions import db, logger_handler +from models.attendance import AttendanceData +from models.employee import Employee +from models.project import Project +from models.qrcode import QRCode, QRCodeStyle +from models.user import User from werkzeug.utils import secure_filename from logger_handler import log_user_activity, log_database_operations from utils.helpers import ( - url_for, admin_required, detect_device_info, generate_default_qr_code, @@ -37,10 +41,6 @@ import openpyxl bp = Blueprint('qr_codes', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/qr-codes/create', methods=['GET', 'POST'], endpoint='create_qr_code') @@ -48,7 +48,6 @@ def _get_models(): @log_database_operations('qr_code_creation') def create_qr_code(): """Enhanced create QR code with customization options""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] if request.method == 'POST': try: # Existing form data @@ -187,7 +186,7 @@ def create_qr_code(): style_info = f" with custom styling (Fill: {fill_color}, Background: {back_color})" flash(f'QR Code "{name}" created successfully{project_info}{coord_info}{style_info}! URL: {qr_url}', 'success') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) except Exception as e: db.session.rollback() @@ -206,7 +205,6 @@ def create_qr_code(): @log_database_operations('qr_code_bulk_import') def import_bulk_qr_codes(): """Bulk import QR codes from Excel file""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] if request.method == 'GET': return render_template('bulk_qr_import.html') @@ -217,7 +215,7 @@ def import_bulk_qr_codes(): if proceed_import: if 'pending_qr_import_file' not in session or 'pending_qr_import_filename' not in session: flash('Import session expired. Please upload the file again.', 'error') - return redirect(url_for('import_bulk_qr_codes')) + return redirect(url_for('qr_codes.import_bulk_qr_codes')) temp_path = session['pending_qr_import_file'] filename = session['pending_qr_import_filename'] @@ -226,7 +224,7 @@ def import_bulk_qr_codes(): flash('Temporary file not found. Please upload the file again.', 'error') session.pop('pending_qr_import_file', None) session.pop('pending_qr_import_filename', None) - return redirect(url_for('import_bulk_qr_codes')) + return redirect(url_for('qr_codes.import_bulk_qr_codes')) else: if 'file' not in request.files: flash('No file uploaded.', 'error') @@ -313,14 +311,13 @@ def import_bulk_qr_codes(): except Exception as e: logger_handler.log_database_error('qr_code_bulk_import', e) flash(f'Import failed: {str(e)}', 'error') - return redirect(url_for('import_bulk_qr_codes')) + return redirect(url_for('qr_codes.import_bulk_qr_codes')) @bp.route('/qr-codes/bulk-import/template', endpoint='download_qr_import_template') @login_required def download_qr_import_template(): """Download Excel template for bulk QR code import""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: from openpyxl import Workbook from openpyxl.styles import Font, Alignment, PatternFill @@ -380,14 +377,13 @@ def download_qr_import_template(): except Exception as e: logger_handler.log_flask_error('qr_import_template_download', str(e)) flash('Error generating template. Please try again.', 'error') - return redirect(url_for('import_bulk_qr_codes')) + return redirect(url_for('qr_codes.import_bulk_qr_codes')) @bp.route('/qr-codes//edit', methods=['GET', 'POST'], endpoint='edit_qr_code') @login_required @log_database_operations('qr_code_edit') def edit_qr_code(qr_id): """Enhanced edit QR code with customization support""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: qr_code = QRCode.query.get_or_404(qr_id) @@ -501,7 +497,7 @@ def edit_qr_code(qr_id): # Success message flash(f'QR Code "{qr_code.name}" updated successfully!', 'success') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) # GET request - render edit form projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() @@ -513,14 +509,13 @@ def edit_qr_code(qr_id): db.session.rollback() logger_handler.log_database_error('qr_code_edit', e) flash('QR Code update failed. Please try again.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/qr-codes//delete', methods=['GET', 'POST'], endpoint='delete_qr_code') @admin_required @log_database_operations('qr_code_deletion') def delete_qr_code(qr_id): """Permanently delete QR code (Admin only) - Hard delete - PRESERVING EXACT ROUTE""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: qr_code = QRCode.query.get_or_404(qr_id) print(f"✅ Found QR Code: {qr_code.name}") @@ -554,7 +549,7 @@ def delete_qr_code(qr_id): print(f"✅ DELETE SUCCESS! Removed {before_count - after_count} records") flash(f'QR code "{qr_name}" has been permanently deleted!', 'success') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) # GET request - show confirmation page print("📄 Showing confirmation page") @@ -567,12 +562,11 @@ def delete_qr_code(qr_id): print(f"❌ Exception type: {type(e)}") print(f"❌ Traceback: {traceback.format_exc()}") flash('Error deleting QR code. Please try again.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/qr/', endpoint='qr_destination') def qr_destination(qr_url): """QR code destination page where staff check in - PRESERVING EXACT ROUTE""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: # Find QR code by URL qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first() @@ -585,7 +579,7 @@ def qr_destination(qr_url): severity="MEDIUM" ) flash('QR code not found or inactive.', 'error') - return redirect(url_for('index')) + return redirect(url_for('auth.index')) # Log QR code access logger_handler.log_qr_code_accessed( @@ -599,7 +593,7 @@ def qr_destination(qr_url): except Exception as e: logger_handler.log_database_error('qr_code_scan', e) flash('Error processing QR code scan.', 'error') - return redirect(url_for('index')) + return redirect(url_for('auth.index')) @bp.route('/qr//checkin', methods=['POST'], endpoint='qr_checkin') def qr_checkin(qr_url): @@ -608,7 +602,6 @@ def qr_checkin(qr_url): Allows multiple check-ins with minimum interval between them PRESERVES coordinate-to-address conversion functionality """ - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: print(f"\n🚀 STARTING ENHANCED CHECK-IN PROCESS") print(f" QR URL: {qr_url}") @@ -913,7 +906,6 @@ def qr_checkin(qr_url): @login_required def toggle_qr_status(qr_id): """Toggle QR code active/inactive status""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: qr_code = QRCode.query.get_or_404(qr_id) @@ -943,7 +935,6 @@ def toggle_qr_status(qr_id): @login_required def copy_qr_url(qr_id): """Log QR code URL copy action""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: qr_code = QRCode.query.get_or_404(qr_id) @@ -967,7 +958,6 @@ def copy_qr_url(qr_id): @login_required def open_qr_link(qr_id): """Log QR code link open action""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: qr_code = QRCode.query.get_or_404(qr_id) @@ -991,7 +981,6 @@ def open_qr_link(qr_id): @login_required def activate_qr_code(qr_id): """Activate a QR code""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: qr_code = QRCode.query.get_or_404(qr_id) qr_code.active_status = True @@ -1017,7 +1006,6 @@ def activate_qr_code(qr_id): @login_required def deactivate_qr_code(qr_id): """Deactivate a QR code""" - QRCode, QRCodeStyle, Project, AttendanceData, Employee, User = _get_models()["QRCode"], _get_models()["QRCodeStyle"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["User"] try: qr_code = QRCode.query.get_or_404(qr_id) qr_code.active_status = False diff --git a/routes/statistics.py b/routes/statistics.py index a0a93ab..3c30db2 100644 --- a/routes/statistics.py +++ b/routes/statistics.py @@ -5,28 +5,26 @@ Statistics dashboard and export routes. Routes: /statistics, /api/statistics/export """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, make_response, current_app +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, make_response, current_app, url_for from datetime import datetime, date, timedelta import io, json, traceback from extensions import db, logger_handler +from models.employee import Employee +from models.project import Project +from models.user import User from sqlalchemy import text from logger_handler import log_user_activity, log_database_operations -from utils.helpers import url_for, login_required, staff_or_admin_required +from utils.helpers import login_required, staff_or_admin_required bp = Blueprint('statistics', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/statistics', endpoint='qr_statistics') @login_required def qr_statistics(): """QR Code Statistics Dashboard with comprehensive analytics""" - AttendanceData, QRCode, Project, Employee, User = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["Employee"], _get_models()["User"] try: # Log statistics page access logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed QR code statistics dashboard") @@ -208,14 +206,13 @@ def qr_statistics(): print(f"❌ Traceback: {traceback.format_exc()}") flash('Error loading statistics. Please try again.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/api/statistics/export', endpoint='export_statistics') @login_required def export_statistics(): """Export statistics data to CSV/Excel""" - AttendanceData, QRCode, Project, Employee, User = _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["Employee"], _get_models()["User"] try: # Check permissions if session.get('role') not in ['admin', 'payroll', 'accounting']: @@ -310,6 +307,6 @@ def export_statistics(): print(f"❌ Traceback: {traceback.format_exc()}") flash('Error loading statistics. Please try again.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) # EMPLOYEE MANAGEMENT ROUTES \ No newline at end of file diff --git a/routes/time_attendance.py b/routes/time_attendance.py index 1146fe0..f218126 100644 --- a/routes/time_attendance.py +++ b/routes/time_attendance.py @@ -9,17 +9,21 @@ Routes: /time-attendance, /time-attendance/import/*, /time-attendance/record/, /time-attendance/delete/, /api/time-attendance/* """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, Response, g, current_app +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, send_file, Response, g, current_app, url_for from datetime import datetime, date, timedelta, time import io, os, json, re, uuid, traceback import time as _time from extensions import db, logger_handler +from models.employee import Employee +from models.project import Project +from models.qrcode import QRCode +from models.time_attendance import TimeAttendance +from models.user import User from sqlalchemy import text from werkzeug.utils import secure_filename from logger_handler import log_user_activity, log_database_operations from utils.helpers import ( - url_for, admin_required, has_admin_privileges, has_staff_level_access, @@ -35,10 +39,6 @@ import openpyxl.cell.cell bp = Blueprint('time_attendance', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/time-attendance', endpoint='time_attendance_dashboard') @@ -46,7 +46,6 @@ def _get_models(): @log_user_activity('time_attendance_view') def time_attendance_dashboard(): """Display time attendance dashboard with table layout""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: # Initialize default values total_records = 0 @@ -59,7 +58,6 @@ def time_attendance_dashboard(): # Try to get data from TimeAttendance model if it exists try: - from models.time_attendance import TimeAttendance # Get summary statistics total_records = TimeAttendance.query.count() @@ -114,14 +112,13 @@ def time_attendance_dashboard(): except Exception as e: logger_handler.logger.error(f"Error in time attendance dashboard: {e}") flash('Error loading time attendance dashboard.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/time-attendance/import', methods=['GET', 'POST'], endpoint='import_time_attendance') @login_required @log_database_operations('time_attendance_import') def import_time_attendance(): """Enhanced import with duplicate review""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] if request.method == 'GET': # Load active projects for dropdown projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() @@ -141,7 +138,7 @@ def import_time_attendance(): # Retrieve file from session if 'pending_import_file' not in session or 'pending_import_filename' not in session: flash('Session expired. Please upload the file again.', 'error') - return redirect(url_for('import_time_attendance')) + return redirect(url_for('time_attendance.import_time_attendance')) temp_path = session['pending_import_file'] filename = session['pending_import_filename'] @@ -151,7 +148,7 @@ def import_time_attendance(): flash('Temporary file not found. Please upload the file again.', 'error') session.pop('pending_import_file', None) session.pop('pending_import_filename', None) - return redirect(url_for('import_time_attendance')) + return redirect(url_for('time_attendance.import_time_attendance')) print(f"✅ Retrieved file from session: {filename}") print(f"✅ Temp path exists: {os.path.exists(temp_path)}") @@ -377,7 +374,7 @@ def import_time_attendance(): print(f" Total imported: {all_results['total_imported']}") print(f" Total duplicates: {all_results['total_duplicates']}") - return redirect(url_for('time_attendance_dashboard')) + return redirect(url_for('time_attendance.time_attendance_dashboard')) # Check if this is coming from duplicate review force_import_hashes = request.form.getlist('force_import_hashes[]') @@ -528,7 +525,6 @@ def import_time_attendance(): @login_required def analyze_import_duplicates(): """AJAX endpoint to analyze file for duplicates""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: if 'file' not in request.files: return jsonify({'success': False, 'message': 'No file provided'}), 400 @@ -594,7 +590,6 @@ def analyze_import_duplicates(): @login_required def analyze_import_invalid(): """AJAX endpoint to analyze file for invalid rows""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: if 'file' not in request.files: return jsonify({'success': False, 'message': 'No file provided'}), 400 @@ -686,7 +681,6 @@ def start_import_job(): progress file, then returns a job_id. The actual import runs inside the SSE stream endpoint so no background thread or shared memory is needed. """ - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: if 'files' not in request.files: return jsonify({'success': False, 'error': 'No file uploaded.'}), 400 @@ -739,7 +733,6 @@ def stream_import_progress(job_id): the browser. Works across multiple gunicorn workers because all state is stored on disk (no in-memory job store). """ - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] # Capture upload_dir HERE in the request context — current_app is NOT # available inside the background thread (_run) or after context teardown. upload_dir = current_app.config.get('UPLOAD_FOLDER', '/tmp') @@ -896,7 +889,6 @@ def stream_import_progress(job_id): @login_required def cancel_pending_import(): """Cancel pending import and cleanup temp file""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: if 'pending_import_file' in session: temp_path = session['pending_import_file'] @@ -911,7 +903,7 @@ def cancel_pending_import(): except Exception as e: logger_handler.logger.error(f"Error cancelling import: {e}") - return redirect(url_for('import_time_attendance')) + return redirect(url_for('time_attendance.import_time_attendance')) @@ -919,7 +911,6 @@ def cancel_pending_import(): @login_required def validate_import_file(): """AJAX endpoint to validate Excel file before import""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: if 'file' not in request.files: return jsonify({'success': False, 'message': 'No file provided'}), 400 @@ -967,14 +958,13 @@ def validate_import_file(): @log_user_activity('view_import_batch') def view_import_batch(batch_id): """View details of a specific import batch""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: import_service = TimeAttendanceImportService(db, logger_handler) batch_summary = import_service.get_import_summary(batch_id) if not batch_summary: flash('Import batch not found.', 'error') - return redirect(url_for('time_attendance_dashboard')) + return redirect(url_for('time_attendance.time_attendance_dashboard')) return render_template('time_attendance_batch_detail.html', batch_summary=batch_summary) @@ -982,7 +972,7 @@ def view_import_batch(batch_id): except Exception as e: logger_handler.logger.error(f"Error viewing batch {batch_id}: {e}") flash('Error loading batch details.', 'error') - return redirect(url_for('time_attendance_dashboard')) + return redirect(url_for('time_attendance.time_attendance_dashboard')) @bp.route('/time-attendance/import/batch//delete', methods=['POST'], endpoint='delete_import_batch') @@ -990,7 +980,6 @@ def view_import_batch(batch_id): @log_database_operations('delete_import_batch') def delete_import_batch(batch_id): """Delete an entire import batch""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: import_service = TimeAttendanceImportService(db, logger_handler) result = import_service.delete_import_batch(batch_id, deleted_by=session['user_id']) @@ -1004,19 +993,18 @@ def delete_import_batch(batch_id): else: flash(result['message'], 'error') - return redirect(url_for('time_attendance_dashboard')) + return redirect(url_for('time_attendance.time_attendance_dashboard')) except Exception as e: logger_handler.logger.error(f"Error deleting batch {batch_id}: {e}") flash('Error deleting import batch.', 'error') - return redirect(url_for('time_attendance_dashboard')) + return redirect(url_for('time_attendance.time_attendance_dashboard')) @bp.route('/time-attendance/import/download-template', endpoint='download_import_template') @login_required def download_import_template(): """Download Excel template for time attendance import""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: import io from openpyxl import Workbook @@ -1119,14 +1107,13 @@ def download_import_template(): except Exception as e: logger_handler.logger.error(f"Error generating template: {e}") flash('Error generating template file.', 'error') - return redirect(url_for('import_time_attendance')) + return redirect(url_for('time_attendance.import_time_attendance')) @bp.route('/time-attendance/export', endpoint='export_time_attendance') @login_required @log_user_activity('time_attendance_export') def export_time_attendance(): """Export time attendance records to CSV or Excel""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: export_format = request.args.get('format', 'excel').lower() @@ -1139,7 +1126,6 @@ def export_time_attendance(): project_filter = request.args.get('project_id') # Build query with same filters as the view - from models.time_attendance import TimeAttendance query = TimeAttendance.query # Apply filters — employee_id supports comma-separated multi-employee values @@ -1169,7 +1155,7 @@ def export_time_attendance(): query = query.filter(TimeAttendance.attendance_date >= start_date_obj) except ValueError: flash('Invalid start date format.', 'error') - return redirect(url_for('time_attendance_records')) + return redirect(url_for('time_attendance.time_attendance_records')) if end_date: try: @@ -1183,7 +1169,7 @@ def export_time_attendance(): query = query.filter(TimeAttendance.attendance_date <= end_date_obj + timedelta(days=1)) except ValueError: flash('Invalid end date format.', 'error') - return redirect(url_for('time_attendance_records')) + return redirect(url_for('time_attendance.time_attendance_records')) if import_batch: query = query.filter(TimeAttendance.import_batch_id == import_batch) @@ -1199,13 +1185,12 @@ def export_time_attendance(): if not records: flash('No records found to export.', 'warning') - return redirect(url_for('time_attendance_records')) + return redirect(url_for('time_attendance.time_attendance_records')) # Get project name if project filter exists project_name_for_filename = '' if project_filter: try: - from models.project import Project project = Project.query.get(int(project_filter)) if project: # Replace spaces and special characters with underscores @@ -1261,7 +1246,7 @@ def export_time_attendance(): except Exception as e: logger_handler.logger.error(f"Error exporting time attendance records: {e}") flash('Error generating export file. Please try again.', 'error') - return redirect(url_for('time_attendance_records')) + return redirect(url_for('time_attendance.time_attendance_records')) def calculate_possible_violation(distance_value): @@ -1333,7 +1318,6 @@ def _qtr(decimal_hours: float) -> float: 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""" - Employee, Project, QRCode, TimeAttendance = _get_models()["Employee"], _get_models()["Project"], _get_models()["QRCode"], _get_models()["TimeAttendance"] from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Border, Side, Alignment from openpyxl.utils import get_column_letter @@ -2504,16 +2488,14 @@ def export_time_attendance_excel(records, project_name_for_filename, date_range_ @log_user_activity('time_attendance_excel_export') def excel_export_time_attendance(): """Excel export with current page filters""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] # Redirect to main export with Excel format - return redirect(url_for('export_time_attendance', format='excel', **request.args)) + return redirect(url_for('time_attendance.export_time_attendance', format='excel', **request.args)) @bp.route('/time-attendance/export-by-building', endpoint='export_time_attendance_by_building') @login_required @log_user_activity('time_attendance_export_by_building') def export_time_attendance_by_building(): """Export time attendance records grouped by building/location to Excel""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: # Get filter parameters (same as records page) employee_filter = request.args.get('employee_id') @@ -2524,7 +2506,6 @@ def export_time_attendance_by_building(): project_filter = request.args.get('project_id') # Build query with same filters as the view - from models.time_attendance import TimeAttendance query = TimeAttendance.query # Apply filters — employee_id supports comma-separated multi-employee values @@ -2554,7 +2535,7 @@ def export_time_attendance_by_building(): query = query.filter(TimeAttendance.attendance_date >= start_date_obj) except ValueError: flash('Invalid start date format.', 'error') - return redirect(url_for('time_attendance_records')) + return redirect(url_for('time_attendance.time_attendance_records')) if end_date: try: @@ -2567,7 +2548,7 @@ def export_time_attendance_by_building(): query = query.filter(TimeAttendance.attendance_date <= end_date_obj + timedelta(days=1)) except ValueError: flash('Invalid end date format.', 'error') - return redirect(url_for('time_attendance_records')) + return redirect(url_for('time_attendance.time_attendance_records')) if import_batch: query = query.filter(TimeAttendance.import_batch_id == import_batch) @@ -2584,13 +2565,12 @@ def export_time_attendance_by_building(): if not records: flash('No records found to export.', 'warning') - return redirect(url_for('time_attendance_records')) + return redirect(url_for('time_attendance.time_attendance_records')) # Get project name if project filter exists project_name_for_filename = '' if project_filter: try: - from models.project import Project project = Project.query.get(int(project_filter)) if project: # Replace spaces and special characters with underscores @@ -2636,11 +2616,10 @@ def export_time_attendance_by_building(): except Exception as e: logger_handler.logger.error(f"Error exporting time attendance records by building: {e}") flash('Error generating export file. Please try again.', 'error') - return redirect(url_for('time_attendance_records')) + return redirect(url_for('time_attendance.time_attendance_records')) def export_time_attendance_by_building_excel(records, project_name_for_filename, date_range_str, start_date_filter=None, end_date_filter=None): """Generate Excel export grouped by building/location with template format""" - Employee, Project, QRCode, TimeAttendance = _get_models()["Employee"], _get_models()["Project"], _get_models()["QRCode"], _get_models()["TimeAttendance"] from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Border, Side, Alignment from openpyxl.utils import get_column_letter @@ -3388,7 +3367,6 @@ def export_time_attendance_by_building_excel(records, project_name_for_filename, @log_user_activity('time_attendance_records_view') def time_attendance_records(): """Display time attendance records with filtering options""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: # Get filter parameters employee_filter = request.args.get('employee_id', '') @@ -3539,14 +3517,13 @@ def time_attendance_records(): except Exception as e: logger_handler.logger.error(f"Error displaying time attendance records: {e}") flash('Error loading attendance records.', 'error') - return redirect(url_for('time_attendance_dashboard')) + return redirect(url_for('time_attendance.time_attendance_dashboard')) @bp.route('/time-attendance/record/', endpoint='time_attendance_record_detail') @login_required @log_user_activity('time_attendance_record_detail') def time_attendance_record_detail(record_id): """Display detailed view of a time attendance record""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: record = TimeAttendance.query.get_or_404(record_id) return render_template('time_attendance_record_detail.html', record=record) @@ -3554,14 +3531,13 @@ def time_attendance_record_detail(record_id): except Exception as e: logger_handler.logger.error(f"Error viewing time attendance record {record_id}: {e}") flash('Error loading record details.', 'error') - return redirect(url_for('time_attendance_records')) + return redirect(url_for('time_attendance.time_attendance_records')) @bp.route('/time-attendance/delete/', methods=['POST'], endpoint='delete_time_attendance_record') @admin_required @log_database_operations('time_attendance_delete') def delete_time_attendance_record(record_id): """Delete a time attendance record""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: record = TimeAttendance.query.get_or_404(record_id) @@ -3605,13 +3581,12 @@ def delete_time_attendance_record(record_id): filter_params[key] = value # Redirect back with filters preserved - return redirect(url_for('time_attendance_records', **filter_params)) + return redirect(url_for('time_attendance.time_attendance_records', **filter_params)) @bp.route('/api/time-attendance/employee/', endpoint='api_time_attendance_by_employee') @login_required def api_time_attendance_by_employee(employee_id): """API endpoint to get time attendance records for a specific employee""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: start_date = request.args.get('start_date') end_date = request.args.get('end_date') @@ -3644,7 +3619,6 @@ def api_time_attendance_by_employee(employee_id): @login_required def api_time_attendance_by_location(location_name): """API endpoint to get time attendance records for a specific location""" - TimeAttendance, Employee, Project, AttendanceData, QRCode, User = _get_models()["TimeAttendance"], _get_models()["Employee"], _get_models()["Project"], _get_models()["AttendanceData"], _get_models()["QRCode"], _get_models()["User"] try: start_date = request.args.get('start_date') end_date = request.args.get('end_date') diff --git a/routes/users.py b/routes/users.py index 5ab8f02..cb20261 100644 --- a/routes/users.py +++ b/routes/users.py @@ -6,14 +6,18 @@ User management routes (admin-only operations). Routes: /users/*, /api/users/stats, /api/locations-by-projects, /api/roles/permissions, /api/geocode, /api/reverse-geocode """ -from flask import Blueprint, render_template, request, redirect, flash, session, jsonify +from flask import Blueprint, render_template, request, redirect, flash, session, jsonify, url_for from datetime import datetime, timedelta import json from extensions import db, logger_handler +from models.permissions import UserLocationPermission, UserProjectPermission +from models.project import Project +from models.qrcode import QRCode +from models.user import User from sqlalchemy import text from logger_handler import log_user_activity, log_database_operations -from utils.helpers import (url_for, +from utils.helpers import ( admin_required, generate_qr_code, get_qr_styling, @@ -34,31 +38,25 @@ from werkzeug.security import generate_password_hash bp = Blueprint('users', __name__) -def _get_models(): - """Return model classes from the current app context.""" - from flask import current_app - return current_app.config['_models'] @bp.route('/users', endpoint='users') @admin_required def users(): """Display all users (Admin only)""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: users = User.query.order_by(User.created_date.desc()).all() return render_template('users.html', users=users) except Exception as e: logger_handler.log_database_error('users_list', e) flash('Error loading users list.', 'error') - return redirect(url_for('dashboard')) + return redirect(url_for('dashboard.dashboard')) @bp.route('/users/create', methods=['GET', 'POST'], endpoint='create_user') @admin_required @log_database_operations('user_creation') def create_user(): """Create new user (Admin only) with Project Manager permissions support""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] if request.method == 'POST': try: # Get basic form data @@ -175,7 +173,7 @@ def create_user(): logger_handler.logger.info(f"Admin user {session['username']} created new user: {username} with role {role}") flash(f'User "{full_name}" created successfully with role "{role}".', 'success') - return redirect(url_for('users')) + return redirect(url_for('users.users')) except KeyError as e: db.session.rollback() @@ -201,7 +199,7 @@ def create_user(): except Exception as e: logger_handler.logger.error(f"Error loading create user form: {e}") flash('Error loading form. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) def get_all_locations_from_qr_codes(): """Helper function to get all unique locations from QR codes""" @@ -222,26 +220,25 @@ def get_all_locations_from_qr_codes(): @admin_required def delete_user(user_id): """Deactivate user (Admin only) - Fixed with proper validation""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: user_to_delete = User.query.get(user_id) current_user = User.query.get(session['user_id']) if not user_to_delete: flash('User not found.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # Prevent self-deletion if user_to_delete.id == current_user.id: flash('You cannot deactivate your own account. Ask another admin to do this.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # Check if trying to delete the last admin if user_to_delete.role == 'admin': active_admin_count = User.query.filter_by(role='admin', active_status=True).count() if active_admin_count <= 1: flash('Cannot deactivate the last admin user. Promote another user to admin first.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # Deactivate the user instead of deleting user_to_delete.active_status = False @@ -250,26 +247,25 @@ def delete_user(user_id): flash(f'User "{user_to_delete.full_name}" has been deactivated successfully.', 'success') print(f"Admin {current_user.username} deactivated user: {user_to_delete.username}") - return redirect(url_for('users')) + return redirect(url_for('users.users')) except Exception as e: db.session.rollback() print(f"Error deactivating user: {e}") flash('Error deactivating user. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) @bp.route('/users//reactivate', methods=['GET', 'POST'], endpoint='reactivate_user') @admin_required def reactivate_user(user_id): """Reactivate a deactivated user (Admin only)""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: user_to_reactivate = User.query.get(user_id) current_user = User.query.get(session['user_id']) if not user_to_reactivate: flash('User not found.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) if user_to_reactivate.active_status: flash('User is already active.', 'info') @@ -279,26 +275,25 @@ def reactivate_user(user_id): flash(f'User "{user_to_reactivate.full_name}" has been reactivated successfully.', 'success') print(f"Admin {current_user.username} reactivated user: {user_to_reactivate.username}") - return redirect(url_for('users')) + return redirect(url_for('users.users')) except Exception as e: db.session.rollback() print(f"Error reactivating user: {e}") flash('Error reactivating user. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) @bp.route('/users//promote', methods=['GET', 'POST'], endpoint='promote_user') @admin_required def promote_user(user_id): """Promote a staff user to admin (Admin only)""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: user_to_promote = User.query.get(user_id) current_user = User.query.get(session['user_id']) if not user_to_promote: flash('User not found.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) if user_to_promote.role == 'admin': flash('User is already an admin.', 'info') @@ -308,37 +303,36 @@ def promote_user(user_id): flash(f'"{user_to_promote.full_name}" has been promoted to admin.', 'success') print(f"Admin {current_user.username} promoted user {user_to_promote.username} to admin") - return redirect(url_for('users')) + return redirect(url_for('users.users')) except Exception as e: db.session.rollback() print(f"Error promoting user: {e}") flash('Error promoting user. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) @bp.route('/users//demote', methods=['GET', 'POST'], endpoint='demote_user') @admin_required def demote_user(user_id): """Demote an admin user to staff (Admin only)""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: user_to_demote = User.query.get(user_id) current_user = User.query.get(session['user_id']) if not user_to_demote: flash('User not found.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # Prevent self-demotion if user_to_demote.id == current_user.id: flash('You cannot demote yourself. Have another admin do this.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # Check if this is the last admin active_admin_count = User.query.filter_by(role='admin', active_status=True).count() if active_admin_count <= 1 and user_to_demote.role == 'admin': flash('Cannot demote the last admin user. Promote another user to admin first.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) if has_staff_level_access(user_to_demote.role): flash('User already has staff-level permissions.', 'info') @@ -348,20 +342,19 @@ def demote_user(user_id): flash(f'"{user_to_demote.full_name}" has been demoted to staff.', 'success') print(f"Admin {current_user.username} demoted user {user_to_demote.username} to staff") - return redirect(url_for('users')) + return redirect(url_for('users.users')) except Exception as e: db.session.rollback() print(f"Error demoting user: {e}") flash('Error demoting user. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) @bp.route('/users//edit', methods=['GET', 'POST'], endpoint='edit_user') @admin_required @log_database_operations('user_edit') def edit_user(user_id): """Edit existing user with Project Manager permissions support""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: user_to_edit = User.query.get_or_404(user_id) @@ -533,7 +526,7 @@ def edit_user(user_id): logger_handler.logger.info(f"Admin user {session['username']} updated user {user_to_edit.username}: {json.dumps(changes, default=str)}") flash(f'User "{user_to_edit.full_name}" updated successfully.', 'success') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # GET request - load form with current assignments try: @@ -561,20 +554,19 @@ def edit_user(user_id): except Exception as e: logger_handler.logger.error(f"Error loading edit user form: {e}") flash('Error loading edit form. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) except Exception as e: db.session.rollback() logger_handler.log_database_error('user_update', e) logger_handler.logger.error(f"User update error details: {str(e)}") flash('Error updating user. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) @bp.route('/users//toggle-status', methods=['POST'], endpoint='toggle_user_status') @admin_required def toggle_user_status(user_id): """Toggle user active status via AJAX (Admin only)""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: user_to_toggle = User.query.get(user_id) current_user = User.query.get(session['user_id']) @@ -634,14 +626,13 @@ def toggle_user_status(user_id): @admin_required def activate_user(user_id): """Activate a user (Admin only) - Alternative route""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: user_to_activate = User.query.get(user_id) current_user = User.query.get(session['user_id']) if not user_to_activate: flash('User not found.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) if user_to_activate.active_status: flash('User is already active.', 'info') @@ -655,39 +646,38 @@ def activate_user(user_id): flash(f'"{user_to_activate.full_name}" has been activated.', 'success') print(f"Admin {current_user.username} activated user {user_to_activate.username}") - return redirect(url_for('users')) + return redirect(url_for('users.users')) except Exception as e: db.session.rollback() logger_handler.log_database_error('user_activation', e) print(f"Error activating user: {e}") flash('Error activating user. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) @bp.route('/users//deactivate', methods=['GET', 'POST'], endpoint='deactivate_user') @admin_required def deactivate_user(user_id): """Deactivate a user (Admin only) - Alternative route""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: user_to_deactivate = User.query.get(user_id) current_user = User.query.get(session['user_id']) if not user_to_deactivate: flash('User not found.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # Prevent self-deactivation if user_to_deactivate.id == current_user.id: flash('You cannot deactivate yourself.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # Check if this is the last admin if user_to_deactivate.role == 'admin' and user_to_deactivate.active_status: active_admin_count = User.query.filter_by(role='admin', active_status=True).count() if active_admin_count <= 1: flash('Cannot deactivate the last admin user.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) if not user_to_deactivate.active_status: flash('User is already inactive.', 'info') @@ -701,21 +691,20 @@ def deactivate_user(user_id): flash(f'"{user_to_deactivate.full_name}" has been deactivated.', 'success') print(f"Admin {current_user.username} deactivated user {user_to_deactivate.username}") - return redirect(url_for('users')) + return redirect(url_for('users.users')) except Exception as e: db.session.rollback() logger_handler.log_database_error('user_deactivation', e) print(f"Error deactivating user: {e}") flash('Error deactivating user. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # ENHANCED USER STATISTICS API @bp.route('/api/users/stats', endpoint='user_stats_api') @admin_required def user_stats_api(): """API endpoint to get user statistics for dashboard""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: # Get current date for recent activity calculations one_week_ago = datetime.now() - timedelta(days=7) @@ -759,7 +748,6 @@ def user_stats_api(): @admin_required def get_locations_by_projects(): """Get locations that belong to selected projects""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: data = request.get_json() project_ids = data.get('project_ids', []) @@ -801,7 +789,6 @@ def get_locations_by_projects(): @admin_required def role_permissions_api(): """API endpoint to get role permissions data""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: permissions_data = {} for role in VALID_ROLES: @@ -822,7 +809,6 @@ def role_permissions_api(): @login_required def geocode_address_api(): """API endpoint to geocode an address and return coordinates using Google Maps""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: data = request.get_json() address = data.get('address', '').strip() @@ -890,7 +876,6 @@ def geocode_address_api(): @login_required def reverse_geocode_api(): """API endpoint for reverse geocoding coordinates to address using Google Maps""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: data = request.get_json() latitude = data.get('latitude') @@ -945,7 +930,6 @@ def reverse_geocode_api(): @admin_required def permanently_delete_user(user_id): """Permanently delete user but preserve associated QR codes (Admin only)""" - User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"] try: user_to_delete = User.query.get_or_404(user_id) current_user = User.query.get(session['user_id']) @@ -953,19 +937,19 @@ def permanently_delete_user(user_id): # Security checks if user_to_delete.id == current_user.id: flash('You cannot delete your own account.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # Only allow deletion of inactive users for safety if user_to_delete.active_status: flash('User must be deactivated before permanent deletion.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # If deleting an admin, ensure at least one admin remains if user_to_delete.role == 'admin': active_admin_count = User.query.filter_by(role='admin', active_status=True).count() if active_admin_count <= 1: flash('Cannot delete the last admin user in the system.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) user_name = user_to_delete.full_name user_qr_count = user_to_delete.created_qr_codes.count() @@ -997,13 +981,13 @@ def permanently_delete_user(user_id): flash(f'User "{user_name}" has been permanently deleted. {user_qr_count} QR codes created by this user are now orphaned but preserved.', 'success') print(f"Admin {current_user.username} permanently deleted user: {username}, preserved {user_qr_count} QR codes") - return redirect(url_for('users')) + return redirect(url_for('users.users')) except Exception as e: db.session.rollback() logger_handler.log_database_error('user_permanent_deletion', e) print(f"Error permanently deleting user: {e}") flash('Error deleting user. Please try again.', 'error') - return redirect(url_for('users')) + return redirect(url_for('users.users')) # Admin logging routes \ No newline at end of file diff --git a/templates/add_manual_attendance.html b/templates/add_manual_attendance.html index 5b0e990..7c389f9 100644 --- a/templates/add_manual_attendance.html +++ b/templates/add_manual_attendance.html @@ -212,7 +212,7 @@ with a fixed distance of 0.010 miles. -
+