Mar 20 2026: refactor 2
This commit is contained in:
+3
-7
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user