Mar 20 2026: refactor 2

This commit is contained in:
2026-03-20 17:14:58 -04:00
parent 7caf12a760
commit f6ad3bd930
49 changed files with 9173 additions and 9332 deletions
+3 -7
View File
@@ -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"""
+35 -53
View File
@@ -9,14 +9,20 @@ Routes: /attendance, /attendance/<id>/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/<int:record_id>/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/<int:record_id>/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}")
+9 -17
View File
@@ -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'))
+9 -14
View File
@@ -6,26 +6,25 @@ Dashboard and related API routes.
Routes: /dashboard, /project/<id>/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/<int:project_id>/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(
+15 -22
View File
@@ -6,12 +6,17 @@ Employee CRUD and search routes.
Routes: /employees, /employees/create, /employees/<id>/edit,
/employees/<id>/delete, /api/employees/search, /employees/<id>
"""
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/<int:employee_index>/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'))
+15 -20
View File
@@ -6,14 +6,19 @@ Payroll dashboard and Excel export routes.
Routes: /payroll, /payroll/export-excel, /api/working-hours/calculate,
/api/employee/<id>/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}")
+9 -16
View File
@@ -6,41 +6,37 @@ Project CRUD and related API routes.
Routes: /projects, /projects/create, /projects/<id>/edit,
/projects/<id>/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/<int:project_id>/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()
+17 -29
View File
@@ -6,15 +6,19 @@ QR code management and destination handler routes.
Routes: /qr-codes/create, /qr-codes/bulk-import, /qr-codes/<id>/*,
/qr/<string:qr_url>
"""
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/<int:qr_id>/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/<int:qr_id>/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/<string:qr_url>', 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/<string:qr_url>/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
+7 -10
View File
@@ -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
+28 -54
View File
@@ -9,17 +9,21 @@ Routes: /time-attendance, /time-attendance/import/*,
/time-attendance/record/<id>, /time-attendance/delete/<id>,
/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/<batch_id>/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/<int:record_id>', 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/<int:record_id>', 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/<employee_id>', 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')
+41 -57
View File
@@ -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/<int:user_id>/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/<int:user_id>/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/<int:user_id>/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/<int:user_id>/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/<int:user_id>/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/<int:user_id>/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