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