Mar 20 2026: refactor 2

This commit is contained in:
2026-03-20 17:14:58 -04:00
parent 7caf12a760
commit f6ad3bd930
49 changed files with 9173 additions and 9332 deletions
+5 -14
View File
@@ -70,13 +70,7 @@ def create_app() -> Flask:
Employee, TimeAttendance, UserProjectPermission, Employee, TimeAttendance, UserProjectPermission,
UserLocationPermission) = set_db(db) UserLocationPermission) = set_db(db)
app.config['_models'] = {
'User': User, 'QRCode': QRCode, 'QRCodeStyle': QRCodeStyle,
'Project': Project, 'AttendanceData': AttendanceData,
'Employee': Employee, 'TimeAttendance': TimeAttendance,
'UserProjectPermission': UserProjectPermission,
'UserLocationPermission': UserLocationPermission,
}
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Logger initialization # Logger initialization
@@ -108,9 +102,7 @@ def create_app() -> Flask:
from extensions import logger_handler as _lh from extensions import logger_handler as _lh
create_location_logging_routes(app, db, _lh) create_location_logging_routes(app, db, _lh)
# Patch Jinja2's url_for global so templates also use the compatibility shim
from utils.helpers import url_for as _compat_url_for
app.jinja_env.globals['url_for'] = _compat_url_for
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Template filters (global — must be on app, not blueprints) # Template filters (global — must be on app, not blueprints)
@@ -155,9 +147,8 @@ def create_app() -> Flask:
def get_qr_code_checkin_count(qr_code_id): def get_qr_code_checkin_count(qr_code_id):
"""Helper function to get total check-ins count for a QR code""" """Helper function to get total check-ins count for a QR code"""
from flask import current_app from models.attendance import AttendanceData
try: try:
AttendanceData = current_app.config['_models']['AttendanceData']
count = AttendanceData.query.filter_by(qr_code_id=qr_code_id).count() count = AttendanceData.query.filter_by(qr_code_id=qr_code_id).count()
return count return count
except Exception as e: except Exception as e:
@@ -325,7 +316,7 @@ def create_tables():
try: try:
_db.create_all() _db.create_all()
from flask import current_app from flask import current_app
User = current_app.config['_models']['User'] from models.user import User
admin = User.query.filter_by(username='admin').first() admin = User.query.filter_by(username='admin').first()
if not admin: if not admin:
default_password = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123') default_password = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123')
@@ -359,7 +350,7 @@ def update_existing_qr_codes():
from utils.helpers import generate_qr_code, get_qr_styling, generate_qr_url from utils.helpers import generate_qr_code, get_qr_styling, generate_qr_url
from flask import current_app, request from flask import current_app, request
try: try:
QRCode = current_app.config['_models']['QRCode'] from models.qrcode import QRCode
qr_codes = QRCode.query.filter_by(active_status=True).all() qr_codes = QRCode.query.filter_by(active_status=True).all()
updated_count = 0 updated_count = 0
for qr_code in qr_codes: for qr_code in qr_codes:
+3 -7
View File
@@ -5,7 +5,7 @@ Admin panel and log management routes.
Routes: /admin/logs, /admin/health/google-maps, /api/logs/* 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 from datetime import datetime, timedelta
import json, math import json, math
@@ -13,14 +13,10 @@ from extensions import db, logger_handler
from sqlalchemy import text from sqlalchemy import text
from utils.geocoding import gmaps_client from utils.geocoding import gmaps_client
from logger_handler import log_user_activity, log_database_operations 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__) 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') @bp.route('/admin/logs', endpoint='admin_logs')
@@ -34,7 +30,7 @@ def admin_logs():
except Exception as e: except Exception as e:
logger_handler.log_database_error('admin_logs_load', e) logger_handler.log_database_error('admin_logs_load', e)
flash('Error loading log statistics.', 'error') flash('Error loading log statistics.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
def check_google_maps_health(): def check_google_maps_health():
"""Check if Google Maps services are working properly""" """Check if Google Maps services are working properly"""
+35 -53
View File
@@ -9,14 +9,20 @@ Routes: /attendance, /attendance/<id>/edit, /attendance/add,
/api/get_project_locations, /verification-review/*, /api/get_project_locations, /verification-review/*,
/export-configuration, /generate-excel-export /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 from datetime import datetime, date, timedelta, time
import io, os, json, re, traceback import io, os, json, re, traceback
from extensions import db, logger_handler 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 sqlalchemy import text
from logger_handler import log_user_activity, log_database_operations from logger_handler import log_user_activity, log_database_operations
from utils.helpers import (url_for, from utils.helpers import (
admin_required, admin_required,
get_client_ip, get_client_ip,
has_admin_privileges, has_admin_privileges,
@@ -31,17 +37,12 @@ from openpyxl.utils import get_column_letter
bp = Blueprint('attendance', __name__) 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') @bp.route('/attendance', endpoint='attendance_report')
@login_required @login_required
def attendance_report(): def attendance_report():
"""Safe attendance report with backward compatibility for location_accuracy and fixed datetime handling""" """Safe attendance report with backward compatibility for location_accuracy and fixed datetime handling"""
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: try:
print("📊 Loading attendance report...") print("📊 Loading attendance report...")
@@ -540,18 +541,17 @@ def attendance_report():
print(f"⚠️ Additional logging error: {log_error}") print(f"⚠️ Additional logging error: {log_error}")
flash('Error loading attendance report. Please check the server logs for details.', 'error') flash('Error loading attendance report. Please check the server logs for details.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
@bp.route('/attendance/<int:record_id>/edit', methods=['GET', 'POST'], endpoint='edit_attendance') @bp.route('/attendance/<int:record_id>/edit', methods=['GET', 'POST'], endpoint='edit_attendance')
@login_required @login_required
@log_database_operations('attendance_update') @log_database_operations('attendance_update')
def edit_attendance(record_id): def edit_attendance(record_id):
"""Edit attendance record (Admin and Payroll only)""" """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 # Check if user has permission to edit attendance records
if session.get('role') not in ['admin', 'payroll', 'accounting']: if session.get('role') not in ['admin', 'payroll', 'accounting']:
flash('Access denied. Only administrators and accounting staff can edit attendance records.', 'error') 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: try:
attendance_record = AttendanceData.query.get_or_404(record_id) 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}") print(f"[LOG] Edit reason: {edit_note}")
flash(f'Attendance record for {new_employee_id} updated successfully! Edit reason logged for audit.', 'success') flash(f'Attendance record for {new_employee_id} updated successfully! Edit reason logged for audit.', 'success')
return redirect(url_for('attendance_report')) return redirect(url_for('attendance.attendance_report'))
# GET request - show edit form # GET request - show edit form
# Get available projects for the dropdown # Get available projects for the dropdown
@@ -695,7 +695,7 @@ def edit_attendance(record_id):
logger_handler.log_database_error('attendance_update', e) logger_handler.log_database_error('attendance_update', e)
print(f"[LOG] Error updating attendance record {record_id}: {e}") print(f"[LOG] Error updating attendance record {record_id}: {e}")
flash('Error updating attendance record. Please try again.', 'error') 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') @bp.route('/attendance/add', methods=['GET'], endpoint='add_manual_attendance')
@login_required @login_required
@@ -705,14 +705,13 @@ def add_manual_attendance():
Display form to manually add attendance record Display form to manually add attendance record
Only accessible by admin and accounting roles 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: try:
user_role = session.get('role') user_role = session.get('role')
# Check authorization # Check authorization
if user_role not in ['admin', 'accounting']: if user_role not in ['admin', 'accounting']:
flash('You do not have permission to manually add attendance records.', 'error') 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 # Get all active projects
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
@@ -731,7 +730,7 @@ def add_manual_attendance():
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error loading manual attendance form: {e}") logger_handler.logger.error(f"Error loading manual attendance form: {e}")
flash('Error loading form. Please try again.', 'error') 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') @bp.route('/attendance/save_manual', methods=['POST'], endpoint='save_manual_attendance')
@@ -743,7 +742,6 @@ def save_manual_attendance():
Save manually created attendance record Save manually created attendance record
Only accessible by admin and accounting roles 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: try:
user_role = session.get('role') user_role = session.get('role')
@@ -763,19 +761,19 @@ def save_manual_attendance():
# Validate required fields # Validate required fields
if not all([employee_id, location_id, check_date, check_time]): if not all([employee_id, location_id, check_date, check_time]):
flash('All fields are required.', 'error') flash('All fields are required.', 'error')
return redirect(url_for('add_manual_attendance')) return redirect(url_for('attendance.add_manual_attendance'))
# Validate employee exists # Validate employee exists
employee = Employee.query.filter_by(id=int(employee_id)).first() employee = Employee.query.filter_by(id=int(employee_id)).first()
if not employee: if not employee:
flash(f'Employee with ID {employee_id} not found.', 'error') 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) # Get QR code (location)
qr_code = QRCode.query.get(int(location_id)) qr_code = QRCode.query.get(int(location_id))
if not qr_code: if not qr_code:
flash('Selected location not found.', 'error') flash('Selected location not found.', 'error')
return redirect(url_for('add_manual_attendance')) return redirect(url_for('attendance.add_manual_attendance'))
# Parse date and time # Parse date and time
try: try:
@@ -784,7 +782,7 @@ def save_manual_attendance():
except ValueError as e: except ValueError as e:
flash('Invalid date or time format.', 'error') flash('Invalid date or time format.', 'error')
logger_handler.logger.error(f"Date/time parsing error: {e}") 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 # Check if record already exists for this employee, location, date, and time
existing_record = AttendanceData.query.filter_by( existing_record = AttendanceData.query.filter_by(
@@ -796,7 +794,7 @@ def save_manual_attendance():
if existing_record: if existing_record:
flash('An attendance record already exists for this employee at this location, date, and time.', 'warning') 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 # Create new attendance record
# Use QR code's location address for both QR address and check-in address # 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') 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: except Exception as e:
db.session.rollback() db.session.rollback()
logger_handler.logger.error(f"Error saving manual attendance record: {e}") logger_handler.logger.error(f"Error saving manual attendance record: {e}")
logger_handler.logger.error(f"Traceback: {traceback.format_exc()}") logger_handler.logger.error(f"Traceback: {traceback.format_exc()}")
flash('Error saving attendance record. Please try again.', 'error') 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') @bp.route('/api/time-attendance/locations', endpoint='time_attendance_locations_api')
@@ -854,7 +852,6 @@ def save_manual_attendance():
def time_attendance_locations_api(): def time_attendance_locations_api():
"""Return distinct location_name values from time_attendance, optionally filtered by project_id. """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.""" 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: try:
project_id = request.args.get('project_id', '').strip() project_id = request.args.get('project_id', '').strip()
@@ -896,7 +893,6 @@ def time_attendance_locations_api():
def attendance_locations_api(): def attendance_locations_api():
"""Return distinct location_name values from attendance_data, optionally filtered by project_id. """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.""" 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: try:
project_id = request.args.get('project_id', '').strip() 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 in attendance_data that have no Employee record — so unregistered IDs
that have attendance records can still be filtered on the attendance page. 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: try:
search_query = request.args.get('q', '').strip() 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 API endpoint to get locations for a specific project
Returns JSON with location list 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: try:
project_id = request.args.get('project_id', '').strip() project_id = request.args.get('project_id', '').strip()
@@ -1062,7 +1056,6 @@ def get_project_locations_api():
@log_database_operations('attendance_delete') @log_database_operations('attendance_delete')
def delete_attendance(record_id): def delete_attendance(record_id):
"""Delete attendance record (Admin and Payroll only)""" """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 # Check if user has permission to delete attendance records
if session.get('role') not in ['admin', 'payroll', 'accounting']: if session.get('role') not in ['admin', 'payroll', 'accounting']:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest': if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
@@ -1072,7 +1065,7 @@ def delete_attendance(record_id):
}), 403 }), 403
else: else:
flash('Access denied. Only administrators and payroll staff can delete attendance records.', 'error') 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: try:
attendance_record = AttendanceData.query.get_or_404(record_id) attendance_record = AttendanceData.query.get_or_404(record_id)
@@ -1110,7 +1103,7 @@ def delete_attendance(record_id):
}) })
else: else:
flash(f'Attendance record for {employee_id} deleted successfully!', 'success') 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: except Exception as e:
db.session.rollback() db.session.rollback()
@@ -1124,18 +1117,17 @@ def delete_attendance(record_id):
}), 500 }), 500
else: else:
flash('Error deleting attendance record. Please try again.', 'error') 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') @bp.route('/verification-review', endpoint='verification_review')
@login_required @login_required
def verification_review(): def verification_review():
"""Admin page to review pending photo verifications""" """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: try:
# Only admins can access # Only admins can access
if session.get('role') not in ['admin', 'payroll', 'accounting']: if session.get('role') not in ['admin', 'payroll', 'accounting']:
flash('Unauthorized access.', 'error') flash('Unauthorized access.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
# Get filter parameters # Get filter parameters
status_filter = request.args.get('status', 'pending') status_filter = request.args.get('status', 'pending')
@@ -1253,14 +1245,13 @@ def verification_review():
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error in verification review: {e}") logger_handler.logger.error(f"Error in verification review: {e}")
flash('Error loading verification review.', 'error') 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') @bp.route('/verification-review/<int:record_id>/update', methods=['POST'], endpoint='update_verification_status')
@login_required @login_required
@log_database_operations('verification_update') @log_database_operations('verification_update')
def update_verification_status(record_id): def update_verification_status(record_id):
"""Update verification status (approve/reject)""" """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: try:
# Only admins can update # Only admins can update
if session.get('role') not in ['admin', 'payroll', 'accounting']: if session.get('role') not in ['admin', 'payroll', 'accounting']:
@@ -1311,7 +1302,6 @@ def update_verification_status(record_id):
@login_required @login_required
def get_verification_details(record_id): def get_verification_details(record_id):
"""API endpoint to get verification details for a specific record""" """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: try:
# Get the attendance record with verification data # Get the attendance record with verification data
record = AttendanceData.query.get_or_404(record_id) record = AttendanceData.query.get_or_404(record_id)
@@ -1397,12 +1387,11 @@ def get_verification_details(record_id):
@login_required @login_required
def verification_review_detail(record_id): def verification_review_detail(record_id):
"""Review a single verification photo on a dedicated page""" """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: try:
# Check permissions # Check permissions
if session.get('role') not in ['admin', 'payroll', 'accounting']: if session.get('role') not in ['admin', 'payroll', 'accounting']:
flash('Access denied. Only administrators, payroll, and accounting staff can review verification photos.', 'error') 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 # Get the attendance record
record = AttendanceData.query.get_or_404(record_id) record = AttendanceData.query.get_or_404(record_id)
@@ -1410,7 +1399,7 @@ def verification_review_detail(record_id):
# Check if this record has verification # Check if this record has verification
if not record.verification_required: if not record.verification_required:
flash('This record does not require verification.', 'warning') 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 # Get the QR code information for additional context
qr_code = QRCode.query.get(record.qr_code_id) if record.qr_code_id else None qr_code = 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: except Exception as e:
logger_handler.logger.error(f"Error loading verification review detail: {e}") logger_handler.logger.error(f"Error loading verification review detail: {e}")
flash('Error loading verification details.', 'error') 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') @bp.route('/api/attendance/stats', endpoint='attendance_stats_api')
@admin_required @admin_required
def attendance_stats_api(): def attendance_stats_api():
"""API endpoint for attendance statistics""" """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: try:
# Daily stats for the last 7 days # Daily stats for the last 7 days
daily_stats = db.session.execute(text(""" daily_stats = db.session.execute(text("""
@@ -1516,13 +1504,12 @@ def attendance_stats_api():
@login_required @login_required
def export_configuration(): def export_configuration():
"""Display export configuration page for customizing Excel exports""" """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: try:
user_role = session.get('role') user_role = session.get('role')
if user_role not in ['admin', 'payroll', 'accounting']: 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") 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') 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 # Log export configuration access using your existing logger
try: try:
@@ -1546,7 +1533,6 @@ def export_configuration():
project_name = None project_name = None
if filters.get('project_filter'): if filters.get('project_filter'):
try: try:
from models.project import Project
project = Project.query.get(int(filters['project_filter'])) project = Project.query.get(int(filters['project_filter']))
if project: if project:
project_name = project.name project_name = project.name
@@ -1611,19 +1597,18 @@ def export_configuration():
print(f"⚠️ Could not log error: {log_error}") print(f"⚠️ Could not log error: {log_error}")
flash('Error loading export configuration page.', 'error') flash('Error loading export configuration page.', 'error')
return redirect(url_for('attendance_report')) return redirect(url_for('attendance.attendance_report'))
@bp.route('/generate-excel-export', methods=['POST'], endpoint='generate_excel_export') @bp.route('/generate-excel-export', methods=['POST'], endpoint='generate_excel_export')
@login_required @login_required
def generate_excel_export(): def generate_excel_export():
"""Generate and download Excel file with selected columns in specified order""" """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: try:
user_role = session.get('role') user_role = session.get('role')
if user_role not in ['admin', 'payroll', 'accounting']: 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") 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') 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") print("📊 Excel export generation started")
@@ -1662,7 +1647,7 @@ def generate_excel_export():
if not selected_columns: if not selected_columns:
flash('Please select at least one column to export.', 'error') flash('Please select at least one column to export.', 'error')
return redirect(url_for('export_configuration')) return redirect(url_for('attendance.export_configuration'))
column_names = {} column_names = {}
for column in selected_columns: for column in selected_columns:
@@ -1695,7 +1680,6 @@ def generate_excel_export():
project_name_for_filename = '' project_name_for_filename = ''
if filters.get('project_filter'): if filters.get('project_filter'):
try: try:
from models.project import Project
project = Project.query.get(int(filters['project_filter'])) project = Project.query.get(int(filters['project_filter']))
if project: if project:
# Replace spaces and special characters with underscores # Replace spaces and special characters with underscores
@@ -1750,7 +1734,7 @@ def generate_excel_export():
) )
else: else:
flash('Error generating Excel file.', 'error') flash('Error generating Excel file.', 'error')
return redirect(url_for('export_configuration')) return redirect(url_for('attendance.export_configuration'))
except Exception as e: except Exception as e:
print(f"❌ Error in generate_excel_export route: {e}") 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}") print(f"⚠️ Could not log error: {log_error}")
flash('Error generating Excel export.', '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): def create_excel_export(selected_columns, column_names, filters):
"""Create Excel file with selected attendance data - Updated to include employee names""" """Create Excel file with selected attendance data - Updated to include employee names"""
AttendanceData, Employee, QRCode = _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["QRCode"]
try: try:
print(f"📊 Creating Excel export with {len(selected_columns)} columns") 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): def create_excel_export_ordered(selected_columns, column_names, filters):
"""Create Excel file with selected attendance data in specified column order""" """Create Excel file with selected attendance data in specified column order"""
AttendanceData, Employee, QRCode = _get_models()["AttendanceData"], _get_models()["Employee"], _get_models()["QRCode"]
try: try:
print(f"📊 Creating Excel export with {len(selected_columns)} columns in order: {selected_columns}") print(f"📊 Creating Excel export with {len(selected_columns)} columns in order: {selected_columns}")
+9 -17
View File
@@ -5,36 +5,31 @@ Authentication and user-profile routes.
Routes: /, /register, /login, /logout, /profile 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 from datetime import datetime
import json import json
from extensions import db, logger_handler from extensions import db, logger_handler
from models.user import User
from logger_handler import log_user_activity, log_database_operations 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 from turnstile_utils import turnstile_utils
bp = Blueprint('auth', __name__) 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') @bp.route('/', endpoint='index')
def index(): def index():
"""Home page - redirect to login if not authenticated""" """Home page - redirect to login if not authenticated"""
User = _get_models()["User"]
if 'user_id' in session: if 'user_id' in session:
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
return redirect(url_for('login')) return redirect(url_for('auth.login'))
@bp.route('/register', methods=['GET', 'POST'], endpoint='register') @bp.route('/register', methods=['GET', 'POST'], endpoint='register')
@log_user_activity('user_registration') @log_user_activity('user_registration')
def register(): def register():
"""User registration endpoint""" """User registration endpoint"""
User = _get_models()["User"]
if request.method == 'POST': if request.method == 'POST':
try: try:
full_name = request.form['full_name'] full_name = request.form['full_name']
@@ -67,7 +62,7 @@ def register():
logger_handler.logger.info(f"New user registered: {username} ({email})") logger_handler.logger.info(f"New user registered: {username} ({email})")
flash('Registration successful! Please log in.', 'success') flash('Registration successful! Please log in.', 'success')
return redirect(url_for('login')) return redirect(url_for('auth.login'))
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
@@ -79,7 +74,6 @@ def register():
@bp.route('/login', methods=['GET', 'POST'], endpoint='login') @bp.route('/login', methods=['GET', 'POST'], endpoint='login')
def login(): def login():
"""Enhanced user authentication with Turnstile and comprehensive logging""" """Enhanced user authentication with Turnstile and comprehensive logging"""
User = _get_models()["User"]
if request.method == 'POST': if request.method == 'POST':
username = request.form.get('username', '').strip() username = request.form.get('username', '').strip()
password = request.form.get('password', '') password = request.form.get('password', '')
@@ -151,7 +145,7 @@ def login():
# Redirect to intended page or dashboard # Redirect to intended page or dashboard
next_page = request.args.get('next') 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: else:
# Invalid credentials - log failed attempt # Invalid credentials - log failed attempt
@@ -176,7 +170,6 @@ def login():
@bp.route('/logout', endpoint='logout') @bp.route('/logout', endpoint='logout')
def logout(): def logout():
"""User logout endpoint with session duration logging""" """User logout endpoint with session duration logging"""
User = _get_models()["User"]
user_id = session.get('user_id') user_id = session.get('user_id')
username = session.get('username') username = session.get('username')
login_time_str = session.get('login_time') login_time_str = session.get('login_time')
@@ -200,14 +193,13 @@ def logout():
session.clear() session.clear()
flash('You have been logged out.', 'info') 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') @bp.route('/profile', methods=['GET', 'POST'], endpoint='profile')
@login_required @login_required
@log_user_activity('profile_update') @log_user_activity('profile_update')
def profile(): def profile():
"""User profile management with logging""" """User profile management with logging"""
User = _get_models()["User"]
try: try:
user = User.query.get(session['user_id']) user = User.query.get(session['user_id'])
@@ -269,4 +261,4 @@ def profile():
except Exception as e: except Exception as e:
logger_handler.log_database_error('profile_update', e) logger_handler.log_database_error('profile_update', e)
flash('Profile update failed. Please try again.', 'error') flash('Profile update failed. Please try again.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
+9 -14
View File
@@ -6,26 +6,25 @@ Dashboard and related API routes.
Routes: /dashboard, /project/<id>/qr-codes, /dashboard/search, Routes: /dashboard, /project/<id>/qr-codes, /dashboard/search,
/api/dashboard/stats, /api/dashboard/realtime /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 datetime import datetime, timedelta, date, time
from extensions import db, logger_handler 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 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__) 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') @bp.route('/dashboard', endpoint='dashboard')
@login_required @login_required
def dashboard(): def dashboard():
"""Enhanced project-centric dashboard with search filters""" """Enhanced project-centric dashboard with search filters"""
User, QRCode, Project, AttendanceData = _get_models()["User"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["AttendanceData"]
try: try:
user = User.query.get(session['user_id']) user = User.query.get(session['user_id'])
@@ -74,7 +73,7 @@ def dashboard():
logger_handler.log_database_error('dashboard_load', e) logger_handler.log_database_error('dashboard_load', e)
print(f"Error loading dashboard: {e}") print(f"Error loading dashboard: {e}")
flash('Error loading dashboard. Please try again.', 'error') flash('Error loading dashboard. Please try again.', 'error')
return redirect(url_for('login')) return redirect(url_for('auth.login'))
@bp.route('/project/<int:project_id>/qr-codes', endpoint='project_qr_codes') @bp.route('/project/<int:project_id>/qr-codes', endpoint='project_qr_codes')
@login_required @login_required
@@ -83,7 +82,6 @@ def project_qr_codes(project_id):
View all QR codes for a specific project with search filters View all QR codes for a specific project with search filters
Allows filtering by name and status within the project 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: try:
# Get the project # Get the project
project = Project.query.get_or_404(project_id) 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) logger_handler.log_database_error('project_qr_codes_view', e)
print(f"Error loading project QR codes: {e}") print(f"Error loading project QR codes: {e}")
flash('Error loading project QR codes. Please try again.', 'error') flash('Error loading project QR codes. Please try again.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
@bp.route('/dashboard/search', methods=['GET'], endpoint='search_qr_codes') @bp.route('/dashboard/search', methods=['GET'], endpoint='search_qr_codes')
@login_required @login_required
def search_qr_codes(): def search_qr_codes():
"""Search QR codes - redirect to dashboard with filters""" """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_name = request.args.get('search_name', '').strip()
search_status = request.args.get('search_status', '').strip() search_status = request.args.get('search_status', '').strip()
@@ -148,13 +145,12 @@ def search_qr_codes():
) )
# Redirect to dashboard with search parameters # 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') @bp.route('/api/dashboard/stats', endpoint='dashboard_stats_api')
@login_required @login_required
def dashboard_stats_api(): def dashboard_stats_api():
"""API endpoint for dashboard statistics""" """API endpoint for dashboard statistics"""
User, QRCode, Project, AttendanceData = _get_models()["User"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["AttendanceData"]
try: try:
# Get current stats # Get current stats
total_qr_codes = QRCode.query.filter_by(active_status=True).count() total_qr_codes = QRCode.query.filter_by(active_status=True).count()
@@ -213,7 +209,6 @@ def dashboard_stats_api():
@login_required @login_required
def dashboard_realtime_api(): def dashboard_realtime_api():
"""API endpoint for real-time dashboard data""" """API endpoint for real-time dashboard data"""
User, QRCode, Project, AttendanceData = _get_models()["User"], _get_models()["QRCode"], _get_models()["Project"], _get_models()["AttendanceData"]
try: try:
# Get recent activity (last 10 check-ins) # Get recent activity (last 10 check-ins)
recent_activity = db.session.query( recent_activity = db.session.query(
+15 -22
View File
@@ -6,12 +6,17 @@ Employee CRUD and search routes.
Routes: /employees, /employees/create, /employees/<id>/edit, Routes: /employees, /employees/create, /employees/<id>/edit,
/employees/<id>/delete, /api/employees/search, /employees/<id> /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 datetime import datetime, date
from extensions import db, logger_handler 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 logger_handler import log_user_activity, log_database_operations
from utils.helpers import (url_for, from utils.helpers import (
admin_required, admin_required,
has_admin_privileges, has_admin_privileges,
has_staff_level_access, has_staff_level_access,
@@ -20,17 +25,12 @@ from utils.helpers import (url_for,
bp = Blueprint('employees', __name__) 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') @bp.route('/employees', endpoint='employees')
@login_required @login_required
def employees(): def employees():
"""Display employee management page with search and pagination""" """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: try:
# Log user accessing employee management # Log user accessing employee management
try: try:
@@ -87,14 +87,13 @@ def employees():
except Exception as e: except Exception as e:
logger_handler.log_database_error('employee_list', e) logger_handler.log_database_error('employee_list', e)
flash('Error loading employee list. Please try again.', 'error') 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') @bp.route('/employees/create', methods=['GET', 'POST'], endpoint='create_employee')
@login_required @login_required
@log_database_operations('employee_creation') @log_database_operations('employee_creation')
def create_employee(): def create_employee():
"""Create new employee (Admin only)""" """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': if request.method == 'POST':
try: try:
# Get form data # Get form data
@@ -147,7 +146,7 @@ def create_employee():
print(f"⚠️ Logging error (non-critical): {log_error}") print(f"⚠️ Logging error (non-critical): {log_error}")
flash(f'Employee "{first_name} {last_name}" (ID: {employee_id}) created successfully.', 'success') 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: except Exception as e:
db.session.rollback() db.session.rollback()
@@ -165,7 +164,6 @@ def create_employee():
@log_database_operations('employee_update') @log_database_operations('employee_update')
def edit_employee(employee_index): def edit_employee(employee_index):
"""Edit existing employee (Admin only)""" """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: try:
# Get employee by index (primary key) # Get employee by index (primary key)
employee = Employee.query.get_or_404(employee_index) 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}") print(f"⚠️ Logging error (non-critical): {log_error}")
flash(f'Employee "{first_name} {last_name}" updated successfully.', 'success') 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 # GET request - load the form with projects
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
@@ -237,14 +235,13 @@ def edit_employee(employee_index):
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('employee_update', e) logger_handler.log_database_error('employee_update', e)
flash('Error updating employee. Please try again.', 'error') 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') @bp.route('/employees/<int:employee_index>/delete', methods=['POST'], endpoint='delete_employee')
@login_required @login_required
@log_database_operations('employee_deletion') @log_database_operations('employee_deletion')
def delete_employee(employee_index): def delete_employee(employee_index):
"""Delete employee (Admin only) - Enhanced with better logging""" """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: try:
print(f"🗑️ DELETE REQUEST: Employee index {employee_index}") print(f"🗑️ DELETE REQUEST: Employee index {employee_index}")
print(f"📋 Request method: {request.method}") print(f"📋 Request method: {request.method}")
@@ -265,7 +262,6 @@ def delete_employee(employee_index):
} }
# Check if employee has attendance records # Check if employee has attendance records
from models.attendance import AttendanceData
attendance_count = AttendanceData.query.filter_by(employee_id=str(employee.id)).count() attendance_count = AttendanceData.query.filter_by(employee_id=str(employee.id)).count()
print(f"📊 Attendance records found: {attendance_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.' 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}") print(f"❌ DELETION BLOCKED: {error_msg}")
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('employees')) return redirect(url_for('employees.employees'))
# Proceed with deletion # Proceed with deletion
print(f"🗑️ Proceeding with deletion of employee: {employee_data['firstName']} {employee_data['lastName']}") 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') flash(success_msg, 'success')
print(f"✅ SUCCESS: {success_msg}") print(f"✅ SUCCESS: {success_msg}")
return redirect(url_for('employees')) return redirect(url_for('employees.employees'))
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
@@ -302,13 +298,12 @@ def delete_employee(employee_index):
print(f"❌ ERROR in delete_employee: {e}") print(f"❌ ERROR in delete_employee: {e}")
print(f"❌ Exception type: {type(e)}") print(f"❌ Exception type: {type(e)}")
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('employees')) return redirect(url_for('employees.employees'))
@bp.route('/api/employees/search', endpoint='api_employees_search') @bp.route('/api/employees/search', endpoint='api_employees_search')
@login_required @login_required
def api_employees_search(): def api_employees_search():
"""API endpoint for employee search (for AJAX)""" """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: try:
search = request.args.get('q', '').strip() search = request.args.get('q', '').strip()
limit = request.args.get('limit', 10, type=int) limit = request.args.get('limit', 10, type=int)
@@ -332,13 +327,11 @@ def api_employees_search():
@login_required @login_required
def employee_detail(employee_index): def employee_detail(employee_index):
"""View employee details with attendance summary""" """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: try:
# Get employee by index (primary key) # Get employee by index (primary key)
employee = Employee.query.outerjoin(Project, Employee.contractId == Project.id).filter(Employee.index == employee_index).first_or_404() employee = Employee.query.outerjoin(Project, Employee.contractId == Project.id).filter(Employee.index == employee_index).first_or_404()
# Get attendance statistics for this employee # Get attendance statistics for this employee
from models.attendance import AttendanceData
# Total attendance records # Total attendance records
total_attendance = AttendanceData.query.filter_by(employee_id=str(employee.id)).count() 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: except Exception as e:
logger_handler.log_database_error('employee_detail', e) logger_handler.log_database_error('employee_detail', e)
flash('Error loading employee details. Please try again.', 'error') flash('Error loading employee details. Please try again.', 'error')
return redirect(url_for('employees')) return redirect(url_for('employees.employees'))
+15 -20
View File
@@ -6,14 +6,19 @@ Payroll dashboard and Excel export routes.
Routes: /payroll, /payroll/export-excel, /api/working-hours/calculate, Routes: /payroll, /payroll/export-excel, /api/working-hours/calculate,
/api/employee/<id>/miss-punch-details /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 from datetime import datetime, date, timedelta, time
import io, json, traceback, os import io, json, traceback, os
from extensions import db, logger_handler 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 sqlalchemy import text
from logger_handler import log_user_activity, log_database_operations from logger_handler import log_user_activity, log_database_operations
from utils.helpers import (url_for, from utils.helpers import (
admin_required, admin_required,
has_admin_privileges, has_admin_privileges,
has_staff_level_access, has_staff_level_access,
@@ -25,24 +30,19 @@ from enhanced_payroll_excel_exporter import EnhancedPayrollExcelExporter
bp = Blueprint('payroll', __name__) 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') @bp.route('/payroll', endpoint='payroll_dashboard')
@login_required @login_required
def payroll_dashboard(): def payroll_dashboard():
"""Payroll dashboard for calculating and exporting working hours""" """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: try:
# Check if user has payroll access # Check if user has payroll access
user_role = session.get('role') user_role = session.get('role')
if user_role not in ['admin', 'payroll', 'accounting']: 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") 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') 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") print("📊 Loading payroll dashboard")
@@ -176,21 +176,20 @@ def payroll_dashboard():
) )
flash('Error loading payroll dashboard. Please check the server logs.', 'error') 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') @bp.route('/payroll/export-excel', methods=['POST'], endpoint='export_payroll_excel')
@login_required @login_required
@log_database_operations('payroll_excel_export') @log_database_operations('payroll_excel_export')
def export_payroll_excel(): def export_payroll_excel():
"""Export payroll report to Excel with working hours calculations including SP/PW support""" """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: try:
# Check permissions # Check permissions
user_role = session.get('role') user_role = session.get('role')
if user_role not in ['admin', 'payroll', 'accounting']: 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") 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') 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") print("📊 Payroll Excel export started")
@@ -202,14 +201,14 @@ def export_payroll_excel():
if not date_from or not date_to: if not date_from or not date_to:
flash('Please provide both start and end dates for the export.', 'error') 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: try:
start_date = datetime.strptime(date_from, '%Y-%m-%d') start_date = datetime.strptime(date_from, '%Y-%m-%d')
end_date = datetime.strptime(date_to, '%Y-%m-%d') end_date = datetime.strptime(date_to, '%Y-%m-%d')
except ValueError: except ValueError:
flash('Invalid date format. Please use YYYY-MM-DD format.', 'error') 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 # Get attendance records with project filter and QR code data
query = db.session.query(AttendanceData, QRCode).join(QRCode, AttendanceData.qr_code_id == QRCode.id) 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: if not attendance_records:
flash('No attendance records found for the selected date range and project.', 'warning') flash('No attendance records found for the selected date range and project.', 'warning')
return redirect(url_for('payroll_dashboard')) return redirect(url_for('payroll.payroll_dashboard'))
print(f"📊 Exporting {len(attendance_records)} attendance records to Excel") print(f"📊 Exporting {len(attendance_records)} attendance records to Excel")
@@ -404,7 +403,7 @@ def export_payroll_excel():
) )
else: else:
flash('Error generating payroll Excel file.', 'error') flash('Error generating payroll Excel file.', 'error')
return redirect(url_for('payroll_dashboard')) return redirect(url_for('payroll.payroll_dashboard'))
except Exception as e: except Exception as e:
print(f"❌ Error in export_payroll_excel route: {e}") 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') 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') @bp.route('/api/working-hours/calculate', methods=['POST'], endpoint='calculate_working_hours_api')
@login_required @login_required
@log_database_operations('working_hours_api_calculation') @log_database_operations('working_hours_api_calculation')
def calculate_working_hours_api(): def calculate_working_hours_api():
"""API endpoint for calculating working hours""" """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: try:
# Check permissions # Check permissions
user_role = session.get('role') user_role = session.get('role')
@@ -503,7 +501,6 @@ def calculate_working_hours_api():
@log_database_operations('miss_punch_details_api') @log_database_operations('miss_punch_details_api')
def get_miss_punch_details(employee_id): def get_miss_punch_details(employee_id):
"""API endpoint to get detailed miss punch information for an employee""" """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: try:
# Check permissions # Check permissions
user_role = session.get('role') user_role = session.get('role')
@@ -667,7 +664,6 @@ def get_miss_punch_details(employee_id):
def get_employee_name(employee_id): def get_employee_name(employee_id):
"""Helper function to get employee full name by ID""" """Helper function to get employee full name by ID"""
Employee = _get_models()["Employee"]
try: try:
result = db.session.execute(text(""" result = db.session.execute(text("""
SELECT CONCAT(firstName, ' ', lastName) as full_name 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): def get_qr_code_checkin_count(qr_code_id):
"""Helper function to get total check-ins count for a QR code""" """Helper function to get total check-ins count for a QR code"""
AttendanceData = _get_models()["AttendanceData"]
try: try:
count = AttendanceData.query.filter_by(qr_code_id=qr_code_id).count() 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}") logger_handler.logger.info(f"QR Code {qr_code_id} total check-ins: {count}")
+9 -16
View File
@@ -6,41 +6,37 @@ Project CRUD and related API routes.
Routes: /projects, /projects/create, /projects/<id>/edit, Routes: /projects, /projects/create, /projects/<id>/edit,
/projects/<id>/toggle, /api/projects/active /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 import json
from datetime import datetime from datetime import datetime
from extensions import db, logger_handler 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 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__) 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') @bp.route('/projects', endpoint='projects')
@admin_required @admin_required
def projects(): def projects():
"""Display all projects""" """Display all projects"""
Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"]
try: try:
projects = Project.query.order_by(Project.created_date.desc()).all() projects = Project.query.order_by(Project.created_date.desc()).all()
return render_template('projects.html', projects=projects) return render_template('projects.html', projects=projects)
except Exception as e: except Exception as e:
logger_handler.log_database_error('projects_list', e) logger_handler.log_database_error('projects_list', e)
flash('Error loading projects list.', 'error') 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') @bp.route('/projects/create', methods=['GET', 'POST'], endpoint='create_project')
@admin_required @admin_required
@log_database_operations('project_creation') @log_database_operations('project_creation')
def create_project(): def create_project():
"""Create new project""" """Create new project"""
Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"]
if request.method == 'POST': if request.method == 'POST':
try: try:
name = request.form['name'] name = request.form['name']
@@ -65,7 +61,7 @@ def create_project():
logger_handler.logger.info(f"User {session['username']} created new project: {name}") logger_handler.logger.info(f"User {session['username']} created new project: {name}")
flash(f'Project "{name}" created successfully.', 'success') flash(f'Project "{name}" created successfully.', 'success')
return redirect(url_for('projects')) return redirect(url_for('projects.projects'))
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
@@ -79,7 +75,6 @@ def create_project():
@log_database_operations('project_edit') @log_database_operations('project_edit')
def edit_project(project_id): def edit_project(project_id):
"""Edit existing project""" """Edit existing project"""
Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"]
try: try:
project = Project.query.get_or_404(project_id) 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)}") logger_handler.logger.info(f"User {session['username']} updated project {project_id}: {json.dumps(changes)}")
flash(f'Project "{project.name}" updated successfully.', 'success') 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) return render_template('edit_project.html', project=project)
@@ -111,14 +106,13 @@ def edit_project(project_id):
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('project_edit', e) logger_handler.log_database_error('project_edit', e)
flash('Project update failed. Please try again.', 'error') 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') @bp.route('/projects/<int:project_id>/toggle', methods=['POST'], endpoint='toggle_project')
@admin_required @admin_required
@log_database_operations('project_toggle') @log_database_operations('project_toggle')
def toggle_project(project_id): def toggle_project(project_id):
"""Toggle project active status""" """Toggle project active status"""
Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"]
try: try:
project = Project.query.get_or_404(project_id) project = Project.query.get_or_404(project_id)
old_status = project.active_status old_status = project.active_status
@@ -137,14 +131,13 @@ def toggle_project(project_id):
logger_handler.log_database_error('project_toggle', e) logger_handler.log_database_error('project_toggle', e)
flash('Failed to update project status.', 'error') flash('Failed to update project status.', 'error')
return redirect(url_for('projects')) return redirect(url_for('projects.projects'))
# API ENDPOINTS FOR DROPDOWN FUNCTIONALITY # API ENDPOINTS FOR DROPDOWN FUNCTIONALITY
@bp.route('/api/projects/active', endpoint='api_active_projects') @bp.route('/api/projects/active', endpoint='api_active_projects')
@login_required @login_required
def api_active_projects(): def api_active_projects():
"""Get active projects for dropdown""" """Get active projects for dropdown"""
Project, QRCode, User = _get_models()["Project"], _get_models()["QRCode"], _get_models()["User"]
try: try:
projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all()
+17 -29
View File
@@ -6,15 +6,19 @@ QR code management and destination handler routes.
Routes: /qr-codes/create, /qr-codes/bulk-import, /qr-codes/<id>/*, Routes: /qr-codes/create, /qr-codes/bulk-import, /qr-codes/<id>/*,
/qr/<string:qr_url> /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 from datetime import datetime, date, timedelta, time
import io, os, base64, re, uuid, json, traceback import io, os, base64, re, uuid, json, traceback
from extensions import db, logger_handler 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 werkzeug.utils import secure_filename
from logger_handler import log_user_activity, log_database_operations from logger_handler import log_user_activity, log_database_operations
from utils.helpers import ( from utils.helpers import (
url_for,
admin_required, admin_required,
detect_device_info, detect_device_info,
generate_default_qr_code, generate_default_qr_code,
@@ -37,10 +41,6 @@ import openpyxl
bp = Blueprint('qr_codes', __name__) 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') @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') @log_database_operations('qr_code_creation')
def create_qr_code(): def create_qr_code():
"""Enhanced create QR code with customization options""" """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': if request.method == 'POST':
try: try:
# Existing form data # Existing form data
@@ -187,7 +186,7 @@ def create_qr_code():
style_info = f" with custom styling (Fill: {fill_color}, Background: {back_color})" 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') 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: except Exception as e:
db.session.rollback() db.session.rollback()
@@ -206,7 +205,6 @@ def create_qr_code():
@log_database_operations('qr_code_bulk_import') @log_database_operations('qr_code_bulk_import')
def import_bulk_qr_codes(): def import_bulk_qr_codes():
"""Bulk import QR codes from Excel file""" """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': if request.method == 'GET':
return render_template('bulk_qr_import.html') return render_template('bulk_qr_import.html')
@@ -217,7 +215,7 @@ def import_bulk_qr_codes():
if proceed_import: if proceed_import:
if 'pending_qr_import_file' not in session or 'pending_qr_import_filename' not in session: 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') 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'] temp_path = session['pending_qr_import_file']
filename = session['pending_qr_import_filename'] 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') flash('Temporary file not found. Please upload the file again.', 'error')
session.pop('pending_qr_import_file', None) session.pop('pending_qr_import_file', None)
session.pop('pending_qr_import_filename', 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: else:
if 'file' not in request.files: if 'file' not in request.files:
flash('No file uploaded.', 'error') flash('No file uploaded.', 'error')
@@ -313,14 +311,13 @@ def import_bulk_qr_codes():
except Exception as e: except Exception as e:
logger_handler.log_database_error('qr_code_bulk_import', e) logger_handler.log_database_error('qr_code_bulk_import', e)
flash(f'Import failed: {str(e)}', 'error') 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') @bp.route('/qr-codes/bulk-import/template', endpoint='download_qr_import_template')
@login_required @login_required
def download_qr_import_template(): def download_qr_import_template():
"""Download Excel template for bulk QR code import""" """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: try:
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill from openpyxl.styles import Font, Alignment, PatternFill
@@ -380,14 +377,13 @@ def download_qr_import_template():
except Exception as e: except Exception as e:
logger_handler.log_flask_error('qr_import_template_download', str(e)) logger_handler.log_flask_error('qr_import_template_download', str(e))
flash('Error generating template. Please try again.', 'error') 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') @bp.route('/qr-codes/<int:qr_id>/edit', methods=['GET', 'POST'], endpoint='edit_qr_code')
@login_required @login_required
@log_database_operations('qr_code_edit') @log_database_operations('qr_code_edit')
def edit_qr_code(qr_id): def edit_qr_code(qr_id):
"""Enhanced edit QR code with customization support""" """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: try:
qr_code = QRCode.query.get_or_404(qr_id) qr_code = QRCode.query.get_or_404(qr_id)
@@ -501,7 +497,7 @@ def edit_qr_code(qr_id):
# Success message # Success message
flash(f'QR Code "{qr_code.name}" updated successfully!', 'success') 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 # GET request - render edit form
projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all()
@@ -513,14 +509,13 @@ def edit_qr_code(qr_id):
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('qr_code_edit', e) logger_handler.log_database_error('qr_code_edit', e)
flash('QR Code update failed. Please try again.', 'error') 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') @bp.route('/qr-codes/<int:qr_id>/delete', methods=['GET', 'POST'], endpoint='delete_qr_code')
@admin_required @admin_required
@log_database_operations('qr_code_deletion') @log_database_operations('qr_code_deletion')
def delete_qr_code(qr_id): def delete_qr_code(qr_id):
"""Permanently delete QR code (Admin only) - Hard delete - PRESERVING EXACT ROUTE""" """Permanently delete QR code (Admin only) - Hard delete - PRESERVING EXACT ROUTE"""
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: try:
qr_code = QRCode.query.get_or_404(qr_id) qr_code = QRCode.query.get_or_404(qr_id)
print(f"✅ Found QR Code: {qr_code.name}") 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") print(f"✅ DELETE SUCCESS! Removed {before_count - after_count} records")
flash(f'QR code "{qr_name}" has been permanently deleted!', 'success') flash(f'QR code "{qr_name}" has been permanently deleted!', 'success')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
# GET request - show confirmation page # GET request - show confirmation page
print("📄 Showing confirmation page") print("📄 Showing confirmation page")
@@ -567,12 +562,11 @@ def delete_qr_code(qr_id):
print(f"❌ Exception type: {type(e)}") print(f"❌ Exception type: {type(e)}")
print(f"❌ Traceback: {traceback.format_exc()}") print(f"❌ Traceback: {traceback.format_exc()}")
flash('Error deleting QR code. Please try again.', 'error') flash('Error deleting QR code. Please try again.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
@bp.route('/qr/<string:qr_url>', endpoint='qr_destination') @bp.route('/qr/<string:qr_url>', endpoint='qr_destination')
def qr_destination(qr_url): def qr_destination(qr_url):
"""QR code destination page where staff check in - PRESERVING EXACT ROUTE""" """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: try:
# Find QR code by URL # Find QR code by URL
qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first() qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first()
@@ -585,7 +579,7 @@ def qr_destination(qr_url):
severity="MEDIUM" severity="MEDIUM"
) )
flash('QR code not found or inactive.', 'error') flash('QR code not found or inactive.', 'error')
return redirect(url_for('index')) return redirect(url_for('auth.index'))
# Log QR code access # Log QR code access
logger_handler.log_qr_code_accessed( logger_handler.log_qr_code_accessed(
@@ -599,7 +593,7 @@ def qr_destination(qr_url):
except Exception as e: except Exception as e:
logger_handler.log_database_error('qr_code_scan', e) logger_handler.log_database_error('qr_code_scan', e)
flash('Error processing QR code scan.', 'error') 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') @bp.route('/qr/<string:qr_url>/checkin', methods=['POST'], endpoint='qr_checkin')
def qr_checkin(qr_url): def qr_checkin(qr_url):
@@ -608,7 +602,6 @@ def qr_checkin(qr_url):
Allows multiple check-ins with minimum interval between them Allows multiple check-ins with minimum interval between them
PRESERVES coordinate-to-address conversion functionality 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: try:
print(f"\n🚀 STARTING ENHANCED CHECK-IN PROCESS") print(f"\n🚀 STARTING ENHANCED CHECK-IN PROCESS")
print(f" QR URL: {qr_url}") print(f" QR URL: {qr_url}")
@@ -913,7 +906,6 @@ def qr_checkin(qr_url):
@login_required @login_required
def toggle_qr_status(qr_id): def toggle_qr_status(qr_id):
"""Toggle QR code active/inactive status""" """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: try:
qr_code = QRCode.query.get_or_404(qr_id) qr_code = QRCode.query.get_or_404(qr_id)
@@ -943,7 +935,6 @@ def toggle_qr_status(qr_id):
@login_required @login_required
def copy_qr_url(qr_id): def copy_qr_url(qr_id):
"""Log QR code URL copy action""" """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: try:
qr_code = QRCode.query.get_or_404(qr_id) qr_code = QRCode.query.get_or_404(qr_id)
@@ -967,7 +958,6 @@ def copy_qr_url(qr_id):
@login_required @login_required
def open_qr_link(qr_id): def open_qr_link(qr_id):
"""Log QR code link open action""" """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: try:
qr_code = QRCode.query.get_or_404(qr_id) qr_code = QRCode.query.get_or_404(qr_id)
@@ -991,7 +981,6 @@ def open_qr_link(qr_id):
@login_required @login_required
def activate_qr_code(qr_id): def activate_qr_code(qr_id):
"""Activate a QR code""" """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: try:
qr_code = QRCode.query.get_or_404(qr_id) qr_code = QRCode.query.get_or_404(qr_id)
qr_code.active_status = True qr_code.active_status = True
@@ -1017,7 +1006,6 @@ def activate_qr_code(qr_id):
@login_required @login_required
def deactivate_qr_code(qr_id): def deactivate_qr_code(qr_id):
"""Deactivate a QR code""" """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: try:
qr_code = QRCode.query.get_or_404(qr_id) qr_code = QRCode.query.get_or_404(qr_id)
qr_code.active_status = False qr_code.active_status = False
+7 -10
View File
@@ -5,28 +5,26 @@ Statistics dashboard and export routes.
Routes: /statistics, /api/statistics/export 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 from datetime import datetime, date, timedelta
import io, json, traceback import io, json, traceback
from extensions import db, logger_handler 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 sqlalchemy import text
from logger_handler import log_user_activity, log_database_operations 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__) 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') @bp.route('/statistics', endpoint='qr_statistics')
@login_required @login_required
def qr_statistics(): def qr_statistics():
"""QR Code Statistics Dashboard with comprehensive analytics""" """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: try:
# Log statistics page access # Log statistics page access
logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed QR code statistics dashboard") 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()}") print(f"❌ Traceback: {traceback.format_exc()}")
flash('Error loading statistics. Please try again.', 'error') flash('Error loading statistics. Please try again.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
@bp.route('/api/statistics/export', endpoint='export_statistics') @bp.route('/api/statistics/export', endpoint='export_statistics')
@login_required @login_required
def export_statistics(): def export_statistics():
"""Export statistics data to CSV/Excel""" """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: try:
# Check permissions # Check permissions
if session.get('role') not in ['admin', 'payroll', 'accounting']: if session.get('role') not in ['admin', 'payroll', 'accounting']:
@@ -310,6 +307,6 @@ def export_statistics():
print(f"❌ Traceback: {traceback.format_exc()}") print(f"❌ Traceback: {traceback.format_exc()}")
flash('Error loading statistics. Please try again.', 'error') flash('Error loading statistics. Please try again.', 'error')
return redirect(url_for('dashboard')) return redirect(url_for('dashboard.dashboard'))
# EMPLOYEE MANAGEMENT ROUTES # EMPLOYEE MANAGEMENT ROUTES
+28 -54
View File
@@ -9,17 +9,21 @@ Routes: /time-attendance, /time-attendance/import/*,
/time-attendance/record/<id>, /time-attendance/delete/<id>, /time-attendance/record/<id>, /time-attendance/delete/<id>,
/api/time-attendance/* /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 from datetime import datetime, date, timedelta, time
import io, os, json, re, uuid, traceback import io, os, json, re, uuid, traceback
import time as _time import time as _time
from extensions import db, logger_handler 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 sqlalchemy import text
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from logger_handler import log_user_activity, log_database_operations from logger_handler import log_user_activity, log_database_operations
from utils.helpers import ( from utils.helpers import (
url_for,
admin_required, admin_required,
has_admin_privileges, has_admin_privileges,
has_staff_level_access, has_staff_level_access,
@@ -35,10 +39,6 @@ import openpyxl.cell.cell
bp = Blueprint('time_attendance', __name__) 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') @bp.route('/time-attendance', endpoint='time_attendance_dashboard')
@@ -46,7 +46,6 @@ def _get_models():
@log_user_activity('time_attendance_view') @log_user_activity('time_attendance_view')
def time_attendance_dashboard(): def time_attendance_dashboard():
"""Display time attendance dashboard with table layout""" """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: try:
# Initialize default values # Initialize default values
total_records = 0 total_records = 0
@@ -59,7 +58,6 @@ def time_attendance_dashboard():
# Try to get data from TimeAttendance model if it exists # Try to get data from TimeAttendance model if it exists
try: try:
from models.time_attendance import TimeAttendance
# Get summary statistics # Get summary statistics
total_records = TimeAttendance.query.count() total_records = TimeAttendance.query.count()
@@ -114,14 +112,13 @@ def time_attendance_dashboard():
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error in time attendance dashboard: {e}") logger_handler.logger.error(f"Error in time attendance dashboard: {e}")
flash('Error loading time attendance dashboard.', 'error') 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') @bp.route('/time-attendance/import', methods=['GET', 'POST'], endpoint='import_time_attendance')
@login_required @login_required
@log_database_operations('time_attendance_import') @log_database_operations('time_attendance_import')
def import_time_attendance(): def import_time_attendance():
"""Enhanced import with duplicate review""" """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': if request.method == 'GET':
# Load active projects for dropdown # Load active projects for dropdown
projects = Project.query.filter_by(active_status=True).order_by(Project.name).all() projects = Project.query.filter_by(active_status=True).order_by(Project.name).all()
@@ -141,7 +138,7 @@ def import_time_attendance():
# Retrieve file from session # Retrieve file from session
if 'pending_import_file' not in session or 'pending_import_filename' not in session: if 'pending_import_file' not in session or 'pending_import_filename' not in session:
flash('Session expired. Please upload the file again.', 'error') 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'] temp_path = session['pending_import_file']
filename = session['pending_import_filename'] filename = session['pending_import_filename']
@@ -151,7 +148,7 @@ def import_time_attendance():
flash('Temporary file not found. Please upload the file again.', 'error') flash('Temporary file not found. Please upload the file again.', 'error')
session.pop('pending_import_file', None) session.pop('pending_import_file', None)
session.pop('pending_import_filename', 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"✅ Retrieved file from session: {filename}")
print(f"✅ Temp path exists: {os.path.exists(temp_path)}") 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 imported: {all_results['total_imported']}")
print(f" Total duplicates: {all_results['total_duplicates']}") 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 # Check if this is coming from duplicate review
force_import_hashes = request.form.getlist('force_import_hashes[]') force_import_hashes = request.form.getlist('force_import_hashes[]')
@@ -528,7 +525,6 @@ def import_time_attendance():
@login_required @login_required
def analyze_import_duplicates(): def analyze_import_duplicates():
"""AJAX endpoint to analyze file for 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: try:
if 'file' not in request.files: if 'file' not in request.files:
return jsonify({'success': False, 'message': 'No file provided'}), 400 return jsonify({'success': False, 'message': 'No file provided'}), 400
@@ -594,7 +590,6 @@ def analyze_import_duplicates():
@login_required @login_required
def analyze_import_invalid(): def analyze_import_invalid():
"""AJAX endpoint to analyze file for invalid rows""" """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: try:
if 'file' not in request.files: if 'file' not in request.files:
return jsonify({'success': False, 'message': 'No file provided'}), 400 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 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. 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: try:
if 'files' not in request.files: if 'files' not in request.files:
return jsonify({'success': False, 'error': 'No file uploaded.'}), 400 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 the browser. Works across multiple gunicorn workers because all state is
stored on disk (no in-memory job store). 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 # Capture upload_dir HERE in the request context — current_app is NOT
# available inside the background thread (_run) or after context teardown. # available inside the background thread (_run) or after context teardown.
upload_dir = current_app.config.get('UPLOAD_FOLDER', '/tmp') upload_dir = current_app.config.get('UPLOAD_FOLDER', '/tmp')
@@ -896,7 +889,6 @@ def stream_import_progress(job_id):
@login_required @login_required
def cancel_pending_import(): def cancel_pending_import():
"""Cancel pending import and cleanup temp file""" """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: try:
if 'pending_import_file' in session: if 'pending_import_file' in session:
temp_path = session['pending_import_file'] temp_path = session['pending_import_file']
@@ -911,7 +903,7 @@ def cancel_pending_import():
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error cancelling import: {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 @login_required
def validate_import_file(): def validate_import_file():
"""AJAX endpoint to validate Excel file before import""" """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: try:
if 'file' not in request.files: if 'file' not in request.files:
return jsonify({'success': False, 'message': 'No file provided'}), 400 return jsonify({'success': False, 'message': 'No file provided'}), 400
@@ -967,14 +958,13 @@ def validate_import_file():
@log_user_activity('view_import_batch') @log_user_activity('view_import_batch')
def view_import_batch(batch_id): def view_import_batch(batch_id):
"""View details of a specific import batch""" """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: try:
import_service = TimeAttendanceImportService(db, logger_handler) import_service = TimeAttendanceImportService(db, logger_handler)
batch_summary = import_service.get_import_summary(batch_id) batch_summary = import_service.get_import_summary(batch_id)
if not batch_summary: if not batch_summary:
flash('Import batch not found.', 'error') 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', return render_template('time_attendance_batch_detail.html',
batch_summary=batch_summary) batch_summary=batch_summary)
@@ -982,7 +972,7 @@ def view_import_batch(batch_id):
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error viewing batch {batch_id}: {e}") logger_handler.logger.error(f"Error viewing batch {batch_id}: {e}")
flash('Error loading batch details.', 'error') 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') @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') @log_database_operations('delete_import_batch')
def delete_import_batch(batch_id): def delete_import_batch(batch_id):
"""Delete an entire import batch""" """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: try:
import_service = TimeAttendanceImportService(db, logger_handler) import_service = TimeAttendanceImportService(db, logger_handler)
result = import_service.delete_import_batch(batch_id, deleted_by=session['user_id']) result = import_service.delete_import_batch(batch_id, deleted_by=session['user_id'])
@@ -1004,19 +993,18 @@ def delete_import_batch(batch_id):
else: else:
flash(result['message'], 'error') flash(result['message'], 'error')
return redirect(url_for('time_attendance_dashboard')) return redirect(url_for('time_attendance.time_attendance_dashboard'))
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error deleting batch {batch_id}: {e}") logger_handler.logger.error(f"Error deleting batch {batch_id}: {e}")
flash('Error deleting import batch.', 'error') 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') @bp.route('/time-attendance/import/download-template', endpoint='download_import_template')
@login_required @login_required
def download_import_template(): def download_import_template():
"""Download Excel template for time attendance import""" """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: try:
import io import io
from openpyxl import Workbook from openpyxl import Workbook
@@ -1119,14 +1107,13 @@ def download_import_template():
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error generating template: {e}") logger_handler.logger.error(f"Error generating template: {e}")
flash('Error generating template file.', 'error') 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') @bp.route('/time-attendance/export', endpoint='export_time_attendance')
@login_required @login_required
@log_user_activity('time_attendance_export') @log_user_activity('time_attendance_export')
def export_time_attendance(): def export_time_attendance():
"""Export time attendance records to CSV or Excel""" """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: try:
export_format = request.args.get('format', 'excel').lower() export_format = request.args.get('format', 'excel').lower()
@@ -1139,7 +1126,6 @@ def export_time_attendance():
project_filter = request.args.get('project_id') project_filter = request.args.get('project_id')
# Build query with same filters as the view # Build query with same filters as the view
from models.time_attendance import TimeAttendance
query = TimeAttendance.query query = TimeAttendance.query
# Apply filters — employee_id supports comma-separated multi-employee values # 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) query = query.filter(TimeAttendance.attendance_date >= start_date_obj)
except ValueError: except ValueError:
flash('Invalid start date format.', 'error') 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: if end_date:
try: try:
@@ -1183,7 +1169,7 @@ def export_time_attendance():
query = query.filter(TimeAttendance.attendance_date <= end_date_obj + timedelta(days=1)) query = query.filter(TimeAttendance.attendance_date <= end_date_obj + timedelta(days=1))
except ValueError: except ValueError:
flash('Invalid end date format.', 'error') 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: if import_batch:
query = query.filter(TimeAttendance.import_batch_id == import_batch) query = query.filter(TimeAttendance.import_batch_id == import_batch)
@@ -1199,13 +1185,12 @@ def export_time_attendance():
if not records: if not records:
flash('No records found to export.', 'warning') 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 # Get project name if project filter exists
project_name_for_filename = '' project_name_for_filename = ''
if project_filter: if project_filter:
try: try:
from models.project import Project
project = Project.query.get(int(project_filter)) project = Project.query.get(int(project_filter))
if project: if project:
# Replace spaces and special characters with underscores # Replace spaces and special characters with underscores
@@ -1261,7 +1246,7 @@ def export_time_attendance():
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error exporting time attendance records: {e}") logger_handler.logger.error(f"Error exporting time attendance records: {e}")
flash('Error generating export file. Please try again.', 'error') 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): 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): def export_time_attendance_excel(records, project_name_for_filename, date_range_str, filter_str, start_date_filter=None, end_date_filter=None):
"""Generate Excel export with template format matching the provided template""" """Generate Excel export with template format matching the provided template"""
Employee, Project, QRCode, TimeAttendance = _get_models()["Employee"], _get_models()["Project"], _get_models()["QRCode"], _get_models()["TimeAttendance"]
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
from openpyxl.utils import get_column_letter 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') @log_user_activity('time_attendance_excel_export')
def excel_export_time_attendance(): def excel_export_time_attendance():
"""Excel export with current page filters""" """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 # 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') @bp.route('/time-attendance/export-by-building', endpoint='export_time_attendance_by_building')
@login_required @login_required
@log_user_activity('time_attendance_export_by_building') @log_user_activity('time_attendance_export_by_building')
def export_time_attendance_by_building(): def export_time_attendance_by_building():
"""Export time attendance records grouped by building/location to Excel""" """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: try:
# Get filter parameters (same as records page) # Get filter parameters (same as records page)
employee_filter = request.args.get('employee_id') employee_filter = request.args.get('employee_id')
@@ -2524,7 +2506,6 @@ def export_time_attendance_by_building():
project_filter = request.args.get('project_id') project_filter = request.args.get('project_id')
# Build query with same filters as the view # Build query with same filters as the view
from models.time_attendance import TimeAttendance
query = TimeAttendance.query query = TimeAttendance.query
# Apply filters — employee_id supports comma-separated multi-employee values # 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) query = query.filter(TimeAttendance.attendance_date >= start_date_obj)
except ValueError: except ValueError:
flash('Invalid start date format.', 'error') 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: if end_date:
try: try:
@@ -2567,7 +2548,7 @@ def export_time_attendance_by_building():
query = query.filter(TimeAttendance.attendance_date <= end_date_obj + timedelta(days=1)) query = query.filter(TimeAttendance.attendance_date <= end_date_obj + timedelta(days=1))
except ValueError: except ValueError:
flash('Invalid end date format.', 'error') 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: if import_batch:
query = query.filter(TimeAttendance.import_batch_id == import_batch) query = query.filter(TimeAttendance.import_batch_id == import_batch)
@@ -2584,13 +2565,12 @@ def export_time_attendance_by_building():
if not records: if not records:
flash('No records found to export.', 'warning') 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 # Get project name if project filter exists
project_name_for_filename = '' project_name_for_filename = ''
if project_filter: if project_filter:
try: try:
from models.project import Project
project = Project.query.get(int(project_filter)) project = Project.query.get(int(project_filter))
if project: if project:
# Replace spaces and special characters with underscores # Replace spaces and special characters with underscores
@@ -2636,11 +2616,10 @@ def export_time_attendance_by_building():
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error exporting time attendance records by building: {e}") logger_handler.logger.error(f"Error exporting time attendance records by building: {e}")
flash('Error generating export file. Please try again.', 'error') 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): 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""" """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 import Workbook
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment from openpyxl.styles import Font, PatternFill, Border, Side, Alignment
from openpyxl.utils import get_column_letter 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') @log_user_activity('time_attendance_records_view')
def time_attendance_records(): def time_attendance_records():
"""Display time attendance records with filtering options""" """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: try:
# Get filter parameters # Get filter parameters
employee_filter = request.args.get('employee_id', '') employee_filter = request.args.get('employee_id', '')
@@ -3539,14 +3517,13 @@ def time_attendance_records():
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error displaying time attendance records: {e}") logger_handler.logger.error(f"Error displaying time attendance records: {e}")
flash('Error loading attendance records.', 'error') 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') @bp.route('/time-attendance/record/<int:record_id>', endpoint='time_attendance_record_detail')
@login_required @login_required
@log_user_activity('time_attendance_record_detail') @log_user_activity('time_attendance_record_detail')
def time_attendance_record_detail(record_id): def time_attendance_record_detail(record_id):
"""Display detailed view of a time attendance record""" """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: try:
record = TimeAttendance.query.get_or_404(record_id) record = TimeAttendance.query.get_or_404(record_id)
return render_template('time_attendance_record_detail.html', record=record) 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: except Exception as e:
logger_handler.logger.error(f"Error viewing time attendance record {record_id}: {e}") logger_handler.logger.error(f"Error viewing time attendance record {record_id}: {e}")
flash('Error loading record details.', 'error') 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') @bp.route('/time-attendance/delete/<int:record_id>', methods=['POST'], endpoint='delete_time_attendance_record')
@admin_required @admin_required
@log_database_operations('time_attendance_delete') @log_database_operations('time_attendance_delete')
def delete_time_attendance_record(record_id): def delete_time_attendance_record(record_id):
"""Delete a time attendance record""" """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: try:
record = TimeAttendance.query.get_or_404(record_id) record = TimeAttendance.query.get_or_404(record_id)
@@ -3605,13 +3581,12 @@ def delete_time_attendance_record(record_id):
filter_params[key] = value filter_params[key] = value
# Redirect back with filters preserved # 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') @bp.route('/api/time-attendance/employee/<employee_id>', endpoint='api_time_attendance_by_employee')
@login_required @login_required
def api_time_attendance_by_employee(employee_id): def api_time_attendance_by_employee(employee_id):
"""API endpoint to get time attendance records for a specific employee""" """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: try:
start_date = request.args.get('start_date') start_date = request.args.get('start_date')
end_date = request.args.get('end_date') end_date = request.args.get('end_date')
@@ -3644,7 +3619,6 @@ def api_time_attendance_by_employee(employee_id):
@login_required @login_required
def api_time_attendance_by_location(location_name): def api_time_attendance_by_location(location_name):
"""API endpoint to get time attendance records for a specific location""" """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: try:
start_date = request.args.get('start_date') start_date = request.args.get('start_date')
end_date = request.args.get('end_date') end_date = request.args.get('end_date')
+41 -57
View File
@@ -6,14 +6,18 @@ User management routes (admin-only operations).
Routes: /users/*, /api/users/stats, /api/locations-by-projects, Routes: /users/*, /api/users/stats, /api/locations-by-projects,
/api/roles/permissions, /api/geocode, /api/reverse-geocode /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 from datetime import datetime, timedelta
import json import json
from extensions import db, logger_handler 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 sqlalchemy import text
from logger_handler import log_user_activity, log_database_operations from logger_handler import log_user_activity, log_database_operations
from utils.helpers import (url_for, from utils.helpers import (
admin_required, admin_required,
generate_qr_code, generate_qr_code,
get_qr_styling, get_qr_styling,
@@ -34,31 +38,25 @@ from werkzeug.security import generate_password_hash
bp = Blueprint('users', __name__) 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') @bp.route('/users', endpoint='users')
@admin_required @admin_required
def users(): def users():
"""Display all users (Admin only)""" """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: try:
users = User.query.order_by(User.created_date.desc()).all() users = User.query.order_by(User.created_date.desc()).all()
return render_template('users.html', users=users) return render_template('users.html', users=users)
except Exception as e: except Exception as e:
logger_handler.log_database_error('users_list', e) logger_handler.log_database_error('users_list', e)
flash('Error loading users list.', 'error') 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') @bp.route('/users/create', methods=['GET', 'POST'], endpoint='create_user')
@admin_required @admin_required
@log_database_operations('user_creation') @log_database_operations('user_creation')
def create_user(): def create_user():
"""Create new user (Admin only) with Project Manager permissions support""" """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': if request.method == 'POST':
try: try:
# Get basic form data # 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}") 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') 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: except KeyError as e:
db.session.rollback() db.session.rollback()
@@ -201,7 +199,7 @@ def create_user():
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error loading create user form: {e}") logger_handler.logger.error(f"Error loading create user form: {e}")
flash('Error loading form. Please try again.', 'error') 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(): def get_all_locations_from_qr_codes():
"""Helper function to get all unique 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 @admin_required
def delete_user(user_id): def delete_user(user_id):
"""Deactivate user (Admin only) - Fixed with proper validation""" """Deactivate user (Admin only) - Fixed with proper validation"""
User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"]
try: try:
user_to_delete = User.query.get(user_id) user_to_delete = User.query.get(user_id)
current_user = User.query.get(session['user_id']) current_user = User.query.get(session['user_id'])
if not user_to_delete: if not user_to_delete:
flash('User not found.', 'error') flash('User not found.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
# Prevent self-deletion # Prevent self-deletion
if user_to_delete.id == current_user.id: if user_to_delete.id == current_user.id:
flash('You cannot deactivate your own account. Ask another admin to do this.', 'error') 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 # Check if trying to delete the last admin
if user_to_delete.role == 'admin': if user_to_delete.role == 'admin':
active_admin_count = User.query.filter_by(role='admin', active_status=True).count() active_admin_count = User.query.filter_by(role='admin', active_status=True).count()
if active_admin_count <= 1: if active_admin_count <= 1:
flash('Cannot deactivate the last admin user. Promote another user to admin first.', 'error') 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 # Deactivate the user instead of deleting
user_to_delete.active_status = False 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') 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}") 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: except Exception as e:
db.session.rollback() db.session.rollback()
print(f"Error deactivating user: {e}") print(f"Error deactivating user: {e}")
flash('Error deactivating user. Please try again.', 'error') 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') @bp.route('/users/<int:user_id>/reactivate', methods=['GET', 'POST'], endpoint='reactivate_user')
@admin_required @admin_required
def reactivate_user(user_id): def reactivate_user(user_id):
"""Reactivate a deactivated user (Admin only)""" """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: try:
user_to_reactivate = User.query.get(user_id) user_to_reactivate = User.query.get(user_id)
current_user = User.query.get(session['user_id']) current_user = User.query.get(session['user_id'])
if not user_to_reactivate: if not user_to_reactivate:
flash('User not found.', 'error') flash('User not found.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
if user_to_reactivate.active_status: if user_to_reactivate.active_status:
flash('User is already active.', 'info') 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') 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}") 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: except Exception as e:
db.session.rollback() db.session.rollback()
print(f"Error reactivating user: {e}") print(f"Error reactivating user: {e}")
flash('Error reactivating user. Please try again.', 'error') 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') @bp.route('/users/<int:user_id>/promote', methods=['GET', 'POST'], endpoint='promote_user')
@admin_required @admin_required
def promote_user(user_id): def promote_user(user_id):
"""Promote a staff user to admin (Admin only)""" """Promote a staff user to admin (Admin only)"""
User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"]
try: try:
user_to_promote = User.query.get(user_id) user_to_promote = User.query.get(user_id)
current_user = User.query.get(session['user_id']) current_user = User.query.get(session['user_id'])
if not user_to_promote: if not user_to_promote:
flash('User not found.', 'error') flash('User not found.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
if user_to_promote.role == 'admin': if user_to_promote.role == 'admin':
flash('User is already an admin.', 'info') 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') 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") 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: except Exception as e:
db.session.rollback() db.session.rollback()
print(f"Error promoting user: {e}") print(f"Error promoting user: {e}")
flash('Error promoting user. Please try again.', 'error') 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') @bp.route('/users/<int:user_id>/demote', methods=['GET', 'POST'], endpoint='demote_user')
@admin_required @admin_required
def demote_user(user_id): def demote_user(user_id):
"""Demote an admin user to staff (Admin only)""" """Demote an admin user to staff (Admin only)"""
User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"]
try: try:
user_to_demote = User.query.get(user_id) user_to_demote = User.query.get(user_id)
current_user = User.query.get(session['user_id']) current_user = User.query.get(session['user_id'])
if not user_to_demote: if not user_to_demote:
flash('User not found.', 'error') flash('User not found.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
# Prevent self-demotion # Prevent self-demotion
if user_to_demote.id == current_user.id: if user_to_demote.id == current_user.id:
flash('You cannot demote yourself. Have another admin do this.', 'error') 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 # Check if this is the last admin
active_admin_count = User.query.filter_by(role='admin', active_status=True).count() active_admin_count = User.query.filter_by(role='admin', active_status=True).count()
if active_admin_count <= 1 and user_to_demote.role == 'admin': 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') 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): if has_staff_level_access(user_to_demote.role):
flash('User already has staff-level permissions.', 'info') 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') 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") 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: except Exception as e:
db.session.rollback() db.session.rollback()
print(f"Error demoting user: {e}") print(f"Error demoting user: {e}")
flash('Error demoting user. Please try again.', 'error') 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') @bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'], endpoint='edit_user')
@admin_required @admin_required
@log_database_operations('user_edit') @log_database_operations('user_edit')
def edit_user(user_id): def edit_user(user_id):
"""Edit existing user with Project Manager permissions support""" """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: try:
user_to_edit = User.query.get_or_404(user_id) 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)}") 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') 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 # GET request - load form with current assignments
try: try:
@@ -561,20 +554,19 @@ def edit_user(user_id):
except Exception as e: except Exception as e:
logger_handler.logger.error(f"Error loading edit user form: {e}") logger_handler.logger.error(f"Error loading edit user form: {e}")
flash('Error loading edit form. Please try again.', 'error') flash('Error loading edit form. Please try again.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('user_update', e) logger_handler.log_database_error('user_update', e)
logger_handler.logger.error(f"User update error details: {str(e)}") logger_handler.logger.error(f"User update error details: {str(e)}")
flash('Error updating user. Please try again.', 'error') 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') @bp.route('/users/<int:user_id>/toggle-status', methods=['POST'], endpoint='toggle_user_status')
@admin_required @admin_required
def toggle_user_status(user_id): def toggle_user_status(user_id):
"""Toggle user active status via AJAX (Admin only)""" """Toggle user active status via AJAX (Admin only)"""
User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"]
try: try:
user_to_toggle = User.query.get(user_id) user_to_toggle = User.query.get(user_id)
current_user = User.query.get(session['user_id']) current_user = User.query.get(session['user_id'])
@@ -634,14 +626,13 @@ def toggle_user_status(user_id):
@admin_required @admin_required
def activate_user(user_id): def activate_user(user_id):
"""Activate a user (Admin only) - Alternative route""" """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: try:
user_to_activate = User.query.get(user_id) user_to_activate = User.query.get(user_id)
current_user = User.query.get(session['user_id']) current_user = User.query.get(session['user_id'])
if not user_to_activate: if not user_to_activate:
flash('User not found.', 'error') flash('User not found.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
if user_to_activate.active_status: if user_to_activate.active_status:
flash('User is already active.', 'info') 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') flash(f'"{user_to_activate.full_name}" has been activated.', 'success')
print(f"Admin {current_user.username} activated user {user_to_activate.username}") 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: except Exception as e:
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('user_activation', e) logger_handler.log_database_error('user_activation', e)
print(f"Error activating user: {e}") print(f"Error activating user: {e}")
flash('Error activating user. Please try again.', 'error') 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') @bp.route('/users/<int:user_id>/deactivate', methods=['GET', 'POST'], endpoint='deactivate_user')
@admin_required @admin_required
def deactivate_user(user_id): def deactivate_user(user_id):
"""Deactivate a user (Admin only) - Alternative route""" """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: try:
user_to_deactivate = User.query.get(user_id) user_to_deactivate = User.query.get(user_id)
current_user = User.query.get(session['user_id']) current_user = User.query.get(session['user_id'])
if not user_to_deactivate: if not user_to_deactivate:
flash('User not found.', 'error') flash('User not found.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
# Prevent self-deactivation # Prevent self-deactivation
if user_to_deactivate.id == current_user.id: if user_to_deactivate.id == current_user.id:
flash('You cannot deactivate yourself.', 'error') flash('You cannot deactivate yourself.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
# Check if this is the last admin # Check if this is the last admin
if user_to_deactivate.role == 'admin' and user_to_deactivate.active_status: 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() active_admin_count = User.query.filter_by(role='admin', active_status=True).count()
if active_admin_count <= 1: if active_admin_count <= 1:
flash('Cannot deactivate the last admin user.', 'error') 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: if not user_to_deactivate.active_status:
flash('User is already inactive.', 'info') 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') flash(f'"{user_to_deactivate.full_name}" has been deactivated.', 'success')
print(f"Admin {current_user.username} deactivated user {user_to_deactivate.username}") 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: except Exception as e:
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('user_deactivation', e) logger_handler.log_database_error('user_deactivation', e)
print(f"Error deactivating user: {e}") print(f"Error deactivating user: {e}")
flash('Error deactivating user. Please try again.', 'error') flash('Error deactivating user. Please try again.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
# ENHANCED USER STATISTICS API # ENHANCED USER STATISTICS API
@bp.route('/api/users/stats', endpoint='user_stats_api') @bp.route('/api/users/stats', endpoint='user_stats_api')
@admin_required @admin_required
def user_stats_api(): def user_stats_api():
"""API endpoint to get user statistics for dashboard""" """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: try:
# Get current date for recent activity calculations # Get current date for recent activity calculations
one_week_ago = datetime.now() - timedelta(days=7) one_week_ago = datetime.now() - timedelta(days=7)
@@ -759,7 +748,6 @@ def user_stats_api():
@admin_required @admin_required
def get_locations_by_projects(): def get_locations_by_projects():
"""Get locations that belong to selected 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: try:
data = request.get_json() data = request.get_json()
project_ids = data.get('project_ids', []) project_ids = data.get('project_ids', [])
@@ -801,7 +789,6 @@ def get_locations_by_projects():
@admin_required @admin_required
def role_permissions_api(): def role_permissions_api():
"""API endpoint to get role permissions data""" """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: try:
permissions_data = {} permissions_data = {}
for role in VALID_ROLES: for role in VALID_ROLES:
@@ -822,7 +809,6 @@ def role_permissions_api():
@login_required @login_required
def geocode_address_api(): def geocode_address_api():
"""API endpoint to geocode an address and return coordinates using Google Maps""" """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: try:
data = request.get_json() data = request.get_json()
address = data.get('address', '').strip() address = data.get('address', '').strip()
@@ -890,7 +876,6 @@ def geocode_address_api():
@login_required @login_required
def reverse_geocode_api(): def reverse_geocode_api():
"""API endpoint for reverse geocoding coordinates to address using Google Maps""" """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: try:
data = request.get_json() data = request.get_json()
latitude = data.get('latitude') latitude = data.get('latitude')
@@ -945,7 +930,6 @@ def reverse_geocode_api():
@admin_required @admin_required
def permanently_delete_user(user_id): def permanently_delete_user(user_id):
"""Permanently delete user but preserve associated QR codes (Admin only)""" """Permanently delete user but preserve associated QR codes (Admin only)"""
User, UserProjectPermission, UserLocationPermission, Project, QRCode = _get_models()["User"], _get_models()["UserProjectPermission"], _get_models()["UserLocationPermission"], _get_models()["Project"], _get_models()["QRCode"]
try: try:
user_to_delete = User.query.get_or_404(user_id) user_to_delete = User.query.get_or_404(user_id)
current_user = User.query.get(session['user_id']) current_user = User.query.get(session['user_id'])
@@ -953,19 +937,19 @@ def permanently_delete_user(user_id):
# Security checks # Security checks
if user_to_delete.id == current_user.id: if user_to_delete.id == current_user.id:
flash('You cannot delete your own account.', 'error') 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 # Only allow deletion of inactive users for safety
if user_to_delete.active_status: if user_to_delete.active_status:
flash('User must be deactivated before permanent deletion.', 'error') 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 deleting an admin, ensure at least one admin remains
if user_to_delete.role == 'admin': if user_to_delete.role == 'admin':
active_admin_count = User.query.filter_by(role='admin', active_status=True).count() active_admin_count = User.query.filter_by(role='admin', active_status=True).count()
if active_admin_count <= 1: if active_admin_count <= 1:
flash('Cannot delete the last admin user in the system.', 'error') 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_name = user_to_delete.full_name
user_qr_count = user_to_delete.created_qr_codes.count() 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') 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") 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: except Exception as e:
db.session.rollback() db.session.rollback()
logger_handler.log_database_error('user_permanent_deletion', e) logger_handler.log_database_error('user_permanent_deletion', e)
print(f"Error permanently deleting user: {e}") print(f"Error permanently deleting user: {e}")
flash('Error deleting user. Please try again.', 'error') flash('Error deleting user. Please try again.', 'error')
return redirect(url_for('users')) return redirect(url_for('users.users'))
# Admin logging routes # Admin logging routes
+2 -2
View File
@@ -212,7 +212,7 @@
with a fixed distance of 0.010 miles. with a fixed distance of 0.010 miles.
</div> </div>
<form id="manualAttendanceForm" method="POST" action="{{ url_for('save_manual_attendance') }}"> <form id="manualAttendanceForm" method="POST" action="{{ url_for('attendance.save_manual_attendance') }}">
<!-- Employee Selection with Autocomplete --> <!-- Employee Selection with Autocomplete -->
<div class="form-group"> <div class="form-group">
<label for="employee_search"> <label for="employee_search">
@@ -307,7 +307,7 @@
<!-- Form Actions --> <!-- Form Actions -->
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('attendance_report') }}" class="btn btn-secondary"> <a href="{{ url_for('attendance.attendance_report') }}" class="btn btn-secondary">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Cancel Cancel
</a> </a>
+6 -6
View File
@@ -41,7 +41,7 @@
<!-- NEW: Manual Add Record Button --> <!-- NEW: Manual Add Record Button -->
{% if session.role in ['admin', 'accounting'] %} {% if session.role in ['admin', 'accounting'] %}
<button onclick="window.location.href='{{ url_for('add_manual_attendance') }}'" class="btn btn-primary"> <button onclick="window.location.href='{{ url_for('attendance.add_manual_attendance') }}'" class="btn btn-primary">
<i class="fas fa-plus-circle"></i> <i class="fas fa-plus-circle"></i>
Add Record Add Record
</button> </button>
@@ -67,7 +67,7 @@
</h3> </h3>
</div> </div>
<div class="filters-form"> <div class="filters-form">
<form method="GET" action="{{ url_for('attendance_report') }}"> <form method="GET" action="{{ url_for('attendance.attendance_report') }}">
<!-- Hidden field to preserve fullscreen state --> <!-- Hidden field to preserve fullscreen state -->
<input type="hidden" id="fullscreenState" name="fullscreen" value=""> <input type="hidden" id="fullscreenState" name="fullscreen" value="">
<div class="filter-row"> <div class="filter-row">
@@ -297,7 +297,7 @@
{% if record.verification_required and record.verification_status == 'pending' %} {% if record.verification_required and record.verification_status == 'pending' %}
<!-- Show Review Needed badge with link to review page --> <!-- Show Review Needed badge with link to review page -->
<div class="location-accuracy-info"> <div class="location-accuracy-info">
<a href="{{ url_for('verification_review_detail', record_id=record.id) }}" <a href="{{ url_for('attendance.verification_review_detail', record_id=record.id) }}"
class="location-accuracy-badge badge-review-needed" class="location-accuracy-badge badge-review-needed"
style="cursor: pointer; text-decoration: none;" style="cursor: pointer; text-decoration: none;"
title="Click to review verification photo - Distance: {{ '%.3f'|format(record.location_accuracy) }} miles"> title="Click to review verification photo - Distance: {{ '%.3f'|format(record.location_accuracy) }} miles">
@@ -368,7 +368,7 @@
<div class="record-actions"> <div class="record-actions">
{% if record.verification_required and record.verification_status == 'pending' %} {% if record.verification_required and record.verification_status == 'pending' %}
<!-- Show Review button linking to review page --> <!-- Show Review button linking to review page -->
<a href="{{ url_for('verification_review_detail', record_id=record.id) }}" <a href="{{ url_for('attendance.verification_review_detail', record_id=record.id) }}"
class="action-btn btn-review" class="action-btn btn-review"
title="Review Verification Photo"> title="Review Verification Photo">
<i class="fas fa-camera"></i> <i class="fas fa-camera"></i>
@@ -487,9 +487,9 @@ function clearFilters() {
// Redirect to clean URL with only fullscreen parameter if active // Redirect to clean URL with only fullscreen parameter if active
if (fullscreenState === '1') { if (fullscreenState === '1') {
window.location.href = '{{ url_for('attendance_report') }}?fullscreen=1'; window.location.href = '{{ url_for('attendance.attendance_report') }}?fullscreen=1';
} else { } else {
window.location.href = '{{ url_for('attendance_report') }}'; window.location.href = '{{ url_for('attendance.attendance_report') }}';
} }
} }
+19 -19
View File
@@ -44,95 +44,95 @@
<div class="sidebar-menu"> <div class="sidebar-menu">
<div class="menu-section"> <div class="menu-section">
<div class="menu-items"> <div class="menu-items">
<a href="{{ url_for('dashboard') }}" class="menu-item"> <a href="{{ url_for('dashboard.dashboard') }}" class="menu-item">
<i class="fas fa-tachometer-alt"></i> <i class="fas fa-tachometer-alt"></i>
<span class="menu-text">Dashboard</span> <span class="menu-text">Dashboard</span>
</a> </a>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<a href="{{ url_for('create_qr_code') }}" class="menu-item"> <a href="{{ url_for('qr_codes.create_qr_code') }}" class="menu-item">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
<span class="menu-text">Create QR</span> <span class="menu-text">Create QR</span>
</a> </a>
<a <a
href="{{ url_for('projects') }}" href="{{ url_for('projects.projects') }}"
class="menu-item {% if request.endpoint in ['projects', 'create_project', 'edit_project'] %}active{% endif %}" class="menu-item {% if request.endpoint in ['projects', 'create_project', 'edit_project'] %}active{% endif %}"
> >
<i class="fas fa-folder"></i> <i class="fas fa-folder"></i>
<span class="menu-text">Projects</span> <span class="menu-text">Projects</span>
</a> </a>
<a href="{{ url_for('users') }}" class="menu-item"> <a href="{{ url_for('users.users') }}" class="menu-item">
<i class="fas fa-user-cog"></i> <i class="fas fa-user-cog"></i>
<span class="menu-text">Users</span> <span class="menu-text">Users</span>
</a> </a>
<a href="{{ url_for('employees') }}" class="menu-item"> <a href="{{ url_for('employees.employees') }}" class="menu-item">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
<span class="menu-text">Employees</span> <span class="menu-text">Employees</span>
</a> </a>
<a href="{{ url_for('attendance_report') }}" class="menu-item"> <a href="{{ url_for('attendance.attendance_report') }}" class="menu-item">
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line"></i>
<span class="menu-text">Reports</span> <span class="menu-text">Reports</span>
</a> </a>
<a href="{{ url_for('verification_review') }}" <a href="{{ url_for('attendance.verification_review') }}"
class="menu-item {% if request.endpoint == 'verification_review' %}active{% endif %}"> class="menu-item {% if request.endpoint == 'verification_review' %}active{% endif %}">
<i class="fas fa-camera-retro"></i> <i class="fas fa-camera-retro"></i>
<span class="menu-text">Verification Review</span> <span class="menu-text">Verification Review</span>
</a> </a>
<a href="{{ url_for('time_attendance_dashboard') }}" <a href="{{ url_for('time_attendance.time_attendance_dashboard') }}"
class="menu-item {% if request.endpoint and (request.endpoint.startswith('time_attendance') or request.endpoint.startswith('import_time_attendance')) %}active{% endif %}"> class="menu-item {% if request.endpoint and (request.endpoint.startswith('time_attendance') or request.endpoint.startswith('import_time_attendance')) %}active{% endif %}">
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
<span class="menu-text">Time Attendance</span> <span class="menu-text">Time Attendance</span>
</a> </a>
<a href="{{ url_for('payroll_dashboard') }}" class="menu-item"> <a href="{{ url_for('payroll.payroll_dashboard') }}" class="menu-item">
<i class="fas fa-calculator"></i> <i class="fas fa-calculator"></i>
<span class="menu-text">Payroll</span> <span class="menu-text">Payroll</span>
</a> </a>
<a <a
href="{{ url_for('qr_statistics') }}" href="{{ url_for('statistics.qr_statistics') }}"
class="menu-item {% if request.endpoint == 'qr_statistics' %}active{% endif %}" class="menu-item {% if request.endpoint == 'qr_statistics' %}active{% endif %}"
> >
<i class="fas fa-chart-pie"></i> <i class="fas fa-chart-pie"></i>
<span class="menu-text">Statistics</span> <span class="menu-text">Statistics</span>
</a> </a>
<a <a
href="{{ url_for('admin_logs') }}" href="{{ url_for('admin.admin_logs') }}"
class="menu-item {% if request.endpoint == 'admin_logs' %}active{% endif %}" class="menu-item {% if request.endpoint == 'admin_logs' %}active{% endif %}"
> >
<i class="fas fa-clipboard-list"></i> <i class="fas fa-clipboard-list"></i>
<span class="menu-text">System Logs</span> <span class="menu-text">System Logs</span>
</a> </a>
{% elif session.role in ['payroll', 'accounting'] %} {% elif session.role in ['payroll', 'accounting'] %}
<a href="{{ url_for('employees') }}" class="menu-item"> <a href="{{ url_for('employees.employees') }}" class="menu-item">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
<span class="menu-text">Employees</span> <span class="menu-text">Employees</span>
</a> </a>
<a href="{{ url_for('attendance_report') }}" class="menu-item"> <a href="{{ url_for('attendance.attendance_report') }}" class="menu-item">
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line"></i>
<span class="menu-text">Reports</span> <span class="menu-text">Reports</span>
</a> </a>
<a href="{{ url_for('verification_review') }}" <a href="{{ url_for('attendance.verification_review') }}"
class="menu-item {% if request.endpoint == 'verification_review' %}active{% endif %}"> class="menu-item {% if request.endpoint == 'verification_review' %}active{% endif %}">
<i class="fas fa-camera-retro"></i> <i class="fas fa-camera-retro"></i>
<span class="menu-text">Verification Review</span> <span class="menu-text">Verification Review</span>
</a> </a>
<a href="{{ url_for('time_attendance_dashboard') }}" <a href="{{ url_for('time_attendance.time_attendance_dashboard') }}"
class="menu-item {% if request.endpoint and (request.endpoint.startswith('time_attendance') or request.endpoint.startswith('import_time_attendance')) %}active{% endif %}"> class="menu-item {% if request.endpoint and (request.endpoint.startswith('time_attendance') or request.endpoint.startswith('import_time_attendance')) %}active{% endif %}">
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
<span class="menu-text">Time Attendance</span> <span class="menu-text">Time Attendance</span>
</a> </a>
<!-- <!--
<a href="{{ url_for('payroll_dashboard') }}" class="menu-item"> <a href="{{ url_for('payroll.payroll_dashboard') }}" class="menu-item">
<i class="fas fa-calculator"></i> <i class="fas fa-calculator"></i>
<span class="menu-text">Payroll</span> <span class="menu-text">Payroll</span>
</a> </a>
--> -->
{% elif session.role in ['project_manager'] %} {% elif session.role in ['project_manager'] %}
<a href="{{ url_for('attendance_report') }}" class="menu-item"> <a href="{{ url_for('attendance.attendance_report') }}" class="menu-item">
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line"></i>
<span class="menu-text">Reports</span> <span class="menu-text">Reports</span>
</a> </a>
{% endif %} {% endif %}
<a href="{{ url_for('profile') }}" class="menu-item"> <a href="{{ url_for('auth.profile') }}" class="menu-item">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
<span class="menu-text">Profile</span> <span class="menu-text">Profile</span>
</a> </a>
@@ -142,7 +142,7 @@
<!-- Bottom Section --> <!-- Bottom Section -->
<div class="sidebar-bottom"> <div class="sidebar-bottom">
<div class="menu-items"> <div class="menu-items">
<a href="{{ url_for('logout') }}" class="menu-item logout"> <a href="{{ url_for('auth.logout') }}" class="menu-item logout">
<i class="fas fa-sign-out-alt"></i> <i class="fas fa-sign-out-alt"></i>
<span class="menu-text">Logout</span> <span class="menu-text">Logout</span>
</a> </a>
+5 -5
View File
@@ -58,7 +58,7 @@
</div> </div>
<h3>Download Template</h3> <h3>Download Template</h3>
<p>Use our pre-formatted template to ensure proper column structure:</p> <p>Use our pre-formatted template to ensure proper column structure:</p>
<a href="{{ url_for('download_qr_import_template') }}" class="btn btn-primary"> <a href="{{ url_for('qr_codes.download_qr_import_template') }}" class="btn btn-primary">
<i class="fas fa-file-excel"></i> <i class="fas fa-file-excel"></i>
Download Excel Template Download Excel Template
</a> </a>
@@ -128,7 +128,7 @@
</h2> </h2>
</div> </div>
<div class="import-body"> <div class="import-body">
<form id="importForm" method="POST" enctype="multipart/form-data" action="{{ url_for('import_bulk_qr_codes') }}"> <form id="importForm" method="POST" enctype="multipart/form-data" action="{{ url_for('qr_codes.import_bulk_qr_codes') }}">
<!-- File Upload Area --> <!-- File Upload Area -->
<div class="file-upload-area" id="fileUploadArea"> <div class="file-upload-area" id="fileUploadArea">
<div class="upload-icon"> <div class="upload-icon">
@@ -171,7 +171,7 @@
<!-- Submit Button --> <!-- Submit Button -->
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Cancel Cancel
</a> </a>
@@ -233,7 +233,7 @@
<h3>Validation Successful!</h3> <h3>Validation Successful!</h3>
<p>All {{ validation_result.valid_rows }} records are valid and ready to import.</p> <p>All {{ validation_result.valid_rows }} records are valid and ready to import.</p>
</div> </div>
<form method="POST" action="{{ url_for('import_bulk_qr_codes') }}"> <form method="POST" action="{{ url_for('qr_codes.import_bulk_qr_codes') }}">
<input type="hidden" name="proceed_import" value="true"> <input type="hidden" name="proceed_import" value="true">
<button type="submit" class="btn btn-success"> <button type="submit" class="btn btn-success">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
@@ -321,7 +321,7 @@
<h3>Import Successful!</h3> <h3>Import Successful!</h3>
<p>Successfully imported {{ import_result.imported_records }} QR codes.</p> <p>Successfully imported {{ import_result.imported_records }} QR codes.</p>
</div> </div>
<a href="{{ url_for('dashboard') }}" class="btn btn-primary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-primary">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
View QR Codes View QR Codes
</a> </a>
+2 -2
View File
@@ -367,13 +367,13 @@ endblock %} {% block extra_head %}
</div> </div>
<div class="confirmation-actions"> <div class="confirmation-actions">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Cancel & Go Back Cancel & Go Back
</a> </a>
<a <a
href="{{ url_for('deactivate_qr_code', qr_id=qr_code.id) }}" href="{{ url_for('qr_codes.deactivate_qr_code', qr_id=qr_code.id) }}"
class="btn btn-warning" class="btn btn-warning"
> >
<i class="fas fa-pause"></i> <i class="fas fa-pause"></i>
+2 -2
View File
@@ -171,7 +171,7 @@
<div class="employees-header"> <div class="employees-header">
<div class="header-content"> <div class="header-content">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('employees') }}" class="back-button"> <a href="{{ url_for('employees.employees') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Employees Back to Employees
</a> </a>
@@ -287,7 +287,7 @@
<!-- Form Actions --> <!-- Form Actions -->
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('employees') }}" class="btn btn-secondary"> <a href="{{ url_for('employees.employees') }}" class="btn btn-secondary">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Cancel Cancel
</a> </a>
+2 -2
View File
@@ -14,7 +14,7 @@
<p>Create a new project to organize your QR codes</p> <p>Create a new project to organize your QR codes</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a href="{{ url_for('projects') }}" class="btn btn-secondary"> <a href="{{ url_for('projects.projects') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Projects Back to Projects
</a> </a>
@@ -71,7 +71,7 @@
<!-- Form Actions --> <!-- Form Actions -->
<div style="display: flex; justify-content: flex-start; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid #e5e7eb;"> <div style="display: flex; justify-content: flex-start; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid #e5e7eb;">
<a href="{{ url_for('projects') }}" class="btn btn-secondary" style="background: #f1f5f9; color: #475569; padding: 0.75rem 1.5rem; border-radius: 0.5rem; text-decoration: none; font-weight: 500; display: inline-flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease-in-out; border: 1px solid #cbd5e1;"> <a href="{{ url_for('projects.projects') }}" class="btn btn-secondary" style="background: #f1f5f9; color: #475569; padding: 0.75rem 1.5rem; border-radius: 0.5rem; text-decoration: none; font-weight: 500; display: inline-flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease-in-out; border: 1px solid #cbd5e1;">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Cancel Cancel
</a> </a>
+2 -2
View File
@@ -557,7 +557,7 @@
</div> </div>
<div style="margin-top: 1rem; margin-bottom: 1rem;"> <div style="margin-top: 1rem; margin-bottom: 1rem;">
<a href="{{ url_for('import_bulk_qr_codes') }}" class="btn btn-outline" style="display: inline-flex; align-items: center; gap: 0.5rem;"> <a href="{{ url_for('qr_codes.import_bulk_qr_codes') }}" class="btn btn-outline" style="display: inline-flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-file-excel"></i> <i class="fas fa-file-excel"></i>
Bulk Import from Excel Bulk Import from Excel
</a> </a>
@@ -928,7 +928,7 @@
<!-- Form Actions --> <!-- Form Actions -->
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Cancel Cancel
</a> </a>
+2 -2
View File
@@ -15,7 +15,7 @@ Code Management{% endblock %} {% block content %}
<form <form
id="createUserForm" id="createUserForm"
method="POST" method="POST"
action="{{ url_for('create_user') }}" action="{{ url_for('users.create_user') }}"
> >
<div class="form-section"> <div class="form-section">
<h3> <h3>
@@ -253,7 +253,7 @@ Code Management{% endblock %} {% block content %}
</div> </div>
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('users') }}" class="btn btn-secondary"> <a href="{{ url_for('users.users') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Users Back to Users
</a> </a>
+11 -11
View File
@@ -1251,7 +1251,7 @@ Management{% endblock %} {% block extra_head %}
</div> </div>
<!-- QR Code Search Section --> <!-- QR Code Search Section -->
<div class="search-section"> <div class="search-section">
<form action="{{ url_for('dashboard') }}" method="GET" class="search-form"> <form action="{{ url_for('dashboard.dashboard') }}" method="GET" class="search-form">
<div class="search-inputs"> <div class="search-inputs">
<div class="search-input-group"> <div class="search-input-group">
<label for="search_name"> <label for="search_name">
@@ -1298,7 +1298,7 @@ Management{% endblock %} {% block extra_head %}
{% if search_name %}matching "{{ search_name }}"{% endif %} {% if search_name %}matching "{{ search_name }}"{% endif %}
{% if search_status %}with status: {{ search_status }}{% endif %} {% if search_status %}with status: {{ search_status }}{% endif %}
</span> </span>
<a href="{{ url_for('dashboard') }}" class="btn btn-sm btn-secondary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-sm btn-secondary">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Clear Filters Clear Filters
</a> </a>
@@ -1442,7 +1442,7 @@ Management{% endblock %} {% block extra_head %}
</button> </button>
<a <a
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}" href="{{ url_for('qr_codes.edit_qr_code', qr_id=qr.id) }}"
class="qr-action-btn edit" class="qr-action-btn edit"
onclick="event.stopPropagation()" onclick="event.stopPropagation()"
title="Edit QR Code" title="Edit QR Code"
@@ -1583,7 +1583,7 @@ Management{% endblock %} {% block extra_head %}
</button> </button>
<a <a
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}" href="{{ url_for('qr_codes.edit_qr_code', qr_id=qr.id) }}"
class="qr-action-btn edit" class="qr-action-btn edit"
onclick="event.stopPropagation()" onclick="event.stopPropagation()"
title="Edit QR Code" title="Edit QR Code"
@@ -1617,7 +1617,7 @@ Management{% endblock %} {% block extra_head %}
</div> </div>
<h3>No QR Codes Found</h3> <h3>No QR Codes Found</h3>
<p>No QR codes match your search criteria. Try adjusting your filters.</p> <p>No QR codes match your search criteria. Try adjusting your filters.</p>
<a href="{{ url_for('dashboard') }}" class="btn btn-primary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-primary">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Clear Filters Clear Filters
</a> </a>
@@ -1755,7 +1755,7 @@ Management{% endblock %} {% block extra_head %}
</button> </button>
<a <a
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}" href="{{ url_for('qr_codes.edit_qr_code', qr_id=qr.id) }}"
class="qr-action-btn edit" class="qr-action-btn edit"
onclick="event.stopPropagation()" onclick="event.stopPropagation()"
title="Edit QR Code" title="Edit QR Code"
@@ -1780,7 +1780,7 @@ Management{% endblock %} {% block extra_head %}
<!-- Show "View All" button if project has more than 20 QR codes --> <!-- Show "View All" button if project has more than 20 QR codes -->
{% if project_qr_codes|length > 12 %} {% if project_qr_codes|length > 12 %}
<a <a
href="{{ url_for('project_qr_codes', project_id=project.id) }}" href="{{ url_for('dashboard.project_qr_codes', project_id=project.id) }}"
class="btn btn-sm btn-primary view-all-qr-btn" class="btn btn-sm btn-primary view-all-qr-btn"
onclick="event.stopPropagation()" onclick="event.stopPropagation()"
> >
@@ -1796,7 +1796,7 @@ Management{% endblock %} {% block extra_head %}
</div> </div>
<h4>No QR Codes in this Project</h4> <h4>No QR Codes in this Project</h4>
<p>Create your first QR code for this project</p> <p>Create your first QR code for this project</p>
<a href="{{ url_for('create_qr_code') }}" class="btn btn-primary"> <a href="{{ url_for('qr_codes.create_qr_code') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
Create QR Code Create QR Code
</a> </a>
@@ -1916,7 +1916,7 @@ Management{% endblock %} {% block extra_head %}
</button> </button>
<a <a
href="{{ url_for('edit_qr_code', qr_id=qr.id) }}" href="{{ url_for('qr_codes.edit_qr_code', qr_id=qr.id) }}"
class="qr-action-btn edit" class="qr-action-btn edit"
onclick="event.stopPropagation()" onclick="event.stopPropagation()"
title="Edit QR Code" title="Edit QR Code"
@@ -1957,7 +1957,7 @@ Management{% endblock %} {% block extra_head %}
{% if search_name and search_status %} or {% endif %} {% if search_name and search_status %} or {% endif %}
{% if search_status %}try changing the status filter{% endif %}. {% if search_status %}try changing the status filter{% endif %}.
</p> </p>
<a href="{{ url_for('dashboard') }}" class="btn btn-primary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-primary">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Clear Filters Clear Filters
</a> </a>
@@ -1970,7 +1970,7 @@ Management{% endblock %} {% block extra_head %}
Get started by creating your first project and QR codes to organize your Get started by creating your first project and QR codes to organize your
digital assets effectively. digital assets effectively.
</p> </p>
<a href="{{ url_for('projects') }}" class="btn btn-primary"> <a href="{{ url_for('projects.projects') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
Create Your First Project Create Your First Project
</a> </a>
+2 -2
View File
@@ -144,7 +144,7 @@
</p> </p>
</div> </div>
<form method="POST" action="{{ url_for('edit_attendance', record_id=attendance_record.id) }}"> <form method="POST" action="{{ url_for('attendance.edit_attendance', record_id=attendance_record.id) }}">
<!-- Audit Note Section --> <!-- Audit Note Section -->
<div class="audit-note-section"> <div class="audit-note-section">
@@ -299,7 +299,7 @@
Update Record Update Record
</button> </button>
{% endif %} {% endif %}
<a href="{{ url_for('attendance_report') }}" class="btn btn-secondary"> <a href="{{ url_for('attendance.attendance_report') }}" class="btn btn-secondary">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Cancel Cancel
</a> </a>
+2 -2
View File
@@ -166,7 +166,7 @@
<div class="employees-header"> <div class="employees-header">
<div class="header-content"> <div class="header-content">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('employees') }}" class="back-button"> <a href="{{ url_for('employees.employees') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Employees Back to Employees
</a> </a>
@@ -291,7 +291,7 @@
<!-- Form Actions --> <!-- Form Actions -->
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('employees') }}" class="btn btn-secondary"> <a href="{{ url_for('employees.employees') }}" class="btn btn-secondary">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Cancel Cancel
</a> </a>
+2 -2
View File
@@ -14,7 +14,7 @@
<p>Update project information and settings</p> <p>Update project information and settings</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a href="{{ url_for('projects') }}" class="btn btn-secondary"> <a href="{{ url_for('projects.projects') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Projects Back to Projects
</a> </a>
@@ -108,7 +108,7 @@
<!-- Form Actions --> <!-- Form Actions -->
<div style="display: flex; justify-content: flex-start; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid #e5e7eb;"> <div style="display: flex; justify-content: flex-start; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid #e5e7eb;">
<a href="{{ url_for('projects') }}" class="btn btn-secondary" style="background: #f1f5f9; color: #475569; padding: 0.75rem 1.5rem; border-radius: 0.5rem; text-decoration: none; font-weight: 500; display: inline-flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease-in-out; border: 1px solid #cbd5e1;"> <a href="{{ url_for('projects.projects') }}" class="btn btn-secondary" style="background: #f1f5f9; color: #475569; padding: 0.75rem 1.5rem; border-radius: 0.5rem; text-decoration: none; font-weight: 500; display: inline-flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease-in-out; border: 1px solid #cbd5e1;">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Cancel Cancel
</a> </a>
+1 -1
View File
@@ -1001,7 +1001,7 @@
<!-- Form Actions --> <!-- Form Actions -->
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Cancel Cancel
</a> </a>
+2 -2
View File
@@ -14,7 +14,7 @@
</div> </div>
<div class="edit-user-container"> <div class="edit-user-container">
<form id="editUserForm" method="POST" action="{{ url_for('edit_user', user_id=user.id) }}"> <form id="editUserForm" method="POST" action="{{ url_for('users.edit_user', user_id=user.id) }}">
<!-- User Information Section --> <!-- User Information Section -->
<div class="form-section"> <div class="form-section">
<h3> <h3>
@@ -261,7 +261,7 @@
<!-- Form Actions --> <!-- Form Actions -->
<div class="form-actions"> <div class="form-actions">
<a href="{{ url_for('users') }}" class="btn btn-secondary"> <a href="{{ url_for('users.users') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Users Back to Users
</a> </a>
+1 -1
View File
@@ -331,7 +331,7 @@
<div class="employees-header" style="margin-bottom: 1rem;"> <div class="employees-header" style="margin-bottom: 1rem;">
<div class="header-content"> <div class="header-content">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('employees') }}" class="back-button"> <a href="{{ url_for('employees.employees') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Employees Back to Employees
</a> </a>
+10 -10
View File
@@ -11,7 +11,7 @@ extra_head %}
<div class="employees-header"> <div class="employees-header">
<div class="header-content"> <div class="header-content">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('dashboard') }}" class="back-button"> <a href="{{ url_for('dashboard.dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Dashboard Back to Dashboard
</a> </a>
@@ -25,7 +25,7 @@ extra_head %}
<div class="header-actions"> <div class="header-actions">
{% if session.role in ['admin', 'payroll'] %} {% if session.role in ['admin', 'payroll'] %}
<a href="{{ url_for('create_employee') }}" class="btn btn-primary"> <a href="{{ url_for('employees.create_employee') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
Add Employee Add Employee
</a> </a>
@@ -95,7 +95,7 @@ extra_head %}
</button> </button>
{% if search %} {% if search %}
<a <a
href="{{ url_for('employees') }}" href="{{ url_for('employees.employees') }}"
class="clear-search-btn" class="clear-search-btn"
title="Clear search" title="Clear search"
> >
@@ -170,7 +170,7 @@ extra_head %}
<td class="actions"> <td class="actions">
<div class="action-buttons"> <div class="action-buttons">
<a <a
href="{{ url_for('employee_detail', employee_index=employee.index) }}" href="{{ url_for('employees.employee_detail', employee_index=employee.index) }}"
class="btn btn-sm btn-info" class="btn btn-sm btn-info"
title="View Details" title="View Details"
> >
@@ -179,7 +179,7 @@ extra_head %}
{% if session.role in ['admin', 'payroll'] %} {% if session.role in ['admin', 'payroll'] %}
<a <a
href="{{ url_for('edit_employee', employee_index=employee.index) }}" href="{{ url_for('employees.edit_employee', employee_index=employee.index) }}"
class="btn btn-sm btn-warning" class="btn btn-sm btn-warning"
title="Edit Employee" title="Edit Employee"
> >
@@ -212,7 +212,7 @@ extra_head %}
{% if employees.has_prev %} {% if employees.has_prev %}
<li> <li>
<a <a
href="{{ url_for('employees', page=employees.prev_num, search=search) }}" href="{{ url_for('employees.employees', page=employees.prev_num, search=search) }}"
class="pagination-link" class="pagination-link"
> >
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
@@ -226,7 +226,7 @@ extra_head %}
page_num != employees.page %} page_num != employees.page %}
<li> <li>
<a <a
href="{{ url_for('employees', page=page_num, search=search) }}" href="{{ url_for('employees.employees', page=page_num, search=search) }}"
class="pagination-link" class="pagination-link"
> >
{{ page_num }} {{ page_num }}
@@ -246,7 +246,7 @@ extra_head %}
{% if employees.has_next %} {% if employees.has_next %}
<li> <li>
<a <a
href="{{ url_for('employees', page=employees.next_num, search=search) }}" href="{{ url_for('employees.employees', page=employees.next_num, search=search) }}"
class="pagination-link" class="pagination-link"
> >
Next Next
@@ -271,9 +271,9 @@ extra_head %}
</h3> </h3>
<p> <p>
{% if search %} Try adjusting your search terms or {% if search %} Try adjusting your search terms or
<a href="{{ url_for('employees') }}">view all employees</a>. {% else %} <a href="{{ url_for('employees.employees') }}">view all employees</a>. {% else %}
Get started by Get started by
<a href="{{ url_for('create_employee') }}">adding your first employee</a <a href="{{ url_for('employees.create_employee') }}">adding your first employee</a
>. {% endif %} >. {% endif %}
</p> </p>
</div> </div>
+2 -2
View File
@@ -85,7 +85,7 @@
<!-- Export Configuration Form --> <!-- Export Configuration Form -->
<div class="export-config-section"> <div class="export-config-section">
<form method="POST" action="{{ url_for('generate_excel_export') }}" id="exportForm"> <form method="POST" action="{{ url_for('attendance.generate_excel_export') }}" id="exportForm">
<!-- Hidden fields for filters --> <!-- Hidden fields for filters -->
<input type="hidden" name="date_from" value="{{ filters.date_from }}"> <input type="hidden" name="date_from" value="{{ filters.date_from }}">
<input type="hidden" name="date_to" value="{{ filters.date_to }}"> <input type="hidden" name="date_to" value="{{ filters.date_to }}">
@@ -262,7 +262,7 @@ function goBackToReport() {
if (employeeFilter) params.append('employee', employeeFilter); if (employeeFilter) params.append('employee', employeeFilter);
if (projectFilter) params.append('project', projectFilter); if (projectFilter) params.append('project', projectFilter);
const url = "{{ url_for('attendance_report') }}" + (params.toString() ? '?' + params.toString() : ''); const url = "{{ url_for('attendance.attendance_report') }}" + (params.toString() ? '?' + params.toString() : '');
window.location.href = url; window.location.href = url;
} }
+7 -7
View File
@@ -642,7 +642,7 @@
{% block content %} {% block content %}
<div class="payroll-container"> <div class="payroll-container">
<!-- Back Navigation --> <!-- Back Navigation -->
<a href="{{ url_for('dashboard') }}" class="back-nav"> <a href="{{ url_for('dashboard.dashboard') }}" class="back-nav">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
<span>Back to Dashboard</span> <span>Back to Dashboard</span>
</a> </a>
@@ -663,7 +663,7 @@
Calculation Parameters Calculation Parameters
</h3> </h3>
<form method="GET" action="{{ url_for('payroll_dashboard') }}" class="filters-form"> <form method="GET" action="{{ url_for('payroll.payroll_dashboard') }}" class="filters-form">
<div class="form-group"> <div class="form-group">
<label for="date_from">Start Date</label> <label for="date_from">Start Date</label>
<input type="date" id="date_from" name="date_from" value="{{ date_from }}" required> <input type="date" id="date_from" name="date_from" value="{{ date_from }}" required>
@@ -820,7 +820,7 @@
<!-- Export Actions --> <!-- Export Actions -->
<div class="export-actions"> <div class="export-actions">
<!-- <!--
<form method="POST" action="{{ url_for('export_payroll_excel') }}" style="display: inline;"> <form method="POST" action="{{ url_for('payroll.export_payroll_excel') }}" style="display: inline;">
<input type="hidden" name="date_from" value="{{ date_from }}"> <input type="hidden" name="date_from" value="{{ date_from }}">
<input type="hidden" name="date_to" value="{{ date_to }}"> <input type="hidden" name="date_to" value="{{ date_to }}">
<input type="hidden" name="project_filter" value="{{ project_filter }}"> <input type="hidden" name="project_filter" value="{{ project_filter }}">
@@ -831,7 +831,7 @@
</button> </button>
</form> </form>
<form method="POST" action="{{ url_for('export_payroll_excel') }}" style="display: inline;"> <form method="POST" action="{{ url_for('payroll.export_payroll_excel') }}" style="display: inline;">
<input type="hidden" name="date_from" value="{{ date_from }}"> <input type="hidden" name="date_from" value="{{ date_from }}">
<input type="hidden" name="date_to" value="{{ date_to }}"> <input type="hidden" name="date_to" value="{{ date_to }}">
<input type="hidden" name="project_filter" value="{{ project_filter }}"> <input type="hidden" name="project_filter" value="{{ project_filter }}">
@@ -842,7 +842,7 @@
</button> </button>
</form> </form>
--> -->
<form method="POST" action="{{ url_for('export_payroll_excel') }}" style="display: inline;"> <form method="POST" action="{{ url_for('payroll.export_payroll_excel') }}" style="display: inline;">
<input type="hidden" name="date_from" value="{{ date_from }}"> <input type="hidden" name="date_from" value="{{ date_from }}">
<input type="hidden" name="date_to" value="{{ date_to }}"> <input type="hidden" name="date_to" value="{{ date_to }}">
<input type="hidden" name="project_filter" value="{{ project_filter }}"> <input type="hidden" name="project_filter" value="{{ project_filter }}">
@@ -853,7 +853,7 @@
</button> </button>
</form> </form>
<!-- <!--
<form method="POST" action="{{ url_for('export_payroll_excel') }}" style="display: inline;"> <form method="POST" action="{{ url_for('payroll.export_payroll_excel') }}" style="display: inline;">
<input type="hidden" name="date_from" value="{{ date_from }}"> <input type="hidden" name="date_from" value="{{ date_from }}">
<input type="hidden" name="date_to" value="{{ date_to }}"> <input type="hidden" name="date_to" value="{{ date_to }}">
<input type="hidden" name="project_filter" value="{{ project_filter }}"> <input type="hidden" name="project_filter" value="{{ project_filter }}">
@@ -864,7 +864,7 @@
</button> </button>
</form> </form>
<form method="POST" action="{{ url_for('export_payroll_excel') }}" style="display: inline;"> <form method="POST" action="{{ url_for('payroll.export_payroll_excel') }}" style="display: inline;">
<input type="hidden" name="date_from" value="{{ date_from }}"> <input type="hidden" name="date_from" value="{{ date_from }}">
<input type="hidden" name="date_to" value="{{ date_to }}"> <input type="hidden" name="date_to" value="{{ date_to }}">
<input type="hidden" name="project_filter" value="{{ project_filter }}"> <input type="hidden" name="project_filter" value="{{ project_filter }}">
+7 -7
View File
@@ -688,7 +688,7 @@
<p class="project-qr-description">{{ project.description }}</p> <p class="project-qr-description">{{ project.description }}</p>
{% endif %} {% endif %}
</div> </div>
<a href="{{ url_for('dashboard') }}" class="back-button"> <a href="{{ url_for('dashboard.dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Dashboard Back to Dashboard
</a> </a>
@@ -712,7 +712,7 @@
<!-- QR Code Search Section --> <!-- QR Code Search Section -->
<div class="search-section"> <div class="search-section">
<form action="{{ url_for('project_qr_codes', project_id=project.id) }}" method="GET" class="search-form"> <form action="{{ url_for('dashboard.project_qr_codes', project_id=project.id) }}" method="GET" class="search-form">
<div class="search-inputs"> <div class="search-inputs">
<div class="search-input-group"> <div class="search-input-group">
<label for="search_name"> <label for="search_name">
@@ -745,7 +745,7 @@
Search Search
</button> </button>
{% if search_name or search_status %} {% if search_name or search_status %}
<a href="{{ url_for('project_qr_codes', project_id=project.id) }}" class="btn btn-outline"> <a href="{{ url_for('dashboard.project_qr_codes', project_id=project.id) }}" class="btn btn-outline">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Clear Clear
</a> </a>
@@ -766,7 +766,7 @@
{% if search_status %} - Status: <strong>{{ search_status|title }}</strong>{% endif %} {% if search_status %} - Status: <strong>{{ search_status|title }}</strong>{% endif %}
({{ qr_codes|length }} QR code{{ 's' if qr_codes|length != 1 else '' }} found) ({{ qr_codes|length }} QR code{{ 's' if qr_codes|length != 1 else '' }} found)
</span> </span>
<a href="{{ url_for('project_qr_codes', project_id=project.id) }}" class="btn btn-sm btn-outline"> <a href="{{ url_for('dashboard.project_qr_codes', project_id=project.id) }}" class="btn btn-sm btn-outline">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Clear Filters Clear Filters
</a> </a>
@@ -846,7 +846,7 @@
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
</button> </button>
<a href="{{ url_for('edit_qr_code', qr_id=qr.id) }}" <a href="{{ url_for('qr_codes.edit_qr_code', qr_id=qr.id) }}"
class="qr-action-btn edit" class="qr-action-btn edit"
onclick="event.stopPropagation()" onclick="event.stopPropagation()"
title="Edit QR Code"> title="Edit QR Code">
@@ -876,7 +876,7 @@
{% if search_name and search_status %} or {% endif %} {% if search_name and search_status %} or {% endif %}
{% if search_status %}try changing the status filter{% endif %}. {% if search_status %}try changing the status filter{% endif %}.
</p> </p>
<a href="{{ url_for('project_qr_codes', project_id=project.id) }}" class="btn btn-primary" style="margin-top: var(--spacing-4)"> <a href="{{ url_for('dashboard.project_qr_codes', project_id=project.id) }}" class="btn btn-primary" style="margin-top: var(--spacing-4)">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Clear Filters Clear Filters
</a> </a>
@@ -885,7 +885,7 @@
<h3>No QR Codes Found</h3> <h3>No QR Codes Found</h3>
<p>This project doesn't have any QR codes yet.</p> <p>This project doesn't have any QR codes yet.</p>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<a href="{{ url_for('create_qr_code') }}" class="btn btn-primary" style="margin-top: var(--spacing-4)"> <a href="{{ url_for('qr_codes.create_qr_code') }}" class="btn btn-primary" style="margin-top: var(--spacing-4)">
<i class="fas fa-plus"></i> Create QR Code <i class="fas fa-plus"></i> Create QR Code
</a> </a>
{% endif %} {% endif %}
+5 -5
View File
@@ -11,7 +11,7 @@
<div class="projects-header"> <div class="projects-header">
<div class="header-content"> <div class="header-content">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('dashboard') }}" class="back-button"> <a href="{{ url_for('dashboard.dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
<span>Back to Dashboard</span> <span>Back to Dashboard</span>
</a> </a>
@@ -20,7 +20,7 @@
<p>Organize and manage your QR code projects</p> <p>Organize and manage your QR code projects</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a href="{{ url_for('create_project') }}" class="btn btn-primary"> <a href="{{ url_for('projects.create_project') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
Create Project Create Project
</a> </a>
@@ -106,13 +106,13 @@
</div> </div>
<div class="project-actions"> <div class="project-actions">
<a href="{{ url_for('edit_project', project_id=project.id) }}" <a href="{{ url_for('projects.edit_project', project_id=project.id) }}"
class="btn btn-secondary"> class="btn btn-secondary">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
Edit Edit
</a> </a>
<!-- Activate/Deactivate button (for future use) <!-- Activate/Deactivate button (for future use)
<form method="POST" action="{{ url_for('toggle_project', project_id=project.id) }}" <form method="POST" action="{{ url_for('projects.toggle_project', project_id=project.id) }}"
style="display: inline;"> style="display: inline;">
<button type="submit" <button type="submit"
class="btn {% if project.active_status %}btn-warning{% else %}btn-success{% endif %}"> class="btn {% if project.active_status %}btn-warning{% else %}btn-success{% endif %}">
@@ -132,7 +132,7 @@
</div> </div>
<h3>No Projects Yet</h3> <h3>No Projects Yet</h3>
<p>Create your first project to organize your QR codes</p> <p>Create your first project to organize your QR codes</p>
<a href="{{ url_for('create_project') }}" class="btn btn-primary"> <a href="{{ url_for('projects.create_project') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
Create First Project Create First Project
</a> </a>
+2 -2
View File
@@ -16,12 +16,12 @@ Management{% endblock %} {% block content %}
</div> </div>
<div class="error-actions"> <div class="error-actions">
{% if session.user_id %} {% if session.user_id %}
<a href="{{ url_for('dashboard') }}" class="btn btn-primary"> <a href="{{ url_for('dashboard.dashboard') }}" class="btn btn-primary">
<i class="fas fa-home"></i> <i class="fas fa-home"></i>
Go to Dashboard Go to Dashboard
</a> </a>
{% else %} {% else %}
<a href="{{ url_for('login') }}" class="btn btn-primary"> <a href="{{ url_for('auth.login') }}" class="btn btn-primary">
<i class="fas fa-sign-in-alt"></i> <i class="fas fa-sign-in-alt"></i>
Login Login
</a> </a>
+1 -1
View File
@@ -73,7 +73,7 @@ endblock %} {% block content %}
<div class="auth-footer"> <div class="auth-footer">
<p> <p>
Already have an account? Already have an account?
<a href="{{ url_for('login') }}" class="auth-link">Sign in here</a> <a href="{{ url_for('auth.login') }}" class="auth-link">Sign in here</a>
</p> </p>
</div> </div>
</div> </div>
+2 -2
View File
@@ -39,7 +39,7 @@
</h3> </h3>
</div> </div>
<div class="filters-form"> <div class="filters-form">
<form method="GET" action="{{ url_for('qr_statistics') }}" id="statisticsFilters"> <form method="GET" action="{{ url_for('statistics.qr_statistics') }}" id="statisticsFilters">
<div class="filter-row"> <div class="filter-row">
<!-- Date Range --> <!-- Date Range -->
<div class="filter-group"> <div class="filter-group">
@@ -106,7 +106,7 @@
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
Apply Filters Apply Filters
</button> </button>
<a href="{{ url_for('qr_statistics') }}" class="btn btn-outline"> <a href="{{ url_for('statistics.qr_statistics') }}" class="btn btn-outline">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Clear Filters Clear Filters
</a> </a>
+2 -2
View File
@@ -93,7 +93,7 @@
<!-- Header --> <!-- Header -->
<div class="time-attendance-header"> <div class="time-attendance-header">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('time_attendance_dashboard') }}" class="back-button"> <a href="{{ url_for('time_attendance.time_attendance_dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Dashboard Back to Dashboard
</a> </a>
@@ -110,7 +110,7 @@
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a href="{{ url_for('time_attendance_records', import_batch=batch_summary.batch_id) }}" <a href="{{ url_for('time_attendance.time_attendance_records', import_batch=batch_summary.batch_id) }}"
class="btn btn-primary"> class="btn btn-primary">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
View All Records View All Records
+8 -8
View File
@@ -19,12 +19,12 @@
<div class="header-actions"> <div class="header-actions">
{% if session.role in ['admin', 'payroll', 'accounting'] %} {% if session.role in ['admin', 'payroll', 'accounting'] %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary"> <a href="{{ url_for('time_attendance.import_time_attendance') }}" class="btn btn-primary">
<i class="fas fa-upload"></i> <i class="fas fa-upload"></i>
Import Excel Data Import Excel Data
</a> </a>
{% endif %} {% endif %}
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-secondary"> <a href="{{ url_for('time_attendance.time_attendance_records') }}" class="btn btn-secondary">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
View All Records View All Records
</a> </a>
@@ -201,7 +201,7 @@
<span>Showing {{ recent_records|length }} of {{ "{:,}".format(total_records) }} total records</span> <span>Showing {{ recent_records|length }} of {{ "{:,}".format(total_records) }} total records</span>
</div> </div>
<div class="table-actions"> <div class="table-actions">
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-primary"> <a href="{{ url_for('time_attendance.time_attendance_records') }}" class="btn btn-primary">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
View All Records View All Records
</a> </a>
@@ -257,11 +257,11 @@
</td> </td>
<td> <td>
<div class="record-actions"> <div class="record-actions">
<a href="{{ url_for('time_attendance_records', import_batch=import_batch.import_batch_id) }}" <a href="{{ url_for('time_attendance.time_attendance_records', import_batch=import_batch.import_batch_id) }}"
class="action-btn btn-view" title="View Records"> class="action-btn btn-view" title="View Records">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{{ url_for('view_import_batch', batch_id=import_batch.import_batch_id) }}" <a href="{{ url_for('time_attendance.view_import_batch', batch_id=import_batch.import_batch_id) }}"
class="action-btn btn-info" title="Batch Details"> class="action-btn btn-info" title="Batch Details">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
</a> </a>
@@ -294,7 +294,7 @@
</p> </p>
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<div class="empty-actions"> <div class="empty-actions">
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary"> <a href="{{ url_for('time_attendance.import_time_attendance') }}" class="btn btn-primary">
<i class="fas fa-upload"></i> <i class="fas fa-upload"></i>
Import Your First File Import Your First File
</a> </a>
@@ -510,7 +510,7 @@
<script> <script>
// View record details // View record details
function viewRecord(recordId) { function viewRecord(recordId) {
window.location.href = "{{ url_for('time_attendance_record_detail', record_id=0) }}".replace('0', recordId); window.location.href = "{{ url_for('time_attendance.time_attendance_record_detail', record_id=0) }}".replace('0', recordId);
} }
// Delete record with confirmation // Delete record with confirmation
@@ -518,7 +518,7 @@ function deleteRecord(recordId, employeeName, date) {
if (confirm(`Are you sure you want to delete the attendance record for ${employeeName} on ${date}?`)) { if (confirm(`Are you sure you want to delete the attendance record for ${employeeName} on ${date}?`)) {
const form = document.createElement('form'); const form = document.createElement('form');
form.method = 'POST'; form.method = 'POST';
form.action = "{{ url_for('delete_time_attendance_record', record_id=0) }}".replace('0', recordId); form.action = "{{ url_for('time_attendance.delete_time_attendance_record', record_id=0) }}".replace('0', recordId);
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();
} }
@@ -315,7 +315,7 @@
<!-- Page Header --> <!-- Page Header -->
<div class="time-attendance-header"> <div class="time-attendance-header">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('import_time_attendance') }}" class="back-button"> <a href="{{ url_for('time_attendance.import_time_attendance') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Import Back to Import
</a> </a>
@@ -366,7 +366,7 @@
<!-- Duplicates List --> <!-- Duplicates List -->
{% if analysis.duplicate_records > 0 %} {% if analysis.duplicate_records > 0 %}
<form id="duplicateReviewForm" method="POST" action="{{ url_for('import_time_attendance') }}"> <form id="duplicateReviewForm" method="POST" action="{{ url_for('time_attendance.import_time_attendance') }}">
<input type="hidden" name="analyze_duplicates" value="false"> <input type="hidden" name="analyze_duplicates" value="false">
<input type="hidden" name="skip_duplicates" value="true"> <input type="hidden" name="skip_duplicates" value="true">
<input type="hidden" name="import_source" value="Import with Duplicates - {{ filename }}"> <input type="hidden" name="import_source" value="Import with Duplicates - {{ filename }}">
@@ -522,7 +522,7 @@
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="action-buttons"> <div class="action-buttons">
<div class="left-actions"> <div class="left-actions">
<a href="{{ url_for('cancel_pending_import') }}" class="btn btn-secondary"> <a href="{{ url_for('time_attendance.cancel_pending_import') }}" class="btn btn-secondary">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Cancel Import Cancel Import
</a> </a>
@@ -549,7 +549,7 @@
<i class="fas fa-check-circle"></i> <i class="fas fa-check-circle"></i>
<h3>No Duplicates Found</h3> <h3>No Duplicates Found</h3>
<p>All records in the file are unique. You can proceed with the import.</p> <p>All records in the file are unique. You can proceed with the import.</p>
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary" style="margin-top: 1rem;"> <a href="{{ url_for('time_attendance.import_time_attendance') }}" class="btn btn-primary" style="margin-top: 1rem;">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Import Back to Import
</a> </a>
+8 -8
View File
@@ -309,7 +309,7 @@
<!-- Page Header --> <!-- Page Header -->
<div class="time-attendance-header"> <div class="time-attendance-header">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('time_attendance_dashboard') }}" class="back-button"> <a href="{{ url_for('time_attendance.time_attendance_dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Time Attendance Back to Time Attendance
</a> </a>
@@ -326,7 +326,7 @@
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a href="{{ url_for('download_import_template') }}" class="btn btn-secondary"> <a href="{{ url_for('time_attendance.download_import_template') }}" class="btn btn-secondary">
<i class="fas fa-download"></i> <i class="fas fa-download"></i>
Download Template Download Template
</a> </a>
@@ -342,7 +342,7 @@
</h2> </h2>
</div> </div>
<div class="import-body"> <div class="import-body">
<form id="importForm" method="POST" enctype="multipart/form-data" action="{{ url_for('import_time_attendance') }}"> <form id="importForm" method="POST" enctype="multipart/form-data" action="{{ url_for('time_attendance.import_time_attendance') }}">
<!-- Project Selection - REQUIRED (moved to top) --> <!-- Project Selection - REQUIRED (moved to top) -->
<div class="form-group" style="margin-bottom: 1.5rem;"> <div class="form-group" style="margin-bottom: 1.5rem;">
@@ -707,12 +707,12 @@
<strong>Batch ID:</strong> <code>{{ import_result.batch_id }}</code> <strong>Batch ID:</strong> <code>{{ import_result.batch_id }}</code>
</p> </p>
<div class="action-buttons"> <div class="action-buttons">
<a href="{{ url_for('time_attendance_records', import_batch=import_result.batch_id) }}" <a href="{{ url_for('time_attendance.time_attendance_records', import_batch=import_result.batch_id) }}"
class="btn btn-primary"> class="btn btn-primary">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
View Imported Records View Imported Records
</a> </a>
<a href="{{ url_for('time_attendance_dashboard') }}" class="btn btn-secondary"> <a href="{{ url_for('time_attendance.time_attendance_dashboard') }}" class="btn btn-secondary">
<i class="fas fa-tachometer-alt"></i> <i class="fas fa-tachometer-alt"></i>
Back to Dashboard Back to Dashboard
</a> </a>
@@ -883,7 +883,7 @@ importForm.addEventListener('submit', async (e) => {
// ── Step 1: POST the file to /start → get job_id ────────────────────── // ── Step 1: POST the file to /start → get job_id ──────────────────────
let jobId; let jobId;
try { try {
const resp = await fetch('{{ url_for("start_import_job") }}', { const resp = await fetch('{{ url_for("time_attendance.start_import_job") }}', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
@@ -905,7 +905,7 @@ importForm.addEventListener('submit', async (e) => {
progressMessage.textContent = 'File received. Starting import...'; progressMessage.textContent = 'File received. Starting import...';
// ── Step 2: Open SSE stream for this job ────────────────────────────── // ── Step 2: Open SSE stream for this job ──────────────────────────────
const streamUrl = '{{ url_for("stream_import_progress", job_id="__JOB_ID__") }}'.replace('__JOB_ID__', jobId); const streamUrl = '{{ url_for("time_attendance.stream_import_progress", job_id="__JOB_ID__") }}'.replace('__JOB_ID__', jobId);
const evtSource = new EventSource(streamUrl); const evtSource = new EventSource(streamUrl);
evtSource.onmessage = (event) => { evtSource.onmessage = (event) => {
@@ -942,7 +942,7 @@ importForm.addEventListener('submit', async (e) => {
// Redirect to records view after a short delay // Redirect to records view after a short delay
if (result.success && result.batch_id) { if (result.success && result.batch_id) {
setTimeout(() => { setTimeout(() => {
window.location.href = '{{ url_for("time_attendance_records") }}?import_batch=' + result.batch_id; window.location.href = '{{ url_for("time_attendance.time_attendance_records") }}?import_batch=' + result.batch_id;
}, 2500); }, 2500);
} else { } else {
submitBtn.disabled = false; submitBtn.disabled = false;
@@ -204,11 +204,11 @@
Import Completed Successfully! Import Completed Successfully!
</h2> </h2>
<div class="completion-actions"> <div class="completion-actions">
<a href="{{ url_for('time_attendance_dashboard') }}" class="btn btn-primary"> <a href="{{ url_for('time_attendance.time_attendance_dashboard') }}" class="btn btn-primary">
<i class="fas fa-chart-line"></i> <i class="fas fa-chart-line"></i>
View Dashboard View Dashboard
</a> </a>
<a href="{{ url_for('time_attendance_records') }}" class="btn btn-secondary"> <a href="{{ url_for('time_attendance.time_attendance_records') }}" class="btn btn-secondary">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
View Records View Records
</a> </a>
@@ -299,7 +299,7 @@ eventSource.onerror = function(error) {
// Try to check if import completed // Try to check if import completed
setTimeout(() => { setTimeout(() => {
window.location.href = "{{ url_for('time_attendance_dashboard') }}"; window.location.href = "{{ url_for('time_attendance.time_attendance_dashboard') }}";
}, 2000); }, 2000);
}; };
</script> </script>
+7 -7
View File
@@ -12,7 +12,7 @@
<!-- Page Header --> <!-- Page Header -->
<div class="time-attendance-header"> <div class="time-attendance-header">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('import_time_attendance') }}" class="back-button"> <a href="{{ url_for('time_attendance.import_time_attendance') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Import Back to Import
</a> </a>
@@ -39,13 +39,13 @@
<div class="header-actions"> <div class="header-actions">
{% if import_result.success %} {% if import_result.success %}
<a href="{{ url_for('time_attendance_records', import_batch=import_result.batch_id) }}" <a href="{{ url_for('time_attendance.time_attendance_records', import_batch=import_result.batch_id) }}"
class="btn btn-primary"> class="btn btn-primary">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
View Imported Records View Imported Records
</a> </a>
{% endif %} {% endif %}
<a href="{{ url_for('time_attendance_dashboard') }}" class="btn btn-secondary"> <a href="{{ url_for('time_attendance.time_attendance_dashboard') }}" class="btn btn-secondary">
<i class="fas fa-tachometer-alt"></i> <i class="fas fa-tachometer-alt"></i>
Dashboard Dashboard
</a> </a>
@@ -157,7 +157,7 @@
What's Next? What's Next?
</h3> </h3>
<div class="action-grid"> <div class="action-grid">
<a href="{{ url_for('time_attendance_records', import_batch=import_result.batch_id) }}" <a href="{{ url_for('time_attendance.time_attendance_records', import_batch=import_result.batch_id) }}"
class="action-card"> class="action-card">
<div class="action-icon"> <div class="action-icon">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
@@ -168,7 +168,7 @@
</div> </div>
</a> </a>
<a href="{{ url_for('time_attendance_records') }}" <a href="{{ url_for('time_attendance.time_attendance_records') }}"
class="action-card"> class="action-card">
<div class="action-icon"> <div class="action-icon">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
@@ -179,7 +179,7 @@
</div> </div>
</a> </a>
<a href="{{ url_for('import_time_attendance') }}" <a href="{{ url_for('time_attendance.import_time_attendance') }}"
class="action-card"> class="action-card">
<div class="action-icon"> <div class="action-icon">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
@@ -231,7 +231,7 @@
</div> </div>
<div class="retry-buttons"> <div class="retry-buttons">
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary"> <a href="{{ url_for('time_attendance.import_time_attendance') }}" class="btn btn-primary">
<i class="fas fa-upload"></i> <i class="fas fa-upload"></i>
Try Import Again Try Import Again
</a> </a>
@@ -263,7 +263,7 @@
<!-- Page Header --> <!-- Page Header -->
<div class="time-attendance-header"> <div class="time-attendance-header">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('import_time_attendance') }}" class="back-button"> <a href="{{ url_for('time_attendance.import_time_attendance') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Import Back to Import
</a> </a>
@@ -320,7 +320,7 @@
<!-- Invalid Rows List --> <!-- Invalid Rows List -->
{% if analysis.invalid_rows > 0 %} {% if analysis.invalid_rows > 0 %}
<form id="invalidReviewForm" method="POST" action="{{ url_for('import_time_attendance') }}"> <form id="invalidReviewForm" method="POST" action="{{ url_for('time_attendance.import_time_attendance') }}">
<input type="hidden" name="from_invalid_review" value="true"> <input type="hidden" name="from_invalid_review" value="true">
<input type="hidden" name="analyze_invalid" value="false"> <input type="hidden" name="analyze_invalid" value="false">
<input type="hidden" name="analyze_duplicates" value="false"> <input type="hidden" name="analyze_duplicates" value="false">
@@ -434,7 +434,7 @@
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="action-buttons"> <div class="action-buttons">
<div class="left-actions"> <div class="left-actions">
<a href="{{ url_for('cancel_pending_import') }}" class="btn btn-secondary"> <a href="{{ url_for('time_attendance.cancel_pending_import') }}" class="btn btn-secondary">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Cancel Import Cancel Import
</a> </a>
@@ -452,7 +452,7 @@
<i class="fas fa-check-circle"></i> <i class="fas fa-check-circle"></i>
<h3>All rows are valid!</h3> <h3>All rows are valid!</h3>
<p>No invalid rows found. You can proceed with the import.</p> <p>No invalid rows found. You can proceed with the import.</p>
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary" style="margin-top: 1rem;"> <a href="{{ url_for('time_attendance.import_time_attendance') }}" class="btn btn-primary" style="margin-top: 1rem;">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Import Back to Import
</a> </a>
+5 -5
View File
@@ -12,7 +12,7 @@
<!-- Page Header --> <!-- Page Header -->
<div class="time-attendance-header"> <div class="time-attendance-header">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('time_attendance_records') }}" class="back-button"> <a href="{{ url_for('time_attendance.time_attendance_records') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Records Back to Records
</a> </a>
@@ -239,7 +239,7 @@
</div> </div>
<div class="section-body"> <div class="section-body">
<div class="action-grid"> <div class="action-grid">
<a href="{{ url_for('time_attendance_records', employee_id=record.employee_id) }}" <a href="{{ url_for('time_attendance.time_attendance_records', employee_id=record.employee_id) }}"
class="action-card"> class="action-card">
<div class="action-icon"> <div class="action-icon">
<i class="fas fa-user-clock"></i> <i class="fas fa-user-clock"></i>
@@ -250,7 +250,7 @@
</div> </div>
</a> </a>
<a href="{{ url_for('time_attendance_records', location_name=record.location_name) }}" <a href="{{ url_for('time_attendance.time_attendance_records', location_name=record.location_name) }}"
class="action-card"> class="action-card">
<div class="action-icon"> <div class="action-icon">
<i class="fas fa-map-marker-alt"></i> <i class="fas fa-map-marker-alt"></i>
@@ -262,7 +262,7 @@
</a> </a>
{% if record.import_batch_id %} {% if record.import_batch_id %}
<a href="{{ url_for('view_import_batch', batch_id=record.import_batch_id) }}" <a href="{{ url_for('time_attendance.view_import_batch', batch_id=record.import_batch_id) }}"
class="action-card"> class="action-card">
<div class="action-icon"> <div class="action-icon">
<i class="fas fa-file-import"></i> <i class="fas fa-file-import"></i>
@@ -854,7 +854,7 @@ function submitDelete() {
// Create form // Create form
const form = document.createElement('form'); const form = document.createElement('form');
form.method = 'POST'; form.method = 'POST';
form.action = "{{ url_for('delete_time_attendance_record', record_id=record.id) }}"; form.action = "{{ url_for('time_attendance.delete_time_attendance_record', record_id=record.id) }}";
// Add all URL parameters as hidden form inputs // Add all URL parameters as hidden form inputs
for (const [key, value] of params.entries()) { for (const [key, value] of params.entries()) {
+8 -8
View File
@@ -244,7 +244,7 @@
<!-- Page Header --> <!-- Page Header -->
<div class="time-attendance-header"> <div class="time-attendance-header">
<div class="header-navigation"> <div class="header-navigation">
<a href="{{ url_for('time_attendance_dashboard') }}" class="back-button"> <a href="{{ url_for('time_attendance.time_attendance_dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Dashboard Back to Dashboard
</a> </a>
@@ -262,7 +262,7 @@
<div class="header-actions"> <div class="header-actions">
{% if session.role == 'admin' %} {% if session.role == 'admin' %}
<a href="{{ url_for('import_time_attendance') }}" class="btn btn-primary"> <a href="{{ url_for('time_attendance.import_time_attendance') }}" class="btn btn-primary">
<i class="fas fa-upload"></i> <i class="fas fa-upload"></i>
Import Data Import Data
</a> </a>
@@ -288,7 +288,7 @@
</h3> </h3>
</div> </div>
<div class="filters-form"> <div class="filters-form">
<form method="GET" action="{{ url_for('time_attendance_records') }}"> <form method="GET" action="{{ url_for('time_attendance.time_attendance_records') }}">
<div class="filter-row"> <div class="filter-row">
<div class="filter-group"> <div class="filter-group">
@@ -499,7 +499,7 @@
<!-- Actions --> <!-- Actions -->
<td class="actions-cell"> <td class="actions-cell">
<div class="action-buttons"> <div class="action-buttons">
<a href="{{ url_for('time_attendance_record_detail', record_id=record.id, **request.args) }}" <a href="{{ url_for('time_attendance.time_attendance_record_detail', record_id=record.id, **request.args) }}"
class="action-btn view"> class="action-btn view">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
View View
@@ -527,7 +527,7 @@
</div> </div>
<div class="pagination-controls"> <div class="pagination-controls">
{% if records.has_prev %} {% if records.has_prev %}
<a href="{{ url_for('time_attendance_records', page=records.prev_num, <a href="{{ url_for('time_attendance.time_attendance_records', page=records.prev_num,
employee_id=request.args.get('employee_id', ''), employee_id=request.args.get('employee_id', ''),
location_name=request.args.get('location_name', ''), location_name=request.args.get('location_name', ''),
project_id=request.args.get('project_id', ''), project_id=request.args.get('project_id', ''),
@@ -542,7 +542,7 @@
{% for page_num in records.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} {% for page_num in records.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
{% if page_num %} {% if page_num %}
{% if page_num != records.page %} {% if page_num != records.page %}
<a href="{{ url_for('time_attendance_records', page=page_num, <a href="{{ url_for('time_attendance.time_attendance_records', page=page_num,
employee_id=request.args.get('employee_id', ''), employee_id=request.args.get('employee_id', ''),
location_name=request.args.get('location_name', ''), location_name=request.args.get('location_name', ''),
project_id=request.args.get('project_id', ''), project_id=request.args.get('project_id', ''),
@@ -560,7 +560,7 @@
{% endfor %} {% endfor %}
{% if records.has_next %} {% if records.has_next %}
<a href="{{ url_for('time_attendance_records', page=records.next_num, <a href="{{ url_for('time_attendance.time_attendance_records', page=records.next_num,
employee_id=request.args.get('employee_id', ''), employee_id=request.args.get('employee_id', ''),
location_name=request.args.get('location_name', ''), location_name=request.args.get('location_name', ''),
project_id=request.args.get('project_id', ''), project_id=request.args.get('project_id', ''),
@@ -905,7 +905,7 @@ document.addEventListener('keydown', function(event) {
chipsWrapper.closest('form').submit(); chipsWrapper.closest('form').submit();
}; };
window.taClearAllFilters = function () { window.taClearAllFilters = function () {
window.location.href = '{{ url_for("time_attendance_records") }}'; window.location.href = '{{ url_for("time_attendance.time_attendance_records") }}';
}; };
}()); }());
+5 -5
View File
@@ -17,7 +17,7 @@ Code Management{% endblock %} {% block extra_head %}
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a href="{{ url_for('create_user') }}" class="btn btn-primary"> <a href="{{ url_for('users.create_user') }}" class="btn btn-primary">
<i class="fas fa-user-plus"></i> <i class="fas fa-user-plus"></i>
Add New User Add New User
</a> </a>
@@ -280,7 +280,7 @@ Code Management{% endblock %} {% block extra_head %}
<div class="action-buttons"> <div class="action-buttons">
<!-- Edit Button --> <!-- Edit Button -->
<a <a
href="{{ url_for('edit_user', user_id=user.id) }}" href="{{ url_for('users.edit_user', user_id=user.id) }}"
class="btn btn-sm btn-primary" class="btn btn-sm btn-primary"
title="Edit User" title="Edit User"
> >
@@ -290,7 +290,7 @@ Code Management{% endblock %} {% block extra_head %}
{% if user.role != 'admin' %} {% if user.role != 'admin' %}
<!-- Promote to Admin --> <!-- Promote to Admin -->
<a <a
href="{{ url_for('promote_user', user_id=user.id) }}" href="{{ url_for('users.promote_user', user_id=user.id) }}"
class="btn btn-sm btn-success" class="btn btn-sm btn-success"
title="Promote to Admin" title="Promote to Admin"
onclick="return confirm('Are you sure you want to promote {{ user.full_name }} to admin?')" onclick="return confirm('Are you sure you want to promote {{ user.full_name }} to admin?')"
@@ -303,7 +303,7 @@ Code Management{% endblock %} {% block extra_head %}
'admin')|selectattr('active_status', 'equalto', True)|list|length 'admin')|selectattr('active_status', 'equalto', True)|list|length
> 1 %} > 1 %}
<a <a
href="{{ url_for('demote_user', user_id=user.id) }}" href="{{ url_for('users.demote_user', user_id=user.id) }}"
class="btn btn-sm btn-warning" class="btn btn-sm btn-warning"
title="Demote from Admin" title="Demote from Admin"
onclick="return confirm('Are you sure you want to demote {{ user.full_name }} from admin?')" onclick="return confirm('Are you sure you want to demote {{ user.full_name }} from admin?')"
@@ -353,7 +353,7 @@ Code Management{% endblock %} {% block extra_head %}
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
<h3>No Users Found</h3> <h3>No Users Found</h3>
<p>There are currently no users in the system.</p> <p>There are currently no users in the system.</p>
<a href="{{ url_for('create_user') }}" class="btn btn-primary"> <a href="{{ url_for('users.create_user') }}" class="btn btn-primary">
<i class="fas fa-user-plus"></i> <i class="fas fa-user-plus"></i>
Create First User Create First User
</a> </a>
+3 -3
View File
@@ -48,7 +48,7 @@
<!-- Filters Section --> <!-- Filters Section -->
<div class="filter-section"> <div class="filter-section">
<form method="GET" action="{{ url_for('verification_review') }}"> <form method="GET" action="{{ url_for('attendance.verification_review') }}">
<div class="filter-grid"> <div class="filter-grid">
<div class="filter-group"> <div class="filter-group">
<label for="status">Status</label> <label for="status">Status</label>
@@ -104,7 +104,7 @@
<button type="submit" class="btn-filter"> <button type="submit" class="btn-filter">
<i class="fas fa-filter"></i> Apply Filters <i class="fas fa-filter"></i> Apply Filters
</button> </button>
<a href="{{ url_for('verification_review') }}" class="btn-reset"> <a href="{{ url_for('attendance.verification_review') }}" class="btn-reset">
<i class="fas fa-undo"></i> Reset <i class="fas fa-undo"></i> Reset
</a> </a>
</div> </div>
@@ -193,7 +193,7 @@
<button type="button" class="btn-photo email" onclick="sendByEmail({{ record.id }}, '{{ record.employee_id }}', '{{ employee_names.get(record.employee_id) or 'Unknown' }}', '{{ record.check_in_date.strftime('%b %d, %Y') }}', '{{ record.check_in_time.strftime('%I:%M %p') }}', '{{ record.location_name }}', '{{ record.qr_code.location_event if record.qr_code and record.qr_code.location_event else 'N/A' }}', '{{ record.verification_status }}', '{{ record.qr_code.location_address if record.qr_code and record.qr_code.location_address else 'N/A' }}', '{{ record.address or 'N/A' }}', '{{ '%.3f'|format(record.location_accuracy) if record.location_accuracy else 'N/A' }}', '{{ record.device_info or 'Unknown' }}')" title="Send by Email"> <button type="button" class="btn-photo email" onclick="sendByEmail({{ record.id }}, '{{ record.employee_id }}', '{{ employee_names.get(record.employee_id) or 'Unknown' }}', '{{ record.check_in_date.strftime('%b %d, %Y') }}', '{{ record.check_in_time.strftime('%I:%M %p') }}', '{{ record.location_name }}', '{{ record.qr_code.location_event if record.qr_code and record.qr_code.location_event else 'N/A' }}', '{{ record.verification_status }}', '{{ record.qr_code.location_address if record.qr_code and record.qr_code.location_address else 'N/A' }}', '{{ record.address or 'N/A' }}', '{{ '%.3f'|format(record.location_accuracy) if record.location_accuracy else 'N/A' }}', '{{ record.device_info or 'Unknown' }}')" title="Send by Email">
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
</button> </button>
<a href="{{ url_for('verification_review_detail', record_id=record.id) }}" class="btn-photo view" title="View Details"> <a href="{{ url_for('attendance.verification_review_detail', record_id=record.id) }}" class="btn-photo view" title="View Details">
<i class="fas fa-expand"></i> <i class="fas fa-expand"></i>
</a> </a>
</div> </div>
+4 -4
View File
@@ -446,14 +446,14 @@ QR Code Management{% endblock %} {% block extra_head %}
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
Reject Verification Reject Verification
</button> </button>
<a href="{{ url_for('attendance_report') }}" class="btn btn-back"> <a href="{{ url_for('attendance.attendance_report') }}" class="btn btn-back">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Attendance Back to Attendance
</a> </a>
</div> </div>
{% else %} {% else %}
<div class="action-buttons"> <div class="action-buttons">
<a href="{{ url_for('attendance_report') }}" class="btn btn-back"> <a href="{{ url_for('attendance.attendance_report') }}" class="btn btn-back">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
Back to Attendance Back to Attendance
</a> </a>
@@ -509,7 +509,7 @@ QR Code Management{% endblock %} {% block extra_head %}
console.log(`[LOG] Successfully updated verification status to ${status}`); console.log(`[LOG] Successfully updated verification status to ${status}`);
alert(`Verification ${status} successfully!`); alert(`Verification ${status} successfully!`);
// Redirect back to attendance report // Redirect back to attendance report
window.location.href = '{{ url_for('attendance_report') }}'; window.location.href = '{{ url_for('attendance.attendance_report') }}';
} else { } else {
throw new Error(data.message || 'Failed to update verification status'); throw new Error(data.message || 'Failed to update verification status');
} }
@@ -578,7 +578,7 @@ QR Code Management{% endblock %} {% block extra_head %}
const distance = '{% if record.location_accuracy %}{{ "%.3f"|format(record.location_accuracy) }} miles{% else %}N/A{% endif %}'; const distance = '{% if record.location_accuracy %}{{ "%.3f"|format(record.location_accuracy) }} miles{% else %}N/A{% endif %}';
const deviceInfo = '{{ record.device_info or "Unknown" }}'; const deviceInfo = '{{ record.device_info or "Unknown" }}';
const recordId = '{{ record.id }}'; const recordId = '{{ record.id }}';
const recordUrl = window.location.origin + '{{ url_for("verification_review_detail", record_id=record.id) }}'; const recordUrl = window.location.origin + '{{ url_for("attendance.verification_review_detail", record_id=record.id) }}';
// Build email subject // Build email subject
const subject = encodeURIComponent(`Verification Review - Employee ${employeeId} (${employeeName}) - ${checkInDate}`); const subject = encodeURIComponent(`Verification Review - Employee ${employeeId} (${employeeName}) - ${checkInDate}`);
+1 -40
View File
@@ -1,43 +1,4 @@
# ---------------------------------------------------------------------------
# url_for compatibility shim
# ---------------------------------------------------------------------------
# Flask Blueprints prefix endpoint names (e.g. 'attendance.attendance_report').
# The original codebase uses bare names (e.g. url_for('attendance_report')).
# This wrapper resolves bare names by searching registered blueprints,
# so zero url_for() calls in routes or templates need to change.
#
# IMPORTANT: Flask's url_for is aliased as _flask_url_for to avoid shadowing
# this function. Decorators in this module that redirect (login_required etc.)
# also use _flask_url_for directly since they only redirect to known bare names
# that this shim already handles.
# ---------------------------------------------------------------------------
import flask.helpers as _flask_helpers
# Capture Flask's original url_for BEFORE any shadowing
_flask_url_for = _flask_helpers.url_for
def url_for(endpoint, **values):
"""
Drop-in replacement for flask.url_for that resolves bare endpoint names
across Blueprints. Qualified names (containing '.') pass through unchanged.
"""
from flask import current_app
if '.' in endpoint:
return _flask_url_for(endpoint, **values)
try:
return _flask_url_for(endpoint, **values)
except Exception:
pass
for bp_name in sorted(current_app.blueprints.keys()):
try:
return _flask_url_for(f'{bp_name}.{endpoint}', **values)
except Exception:
pass
return _flask_url_for(endpoint, **values) # raises Flask's normal BuildError
""" """
utils/helpers.py utils/helpers.py
================ ================
@@ -360,7 +321,7 @@ def get_employee_checkin_history(employee_id, qr_code_id, date_filter=None):
date_filter = date.today() date_filter = date.today()
# AttendanceData imported at call site to avoid circular import # AttendanceData imported at call site to avoid circular import
from flask import current_app from flask import current_app
AttendanceData = current_app.config.get('_models', {}).get('AttendanceData') from models.attendance import AttendanceData
if AttendanceData: if AttendanceData:
checkins = AttendanceData.query.filter_by( checkins = AttendanceData.query.filter_by(
employee_id=employee_id.upper(), employee_id=employee_id.upper(),