Apr 06 2026: enhanced - update the dynamic QR code
This commit is contained in:
@@ -56,9 +56,9 @@ def create_app() -> Flask:
|
|||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
# Unpack model classes and store on app for shared access
|
# Unpack model classes and store on app for shared access
|
||||||
(User, QRCode, QRCodeStyle, Project, AttendanceData,
|
(User, QRCode, QRCodeStyle, QRCodeLocation, Project, AttendanceData,
|
||||||
Employee, TimeAttendance, UserProjectPermission,
|
Employee, TimeAttendance, UserProjectPermission,
|
||||||
UserLocationPermission) = set_db(db)
|
UserLocationPermission) = set_db(db) # ADDED: QRCodeLocation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -14,11 +14,11 @@ def set_db(database):
|
|||||||
|
|
||||||
# Now import all models (they will use base.db)
|
# Now import all models (they will use base.db)
|
||||||
from .user import User
|
from .user import User
|
||||||
from .qrcode import QRCode, QRCodeStyle
|
from .qrcode import QRCode, QRCodeStyle, QRCodeLocation # ADDED: QRCodeLocation
|
||||||
from .project import Project
|
from .project import Project
|
||||||
from .attendance import AttendanceData
|
from .attendance import AttendanceData
|
||||||
from .employee import Employee
|
from .employee import Employee
|
||||||
from .time_attendance import TimeAttendance
|
from .time_attendance import TimeAttendance
|
||||||
from .permissions import UserProjectPermission, UserLocationPermission
|
from .permissions import UserProjectPermission, UserLocationPermission
|
||||||
|
|
||||||
return User, QRCode, QRCodeStyle, Project, AttendanceData, Employee, TimeAttendance, UserProjectPermission, UserLocationPermission
|
return User, QRCode, QRCodeStyle, QRCodeLocation, Project, AttendanceData, Employee, TimeAttendance, UserProjectPermission, UserLocationPermission
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class AttendanceData(base.db.Model):
|
|||||||
altitude = base.db.Column(base.db.Float, nullable=True)
|
altitude = base.db.Column(base.db.Float, nullable=True)
|
||||||
location_source = base.db.Column(base.db.String(50), default='manual')
|
location_source = base.db.Column(base.db.String(50), default='manual')
|
||||||
address = base.db.Column(base.db.String(500), nullable=True)
|
address = base.db.Column(base.db.String(500), nullable=True)
|
||||||
|
# Stores the QR-side address for dynamic QR check-ins (overrides qr_codes.location_address join)
|
||||||
|
qr_address = base.db.Column(base.db.Text, nullable=True)
|
||||||
verification_photo = base.db.Column(base.db.Text, nullable=True) # Base64 encoded image
|
verification_photo = base.db.Column(base.db.Text, nullable=True) # Base64 encoded image
|
||||||
verification_required = base.db.Column(base.db.Boolean, default=False)
|
verification_required = base.db.Column(base.db.Boolean, default=False)
|
||||||
verification_status = base.db.Column(base.db.String(20), nullable=True) # 'pending', 'approved', 'rejected'
|
verification_status = base.db.Column(base.db.String(20), nullable=True) # 'pending', 'approved', 'rejected'
|
||||||
|
|||||||
+41
-6
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
QRCode and QRCodeStyle Models for QR Attendance Management System
|
QRCode, QRCodeStyle, and QRCodeLocation Models for QR Attendance Management System
|
||||||
================================================================
|
===================================================================================
|
||||||
|
|
||||||
QRCode models to manage QR code records and metadata with customization options.
|
QRCode models to manage QR code records and metadata with customization options.
|
||||||
Extracted from app.py for better code organization.
|
Extracted from app.py for better code organization.
|
||||||
@@ -17,8 +17,9 @@ class QRCode(base.db.Model):
|
|||||||
|
|
||||||
id = base.db.Column(base.db.Integer, primary_key=True)
|
id = base.db.Column(base.db.Integer, primary_key=True)
|
||||||
name = base.db.Column(base.db.String(100), nullable=False)
|
name = base.db.Column(base.db.String(100), nullable=False)
|
||||||
location = base.db.Column(base.db.String(100), nullable=False)
|
# nullable=True for dynamic QR codes which have no single fixed location/address
|
||||||
location_address = base.db.Column(base.db.Text, nullable=False)
|
location = base.db.Column(base.db.String(100), nullable=True)
|
||||||
|
location_address = base.db.Column(base.db.Text, nullable=True)
|
||||||
location_event = base.db.Column(base.db.String(200), nullable=False)
|
location_event = base.db.Column(base.db.String(200), nullable=False)
|
||||||
qr_code_image = base.db.Column(base.db.Text, nullable=False) # Base64 encoded image
|
qr_code_image = base.db.Column(base.db.Text, nullable=False) # Base64 encoded image
|
||||||
created_by = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id'), nullable=True)
|
created_by = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id'), nullable=True)
|
||||||
@@ -38,7 +39,11 @@ class QRCode(base.db.Model):
|
|||||||
border = base.db.Column(base.db.Integer, default=4)
|
border = base.db.Column(base.db.Integer, default=4)
|
||||||
error_correction = base.db.Column(base.db.String(1), default='L')
|
error_correction = base.db.Column(base.db.String(1), default='L')
|
||||||
style_id = base.db.Column(base.db.Integer, base.db.ForeignKey('qr_code_styles.id'), nullable=True)
|
style_id = base.db.Column(base.db.Integer, base.db.ForeignKey('qr_code_styles.id'), nullable=True)
|
||||||
|
# --- ADDED: QR Code Type ---
|
||||||
|
# 'standard' = fixed single location (existing behavior, default)
|
||||||
|
# 'dynamic' = employee selects location from a list at scan time
|
||||||
|
qr_type = base.db.Column(base.db.String(20), nullable=False, default='standard')
|
||||||
|
|
||||||
# Relationship to style
|
# Relationship to style
|
||||||
style = base.db.relationship('QRCodeStyle', backref='qr_codes')
|
style = base.db.relationship('QRCodeStyle', backref='qr_codes')
|
||||||
|
|
||||||
@@ -78,4 +83,34 @@ class QRCodeStyle(base.db.Model):
|
|||||||
created_by = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id'))
|
created_by = base.db.Column(base.db.Integer, base.db.ForeignKey('users.id'))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<QRCodeStyle {self.name}>'
|
return f'<QRCodeStyle {self.name}>'
|
||||||
|
|
||||||
|
|
||||||
|
# --- ADDED: QRCodeLocation model ---
|
||||||
|
class QRCodeLocation(base.db.Model):
|
||||||
|
"""
|
||||||
|
Selectable locations for dynamic QR codes.
|
||||||
|
Each record represents one location option displayed to the employee at scan time.
|
||||||
|
Only relevant when the parent QRCode.qr_type == 'dynamic'.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'qr_code_locations'
|
||||||
|
|
||||||
|
id = base.db.Column(base.db.Integer, primary_key=True)
|
||||||
|
qr_code_id = base.db.Column(
|
||||||
|
base.db.Integer,
|
||||||
|
base.db.ForeignKey('qr_codes.id', ondelete='CASCADE'),
|
||||||
|
nullable=False
|
||||||
|
)
|
||||||
|
location_name = base.db.Column(base.db.String(100), nullable=False)
|
||||||
|
location_address = base.db.Column(base.db.Text, nullable=True)
|
||||||
|
address_latitude = base.db.Column(base.db.Float, nullable=True)
|
||||||
|
address_longitude = base.db.Column(base.db.Float, nullable=True)
|
||||||
|
sort_order = base.db.Column(base.db.Integer, default=0, nullable=False)
|
||||||
|
active_status = base.db.Column(base.db.Boolean, default=True, nullable=False)
|
||||||
|
created_date = base.db.Column(base.db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# Back-reference: qr_code_instance.locations → list of QRCodeLocation rows
|
||||||
|
qr_code = base.db.relationship('QRCode', backref='locations')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<QRCodeLocation "{self.location_name}" (QR #{self.qr_code_id})>'
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ def attendance_report():
|
|||||||
ad.check_in_time,
|
ad.check_in_time,
|
||||||
ad.location_name,
|
ad.location_name,
|
||||||
qc.location_event,
|
qc.location_event,
|
||||||
qc.location_address as qr_address,
|
COALESCE(ad.qr_address, qc.location_address) as qr_address,
|
||||||
ad.address as checked_in_address,
|
ad.address as checked_in_address,
|
||||||
ad.latitude,
|
ad.latitude,
|
||||||
ad.longitude,
|
ad.longitude,
|
||||||
@@ -196,7 +196,7 @@ def attendance_report():
|
|||||||
ad.check_in_time,
|
ad.check_in_time,
|
||||||
ad.location_name,
|
ad.location_name,
|
||||||
qc.location_event,
|
qc.location_event,
|
||||||
qc.location_address as qr_address,
|
COALESCE(ad.qr_address, qc.location_address) as qr_address,
|
||||||
ad.address as checked_in_address,
|
ad.address as checked_in_address,
|
||||||
ad.latitude,
|
ad.latitude,
|
||||||
ad.longitude,
|
ad.longitude,
|
||||||
|
|||||||
+214
-21
@@ -14,7 +14,7 @@ from extensions import db, logger_handler
|
|||||||
from models.attendance import AttendanceData
|
from models.attendance import AttendanceData
|
||||||
from models.employee import Employee
|
from models.employee import Employee
|
||||||
from models.project import Project
|
from models.project import Project
|
||||||
from models.qrcode import QRCode, QRCodeStyle
|
from models.qrcode import QRCode, QRCodeStyle, QRCodeLocation # ADDED: QRCodeLocation for dynamic QR
|
||||||
from models.user import User
|
from models.user import User
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from logger_handler import log_user_activity, log_database_operations
|
from logger_handler import log_user_activity, log_database_operations
|
||||||
@@ -42,6 +42,37 @@ import openpyxl
|
|||||||
bp = Blueprint('qr_codes', __name__)
|
bp = Blueprint('qr_codes', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
# --- ADDED: helper — returns distinct (location, location_address) pairs from
|
||||||
|
# all standard QR codes, used to auto-populate the dynamic QR location list ---
|
||||||
|
def get_unique_qr_locations():
|
||||||
|
"""
|
||||||
|
Query all unique (location, location_address) pairs from the qr_codes table
|
||||||
|
(standard QR codes only). Returns a list of dicts:
|
||||||
|
[{'name': str, 'address': str}, ...]
|
||||||
|
Sorted alphabetically by name, duplicates removed.
|
||||||
|
"""
|
||||||
|
rows = (
|
||||||
|
db.session.query(QRCode.location, QRCode.location_address)
|
||||||
|
.filter(
|
||||||
|
QRCode.qr_type == 'standard',
|
||||||
|
QRCode.location.isnot(None),
|
||||||
|
QRCode.location != ''
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.order_by(QRCode.location.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
seen = set()
|
||||||
|
result = []
|
||||||
|
for loc, addr in rows:
|
||||||
|
key = loc.strip().lower()
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
result.append({'name': loc.strip(), 'address': (addr or '').strip()})
|
||||||
|
return result
|
||||||
|
# --- END ADDED ---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/qr-codes/create', methods=['GET', 'POST'], endpoint='create_qr_code')
|
@bp.route('/qr-codes/create', methods=['GET', 'POST'], endpoint='create_qr_code')
|
||||||
@login_required
|
@login_required
|
||||||
@@ -52,8 +83,26 @@ def create_qr_code():
|
|||||||
try:
|
try:
|
||||||
# Existing form data
|
# Existing form data
|
||||||
name = request.form['name']
|
name = request.form['name']
|
||||||
location = request.form['location']
|
qr_type = request.form.get('qr_type', 'standard') # read type first
|
||||||
location_address = request.form['location_address']
|
|
||||||
|
# For dynamic QR codes, location/address are auto-managed (not user-entered)
|
||||||
|
if qr_type == 'dynamic':
|
||||||
|
location = 'Dynamic' # placeholder — selectable locations come from standard QR codes at scan time
|
||||||
|
location_address = '' # no single fixed address
|
||||||
|
else:
|
||||||
|
location = request.form.get('location', '').strip()
|
||||||
|
location_address = request.form.get('location_address', '').strip()
|
||||||
|
if not location:
|
||||||
|
flash('Location Name is required for Standard QR codes.', 'error')
|
||||||
|
return render_template('create_qr_code.html',
|
||||||
|
projects=Project.query.filter_by(active_status=True).all(),
|
||||||
|
styles=QRCodeStyle.query.all())
|
||||||
|
if not location_address:
|
||||||
|
flash('Address is required for Standard QR codes.', 'error')
|
||||||
|
return render_template('create_qr_code.html',
|
||||||
|
projects=Project.query.filter_by(active_status=True).all(),
|
||||||
|
styles=QRCodeStyle.query.all())
|
||||||
|
|
||||||
location_event = request.form.get('location_event', '')
|
location_event = request.form.get('location_event', '')
|
||||||
project_id = request.form.get('project_id')
|
project_id = request.form.get('project_id')
|
||||||
|
|
||||||
@@ -124,6 +173,7 @@ def create_qr_code():
|
|||||||
address_longitude=address_longitude,
|
address_longitude=address_longitude,
|
||||||
coordinate_accuracy=coordinate_accuracy if has_coordinates else None,
|
coordinate_accuracy=coordinate_accuracy if has_coordinates else None,
|
||||||
coordinates_updated_date=datetime.utcnow() if has_coordinates else None,
|
coordinates_updated_date=datetime.utcnow() if has_coordinates else None,
|
||||||
|
qr_type=qr_type, # ADDED: store QR type
|
||||||
# NEW: Customization fields (only if columns exist)
|
# NEW: Customization fields (only if columns exist)
|
||||||
**({
|
**({
|
||||||
'fill_color': fill_color,
|
'fill_color': fill_color,
|
||||||
@@ -407,8 +457,19 @@ def edit_qr_code(qr_id):
|
|||||||
# Update QR code fields
|
# Update QR code fields
|
||||||
new_name = request.form['name']
|
new_name = request.form['name']
|
||||||
qr_code.name = new_name
|
qr_code.name = new_name
|
||||||
qr_code.location = request.form['location']
|
|
||||||
qr_code.location_address = request.form['location_address']
|
# --- ADDED: for dynamic QR codes, location/address are auto-managed ---
|
||||||
|
new_qr_type = request.form.get('qr_type', 'standard')
|
||||||
|
qr_code.qr_type = new_qr_type
|
||||||
|
|
||||||
|
if new_qr_type == 'dynamic':
|
||||||
|
qr_code.location = 'Dynamic' # placeholder — selectable locations come from standard QR codes at scan time
|
||||||
|
qr_code.location_address = '' # no single fixed address
|
||||||
|
else:
|
||||||
|
qr_code.location = request.form.get('location', '').strip()
|
||||||
|
qr_code.location_address = request.form.get('location_address', '').strip()
|
||||||
|
# --- END ADDED ---
|
||||||
|
|
||||||
qr_code.location_event = request.form.get('location_event', '')
|
qr_code.location_event = request.form.get('location_event', '')
|
||||||
|
|
||||||
# Handle coordinates
|
# Handle coordinates
|
||||||
@@ -502,8 +563,8 @@ def edit_qr_code(qr_id):
|
|||||||
# GET request - render edit form
|
# GET request - render edit form
|
||||||
projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all()
|
projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all()
|
||||||
styles = QRCodeStyle.query.order_by(QRCodeStyle.name.asc()).all()
|
styles = QRCodeStyle.query.order_by(QRCodeStyle.name.asc()).all()
|
||||||
|
return render_template('edit_qr_code.html', qr_code=qr_code,
|
||||||
return render_template('edit_qr_code.html', qr_code=qr_code, projects=projects, styles=styles)
|
projects=projects, styles=styles)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
@@ -586,7 +647,25 @@ def qr_destination(qr_url):
|
|||||||
access_method='scan'
|
access_method='scan'
|
||||||
)
|
)
|
||||||
|
|
||||||
return render_template('qr_destination.html', qr_code=qr_code)
|
# Load selectable locations for dynamic QR — auto-generated from all
|
||||||
|
# active standard QR codes' unique (location, location_address) pairs.
|
||||||
|
locations = []
|
||||||
|
if getattr(qr_code, 'qr_type', 'standard') == 'dynamic':
|
||||||
|
locations = (
|
||||||
|
db.session.query(QRCode.location, QRCode.location_address)
|
||||||
|
.filter(
|
||||||
|
QRCode.qr_type == 'standard',
|
||||||
|
QRCode.active_status == True,
|
||||||
|
QRCode.location.isnot(None),
|
||||||
|
QRCode.location != '',
|
||||||
|
QRCode.location != 'Dynamic'
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.order_by(QRCode.location.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template('qr_destination.html', qr_code=qr_code, locations=locations)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger_handler.log_database_error('qr_code_scan', e)
|
logger_handler.log_database_error('qr_code_scan', e)
|
||||||
@@ -618,6 +697,53 @@ def qr_checkin(qr_url):
|
|||||||
# Get and validate employee ID
|
# Get and validate employee ID
|
||||||
employee_id = request.form.get('employee_id', '').strip()
|
employee_id = request.form.get('employee_id', '').strip()
|
||||||
|
|
||||||
|
# --- ADDED: dynamic QR — resolve effective location from the employee's selection ---
|
||||||
|
selected_location_name = request.form.get('selected_location_name', '').strip()
|
||||||
|
selected_location_address = request.form.get('selected_location_address', '').strip()
|
||||||
|
|
||||||
|
if getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and selected_location_name:
|
||||||
|
effective_location_name = selected_location_name
|
||||||
|
effective_location_address = selected_location_address or ''
|
||||||
|
|
||||||
|
# Look up the matching standard QR code so we can inherit its
|
||||||
|
# location_event, coordinates, and exact address — this ensures all
|
||||||
|
# calculations (GPS accuracy, interval messages, success labels) behave
|
||||||
|
# exactly as if the employee had scanned that standard QR directly.
|
||||||
|
matching_qr = QRCode.query.filter_by(
|
||||||
|
location=selected_location_name,
|
||||||
|
qr_type='standard',
|
||||||
|
active_status=True
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if matching_qr:
|
||||||
|
# Use the standard QR's address if the selection has none
|
||||||
|
if not effective_location_address:
|
||||||
|
effective_location_address = matching_qr.location_address or ''
|
||||||
|
effective_location_event = matching_qr.location_event or qr_code.location_event or 'Check In'
|
||||||
|
effective_address_latitude = matching_qr.address_latitude
|
||||||
|
effective_address_longitude = matching_qr.address_longitude
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"DYNAMIC check-in: employee={employee_id}, "
|
||||||
|
f"selected='{selected_location_name}', "
|
||||||
|
f"matched standard QR #{matching_qr.id} '{matching_qr.name}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# No matching standard QR — use whatever the dynamic QR has
|
||||||
|
effective_location_event = qr_code.location_event or 'Check In'
|
||||||
|
effective_address_latitude = None
|
||||||
|
effective_address_longitude = None
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"DYNAMIC check-in: employee={employee_id}, "
|
||||||
|
f"selected='{selected_location_name}', no matching standard QR found"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
effective_location_name = qr_code.location
|
||||||
|
effective_location_address = qr_code.location_address
|
||||||
|
effective_location_event = qr_code.location_event
|
||||||
|
effective_address_latitude = qr_code.address_latitude
|
||||||
|
effective_address_longitude = qr_code.address_longitude
|
||||||
|
# --- END ADDED ---
|
||||||
|
|
||||||
if not employee_id:
|
if not employee_id:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -644,17 +770,22 @@ def qr_checkin(qr_url):
|
|||||||
# Check if 30 minutes have passed since the last check-in
|
# Check if 30 minutes have passed since the last check-in
|
||||||
if recent_checkin_datetime > the_last_checkin_time:
|
if recent_checkin_datetime > the_last_checkin_time:
|
||||||
minutes_remaining = time_interval - int((current_time - recent_checkin_datetime).total_seconds() / 60)
|
minutes_remaining = time_interval - int((current_time - recent_checkin_datetime).total_seconds() / 60)
|
||||||
logger_handler.logger.info(f"Too soon for another {qr_code.location_event} for employee {employee_id}: {minutes_remaining} minutes remaining")
|
logger_handler.logger.info(f"Too soon for another {effective_location_event} for employee {employee_id}: {minutes_remaining} minutes remaining")
|
||||||
|
|
||||||
|
checkin_time_str = recent_checkin.check_in_time.strftime('%H:%M')
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f"You can {qr_code.location_event} again in {minutes_remaining} minutes. Last {qr_code.location_event} was at {recent_checkin.check_in_time.strftime("%H:%M")}. \n"
|
'message': (
|
||||||
f"Puedes volver a registrarte en {minutes_remaining} minutos. El último registro fue a las {recent_checkin.check_in_time.strftime("%H:%M")}."
|
f'You can {effective_location_event} again in {minutes_remaining} minutes. '
|
||||||
|
f'Last {effective_location_event} was at {checkin_time_str}. \n'
|
||||||
|
f'Puedes volver a registrarte en {minutes_remaining} minutos. '
|
||||||
|
f'El ultimo registro fue a las {checkin_time_str}.'
|
||||||
|
)
|
||||||
}), 400
|
}), 400
|
||||||
else:
|
else:
|
||||||
logger_handler.logger.debug(f"{time_interval}-minute interval satisfied for employee {employee_id}")
|
logger_handler.logger.debug(f"{time_interval}-minute interval satisfied for employee {employee_id}")
|
||||||
else:
|
else:
|
||||||
logger_handler.logger.debug(f"First {qr_code.location_event} today for employee {employee_id}")
|
logger_handler.logger.debug(f"First {effective_location_event} today for employee {employee_id}")
|
||||||
|
|
||||||
# Process location data with coordinate-to-address conversion
|
# Process location data with coordinate-to-address conversion
|
||||||
location_data = process_location_data_enhanced(request.form)
|
location_data = process_location_data_enhanced(request.form)
|
||||||
@@ -669,6 +800,14 @@ def qr_checkin(qr_url):
|
|||||||
# Create attendance record
|
# Create attendance record
|
||||||
logger_handler.logger.debug("Creating attendance record")
|
logger_handler.logger.debug("Creating attendance record")
|
||||||
|
|
||||||
|
# For dynamic QR: append tag to location_name so reports distinguish the source
|
||||||
|
is_dynamic = getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and bool(selected_location_name)
|
||||||
|
record_location_name = (
|
||||||
|
f"{effective_location_name} (Dynamic QR)" if is_dynamic else effective_location_name
|
||||||
|
)
|
||||||
|
# For dynamic QR: store the selected location's address as the QR-side address
|
||||||
|
record_qr_address = effective_location_address if is_dynamic else None
|
||||||
|
|
||||||
attendance = AttendanceData(
|
attendance = AttendanceData(
|
||||||
qr_code_id=qr_code.id,
|
qr_code_id=qr_code.id,
|
||||||
employee_id=employee_id.upper(),
|
employee_id=employee_id.upper(),
|
||||||
@@ -677,7 +816,8 @@ def qr_checkin(qr_url):
|
|||||||
device_info=device_info,
|
device_info=device_info,
|
||||||
user_agent=user_agent_string,
|
user_agent=user_agent_string,
|
||||||
ip_address=client_ip,
|
ip_address=client_ip,
|
||||||
location_name=qr_code.location,
|
location_name=record_location_name,
|
||||||
|
qr_address=record_qr_address,
|
||||||
latitude=location_data['latitude'],
|
latitude=location_data['latitude'],
|
||||||
longitude=location_data['longitude'],
|
longitude=location_data['longitude'],
|
||||||
accuracy=location_data['accuracy'],
|
accuracy=location_data['accuracy'],
|
||||||
@@ -702,7 +842,8 @@ def qr_checkin(qr_url):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if we have the required data
|
# Check if we have the required data
|
||||||
if not qr_code.location_address:
|
# CHANGED: use effective_location_address (respects dynamic QR selection)
|
||||||
|
if not effective_location_address:
|
||||||
logger_handler.logger.warning(f"QR code location_address is empty or None for QR ID: {qr_code.id}")
|
logger_handler.logger.warning(f"QR code location_address is empty or None for QR ID: {qr_code.id}")
|
||||||
elif not location_data['address'] and not (location_data['latitude'] and location_data['longitude']):
|
elif not location_data['address'] and not (location_data['latitude'] and location_data['longitude']):
|
||||||
logger_handler.logger.warning(
|
logger_handler.logger.warning(
|
||||||
@@ -714,7 +855,7 @@ def qr_checkin(qr_url):
|
|||||||
logger_handler.logger.debug("Required location data available, proceeding with accuracy calculation")
|
logger_handler.logger.debug("Required location data available, proceeding with accuracy calculation")
|
||||||
|
|
||||||
location_accuracy = calculate_location_accuracy_enhanced(
|
location_accuracy = calculate_location_accuracy_enhanced(
|
||||||
qr_address=qr_code.location_address,
|
qr_address=effective_location_address, # CHANGED: dynamic QR uses selected location address
|
||||||
checkin_address=location_data['address'],
|
checkin_address=location_data['address'],
|
||||||
checkin_lat=location_data['latitude'],
|
checkin_lat=location_data['latitude'],
|
||||||
checkin_lng=location_data['longitude']
|
checkin_lng=location_data['longitude']
|
||||||
@@ -808,7 +949,7 @@ def qr_checkin(qr_url):
|
|||||||
check_in_date=today
|
check_in_date=today
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
checkin_sequence_text = f"{qr_code.location_event} details"
|
checkin_sequence_text = f"{effective_location_event} details"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger_handler.logger.error(f"Database error saving attendance record: {e}", exc_info=True)
|
logger_handler.logger.error(f"Database error saving attendance record: {e}", exc_info=True)
|
||||||
@@ -825,11 +966,11 @@ def qr_checkin(qr_url):
|
|||||||
'message': f'Check-in successful! {checkin_sequence_text} for today.',
|
'message': f'Check-in successful! {checkin_sequence_text} for today.',
|
||||||
'data': {
|
'data': {
|
||||||
'employee_id': attendance.employee_id,
|
'employee_id': attendance.employee_id,
|
||||||
'location': qr_code.location_address,
|
'location': effective_location_name, # CHANGED: use selected location name
|
||||||
'location_event': qr_code.location_event,
|
'location_event': effective_location_event, # CHANGED: use resolved event
|
||||||
'event': qr_code.location_event, # Add both for compatibility
|
'event': effective_location_event, # CHANGED: use resolved event
|
||||||
'check_in_time': attendance.check_in_time.strftime('%I:%M %p'), # 12-hour format
|
'check_in_time': attendance.check_in_time.strftime('%I:%M %p'),
|
||||||
'check_in_date': attendance.check_in_date.strftime('%B %d, %Y'), # Full date format
|
'check_in_date': attendance.check_in_date.strftime('%B %d, %Y'),
|
||||||
'device_info': attendance.device_info,
|
'device_info': attendance.device_info,
|
||||||
'ip_address': attendance.ip_address,
|
'ip_address': attendance.ip_address,
|
||||||
'location_accuracy': location_accuracy,
|
'location_accuracy': location_accuracy,
|
||||||
@@ -865,6 +1006,58 @@ def qr_checkin(qr_url):
|
|||||||
'message': 'An unexpected error occurred during check-in.'
|
'message': 'An unexpected error occurred during check-in.'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
# --- ADDED: API endpoint returning selectable locations for a dynamic QR code ---
|
||||||
|
@bp.route('/qr/<string:qr_url>/locations', methods=['GET'], endpoint='qr_get_locations')
|
||||||
|
def qr_get_locations(qr_url):
|
||||||
|
"""
|
||||||
|
Return JSON list of active selectable locations for a dynamic QR code.
|
||||||
|
Used by the scan page to populate the location selector.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
qr_code = QRCode.query.filter_by(qr_url=qr_url, active_status=True).first()
|
||||||
|
if not qr_code:
|
||||||
|
return jsonify({'success': False, 'message': 'QR code not found or inactive.'}), 404
|
||||||
|
|
||||||
|
if getattr(qr_code, 'qr_type', 'standard') != 'dynamic':
|
||||||
|
return jsonify({'success': False, 'message': 'Not a dynamic QR code.'}), 400
|
||||||
|
|
||||||
|
locations = (
|
||||||
|
db.session.query(QRCode.location, QRCode.location_address)
|
||||||
|
.filter(
|
||||||
|
QRCode.qr_type == 'standard',
|
||||||
|
QRCode.active_status == True,
|
||||||
|
QRCode.location.isnot(None),
|
||||||
|
QRCode.location != '',
|
||||||
|
QRCode.location != 'Dynamic'
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.order_by(QRCode.location.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
logger_handler.logger.info(
|
||||||
|
f"qr_get_locations: QR '{qr_url}' returned {len(locations)} locations"
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'locations': [
|
||||||
|
{
|
||||||
|
'name': loc.location,
|
||||||
|
'address': loc.location_address or ''
|
||||||
|
}
|
||||||
|
for loc in locations
|
||||||
|
]
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.logger.error(
|
||||||
|
f"Error fetching locations for QR '{qr_url}': {e}", exc_info=True
|
||||||
|
)
|
||||||
|
return jsonify({'success': False, 'message': 'Server error fetching locations.'}), 500
|
||||||
|
# --- END ADDED ---
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/qr-codes/<int:qr_id>/toggle-status', methods=['POST'], endpoint='toggle_qr_status')
|
@bp.route('/qr-codes/<int:qr_id>/toggle-status', methods=['POST'], endpoint='toggle_qr_status')
|
||||||
@login_required
|
@login_required
|
||||||
def toggle_qr_status(qr_id):
|
def toggle_qr_status(qr_id):
|
||||||
|
|||||||
@@ -238,6 +238,18 @@ function proceedWithCheckin() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADDED: For dynamic QR codes, require a location to be selected before proceeding
|
||||||
|
var selLocField = document.getElementById("selected_location_name");
|
||||||
|
var locationSelectCard = document.getElementById("locationSelectCard");
|
||||||
|
if (locationSelectCard && selLocField && !selLocField.value.trim()) {
|
||||||
|
// No location selected — redirect employee back to Step 1
|
||||||
|
document.getElementById("checkinFormCard").style.display = "none";
|
||||||
|
locationSelectCard.style.display = "block";
|
||||||
|
showLocalizedStatusMessage("invalidId", "error");
|
||||||
|
console.log("❌ Dynamic QR: no location selected, returning to Step 1");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const employeeId = document.getElementById("employee_id")?.value?.trim();
|
const employeeId = document.getElementById("employee_id")?.value?.trim();
|
||||||
|
|
||||||
if (!employeeId) {
|
if (!employeeId) {
|
||||||
@@ -309,6 +321,16 @@ function submitCheckin() {
|
|||||||
formData.append("location_source", userLocation.source || "manual");
|
formData.append("location_source", userLocation.source || "manual");
|
||||||
formData.append("address", userLocation.address || "");
|
formData.append("address", userLocation.address || "");
|
||||||
|
|
||||||
|
// ADDED: Forward the employee-selected location for dynamic QR check-in
|
||||||
|
const selLocName = document.getElementById("selected_location_name");
|
||||||
|
const selLocAddr = document.getElementById("selected_location_address");
|
||||||
|
if (selLocName && selLocName.value.trim()) {
|
||||||
|
formData.append("selected_location_name", selLocName.value.trim());
|
||||||
|
}
|
||||||
|
if (selLocAddr && selLocAddr.value.trim()) {
|
||||||
|
formData.append("selected_location_address", selLocAddr.value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
const currentUrl = window.location.pathname;
|
const currentUrl = window.location.pathname;
|
||||||
const checkinUrl = `${currentUrl}/checkin`;
|
const checkinUrl = `${currentUrl}/checkin`;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<li><strong>QR Code Location</strong> - Location identifier</li>
|
<li><strong>QR Code Location</strong> - Location identifier</li>
|
||||||
<li><strong>Project</strong> - Project name (must exist)</li>
|
<li><strong>Project</strong> - Project name (must exist)</li>
|
||||||
<li><strong>Location Address</strong> - Complete address</li>
|
<li><strong>Location Address</strong> - Complete address</li>
|
||||||
<li><strong>Event</strong> - Check IN or Check OUT</li>
|
<li><strong>Event</strong> - Check In or Check Out</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<td>Main Building</td>
|
<td>Main Building</td>
|
||||||
<td>Corporate HQ</td>
|
<td>Corporate HQ</td>
|
||||||
<td>123 Main St, Springfield, IL 62701</td>
|
<td>123 Main St, Springfield, IL 62701</td>
|
||||||
<td>Check IN</td>
|
<td>Check In</td>
|
||||||
<td>39.781721</td>
|
<td>39.781721</td>
|
||||||
<td>-89.650148</td>
|
<td>-89.650148</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<td>Main Building</td>
|
<td>Main Building</td>
|
||||||
<td>Corporate HQ</td>
|
<td>Corporate HQ</td>
|
||||||
<td>123 Main St, Springfield, IL 62701</td>
|
<td>123 Main St, Springfield, IL 62701</td>
|
||||||
<td>Check OUT</td>
|
<td>Check Out</td>
|
||||||
<td>39.781721</td>
|
<td>39.781721</td>
|
||||||
<td>-89.650148</td>
|
<td>-89.650148</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<td>Construction Site A</td>
|
<td>Construction Site A</td>
|
||||||
<td>Construction Projects</td>
|
<td>Construction Projects</td>
|
||||||
<td>456 Oak Ave, Chicago, IL 60601</td>
|
<td>456 Oak Ave, Chicago, IL 60601</td>
|
||||||
<td>Check IN</td>
|
<td>Check In</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -595,6 +595,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== ADDED: QR Code Type selector — shown first so user picks type before entering location ===== -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="qr_type">
|
||||||
|
<i class="fas fa-qrcode"></i>
|
||||||
|
QR Code Type
|
||||||
|
<span style="color: var(--error-color)">*</span>
|
||||||
|
</label>
|
||||||
|
<select id="qr_type" name="qr_type" class="form-control" onchange="toggleQRTypeSection()">
|
||||||
|
<option value="standard" selected>Standard — Fixed Location</option>
|
||||||
|
<option value="dynamic">Dynamic — Employee Selects Location</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">
|
||||||
|
<strong>Standard:</strong> employee checks in at one fixed location.<br>
|
||||||
|
<strong>Dynamic:</strong> employee picks a location from a list when scanning.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<!-- ===== END ADDED ===== -->
|
||||||
|
|
||||||
|
<!-- ADDED: wrapper to hide/show standard-only location field -->
|
||||||
|
<div id="standardLocationNameGroup">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="location">
|
<label for="location">
|
||||||
<i class="fas fa-map-marker-alt"></i>
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
@@ -616,6 +636,7 @@
|
|||||||
<span id="locationCounter">0/100</span>
|
<span id="locationCounter">0/100</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div><!-- end standardLocationNameGroup -->
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="project_id">
|
<label for="project_id">
|
||||||
@@ -637,7 +658,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location Details Section -->
|
<!-- Location Details Section — hidden when Dynamic QR type is selected -->
|
||||||
|
<div id="standardLocationDetailsSection">
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
@@ -721,6 +743,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div><!-- end standardLocationDetailsSection -->
|
||||||
|
|
||||||
|
<!-- Event or Purpose — always visible for both Standard and Dynamic QR -->
|
||||||
|
<div class="form-section">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="location_event">
|
<label for="location_event">
|
||||||
<i class="fas fa-calendar-alt"></i>
|
<i class="fas fa-calendar-alt"></i>
|
||||||
@@ -741,6 +768,7 @@
|
|||||||
>Select the event type for this QR code</small
|
>Select the event type for this QR code</small
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR Code Customization Section -->
|
<!-- QR Code Customization Section -->
|
||||||
@@ -1404,5 +1432,29 @@
|
|||||||
clearBtn.addEventListener('click', clearCoordinates);
|
clearBtn.addEventListener('click', clearCoordinates);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- ===== Dynamic QR Type Toggle ===== -->
|
||||||
|
<script>
|
||||||
|
function toggleQRTypeSection() {
|
||||||
|
var type = document.getElementById('qr_type').value;
|
||||||
|
var stdName = document.getElementById('standardLocationNameGroup');
|
||||||
|
var stdDetails= document.getElementById('standardLocationDetailsSection');
|
||||||
|
var locInput = document.getElementById('location');
|
||||||
|
var addrInput = document.getElementById('location_address');
|
||||||
|
|
||||||
|
if (type === 'dynamic') {
|
||||||
|
if (stdName) stdName.style.display = 'none';
|
||||||
|
if (stdDetails)stdDetails.style.display = 'none';
|
||||||
|
if (locInput) locInput.removeAttribute('required');
|
||||||
|
if (addrInput) addrInput.removeAttribute('required');
|
||||||
|
} else {
|
||||||
|
if (stdName) stdName.style.display = 'block';
|
||||||
|
if (stdDetails)stdDetails.style.display = 'block';
|
||||||
|
if (locInput) locInput.setAttribute('required', '');
|
||||||
|
if (addrInput) addrInput.setAttribute('required', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<!-- ===== END Dynamic QR Type Toggle ===== -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -649,6 +649,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== ADDED: QR Code Type selector ===== -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="qr_type">
|
||||||
|
<i class="fas fa-qrcode"></i>
|
||||||
|
QR Code Type
|
||||||
|
<span style="color: var(--error-color)">*</span>
|
||||||
|
</label>
|
||||||
|
<select id="qr_type" name="qr_type" class="form-control" onchange="toggleQRTypeSection()">
|
||||||
|
<option value="standard" {% if qr_code.qr_type != 'dynamic' %}selected{% endif %}>Standard — Fixed Location</option>
|
||||||
|
<option value="dynamic" {% if qr_code.qr_type == 'dynamic' %}selected{% endif %}>Dynamic — Employee Selects Location</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">
|
||||||
|
<strong>Standard:</strong> employee checks in at one fixed location.<br>
|
||||||
|
<strong>Dynamic:</strong> employee picks a location from a list when scanning.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<!-- ===== END ADDED ===== -->
|
||||||
|
|
||||||
|
<!-- ADDED: wrapper to hide/show for standard QR only -->
|
||||||
|
<div id="standardLocationNameGroup" {% if qr_code.qr_type == 'dynamic' %}style="display:none;"{% endif %}>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="location">
|
<label for="location">
|
||||||
<i class="fas fa-map-marker-alt"></i>
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
@@ -658,8 +678,8 @@
|
|||||||
type="text"
|
type="text"
|
||||||
id="location"
|
id="location"
|
||||||
name="location"
|
name="location"
|
||||||
required
|
{% if qr_code.qr_type != 'dynamic' %}required{% endif %}
|
||||||
value="{{ qr_code.location }}"
|
value="{{ qr_code.location if qr_code.qr_type != 'dynamic' else '' }}"
|
||||||
data-original="{{ qr_code.location }}"
|
data-original="{{ qr_code.location }}"
|
||||||
placeholder="e.g., Corporate Headquarters, Branch Office"
|
placeholder="e.g., Corporate Headquarters, Branch Office"
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
@@ -669,9 +689,10 @@
|
|||||||
used</small
|
used</small
|
||||||
>
|
>
|
||||||
<div class="character-counter">
|
<div class="character-counter">
|
||||||
<span id="locationCounter">{{ qr_code.location|length }}/100</span>
|
<span id="locationCounter">{{ qr_code.location|length if qr_code.qr_type != 'dynamic' else '0' }}/100</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div><!-- end standardLocationNameGroup -->
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="project_id">
|
<label for="project_id">
|
||||||
@@ -693,7 +714,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Location Details Section -->
|
<!-- Location Details Section — hidden when Dynamic QR type is selected -->
|
||||||
|
<div id="standardLocationDetailsSection" {% if qr_code.qr_type == 'dynamic' %}style="display:none;"{% endif %}>
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>
|
<h3>
|
||||||
@@ -782,6 +804,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div><!-- end standardLocationDetailsSection -->
|
||||||
|
|
||||||
|
<!-- Event or Purpose — always visible for both Standard and Dynamic QR -->
|
||||||
|
<div class="form-section">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="location_event">
|
<label for="location_event">
|
||||||
<i class="fas fa-calendar-alt"></i>
|
<i class="fas fa-calendar-alt"></i>
|
||||||
@@ -803,6 +830,7 @@
|
|||||||
>Select the event type for this QR code</small
|
>Select the event type for this QR code</small
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- QR Code Customization Section -->
|
<!-- QR Code Customization Section -->
|
||||||
@@ -1451,4 +1479,32 @@
|
|||||||
clearBtn.addEventListener('click', clearCoordinates);
|
clearBtn.addEventListener('click', clearCoordinates);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- ===== Dynamic QR Type Toggle ===== -->
|
||||||
|
<script>
|
||||||
|
function toggleQRTypeSection() {
|
||||||
|
var type = document.getElementById('qr_type').value;
|
||||||
|
var stdName = document.getElementById('standardLocationNameGroup');
|
||||||
|
var stdDetails= document.getElementById('standardLocationDetailsSection');
|
||||||
|
var locInput = document.getElementById('location');
|
||||||
|
var addrInput = document.getElementById('location_address');
|
||||||
|
|
||||||
|
if (type === 'dynamic') {
|
||||||
|
if (stdName) stdName.style.display = 'none';
|
||||||
|
if (stdDetails)stdDetails.style.display = 'none';
|
||||||
|
if (locInput) locInput.removeAttribute('required');
|
||||||
|
if (addrInput) addrInput.removeAttribute('required');
|
||||||
|
} else {
|
||||||
|
if (stdName) stdName.style.display = 'block';
|
||||||
|
if (stdDetails)stdDetails.style.display = 'block';
|
||||||
|
if (locInput) locInput.setAttribute('required', '');
|
||||||
|
if (addrInput) addrInput.setAttribute('required', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
toggleQRTypeSection();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<!-- ===== END Dynamic QR Type Toggle ===== -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
+210
-32
@@ -1068,7 +1068,67 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Check-in Form with Dynamic Styling and Color-Coded Instructions -->
|
<!-- Check-in Form with Dynamic Styling and Color-Coded Instructions -->
|
||||||
<div class="checkin-card fade-transition active">
|
|
||||||
|
<!-- ===== Step 1 — Location Dropdown Selector (Dynamic QR only) ===== -->
|
||||||
|
{% if qr_code.qr_type == 'dynamic' %}
|
||||||
|
<div class="checkin-card fade-transition active" id="locationSelectCard">
|
||||||
|
<div class="checkin-header">
|
||||||
|
<div class="bilingual-container">
|
||||||
|
<p>
|
||||||
|
<span class="english-text">
|
||||||
|
<i class="fas fa-map-marker-alt"></i> Please select your work location.
|
||||||
|
</span>
|
||||||
|
<span class="language-separator">/</span>
|
||||||
|
<span class="spanish-text">Por favor seleccione su ubicación de trabajo.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-top:1rem;">
|
||||||
|
<select id="locationDropdown" class="form-control"
|
||||||
|
style="font-size:1rem;padding:0.75rem;height:auto;">
|
||||||
|
<option value="">-- Select a location / Seleccione una ubicación --</option>
|
||||||
|
{% for loc in locations %}
|
||||||
|
<option
|
||||||
|
value="{{ loc.location | e }}"
|
||||||
|
data-address="{{ (loc.location_address or '') | e }}"
|
||||||
|
>{{ loc.location }}</option>
|
||||||
|
{% else %}
|
||||||
|
<option value="" disabled>No locations configured</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary"
|
||||||
|
id="confirmLocationBtn"
|
||||||
|
style="width:100%;margin-top:0.75rem;padding:0.9rem;font-size:1rem;"
|
||||||
|
onclick="confirmLocationSelection()">
|
||||||
|
<i class="fas fa-check" style="margin-right:0.5rem;"></i>
|
||||||
|
<span class="english-text">Confirm Location</span>
|
||||||
|
<span class="language-separator">/</span>
|
||||||
|
<span class="spanish-text">Confirmar Ubicación</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="locationSelectError" style="display:none;margin-top:0.5rem;">
|
||||||
|
<div class="bilingual-container" style="background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.3);">
|
||||||
|
<p style="margin:0;color:#dc2626;">
|
||||||
|
<i class="fas fa-exclamation-circle"></i>
|
||||||
|
<span class="english-text">Please select a location before continuing.</span>
|
||||||
|
<span class="language-separator">/</span>
|
||||||
|
<span class="spanish-text">Por favor seleccione una ubicación.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<!-- ===== END Step 1 ===== -->
|
||||||
|
|
||||||
|
<!-- Step 2 (or sole step for Standard QR): Employee ID form -->
|
||||||
|
<div
|
||||||
|
class="checkin-card fade-transition {% if qr_code.qr_type != 'dynamic' %}active{% endif %}"
|
||||||
|
id="checkinFormCard"
|
||||||
|
{% if qr_code.qr_type == 'dynamic' %}style="display:none;"{% endif %}
|
||||||
|
>
|
||||||
<div class="checkin-header">
|
<div class="checkin-header">
|
||||||
<div class="bilingual-container">
|
<div class="bilingual-container">
|
||||||
<p>
|
<p>
|
||||||
@@ -1082,6 +1142,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="checkinForm" onsubmit="return false;">
|
<form id="checkinForm" onsubmit="return false;">
|
||||||
|
<!-- ADDED: carries the employee-selected location to the backend -->
|
||||||
|
<input type="hidden" id="selected_location_name" name="selected_location_name" value="" />
|
||||||
|
<input type="hidden" id="selected_location_address" name="selected_location_address" value="" />
|
||||||
|
|
||||||
|
<!-- ADDED: selected location confirmation badge (dynamic QR only) -->
|
||||||
|
{% if qr_code.qr_type == 'dynamic' %}
|
||||||
|
<div id="selectedLocationBadge"
|
||||||
|
class="bilingual-container"
|
||||||
|
style="display:none;margin-bottom:1rem;background:rgba(37,99,235,0.07);border:1px solid rgba(37,99,235,0.28);">
|
||||||
|
<p style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;margin:0;">
|
||||||
|
<span>
|
||||||
|
<i class="fas fa-map-marker-alt" style="color:#2563eb;"></i>
|
||||||
|
<strong id="selectedLocationDisplay" style="margin-left:0.4rem;"></strong>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="backToLocationSelect()"
|
||||||
|
style="background:none;border:1px solid #2563eb;color:#2563eb;border-radius:4px;padding:3px 12px;cursor:pointer;font-size:0.85rem;"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
<span class="english-text">Change</span>
|
||||||
|
<span class="language-separator">/</span>
|
||||||
|
<span class="spanish-text">Cambiar</span>
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="employee_id">
|
<label for="employee_id">
|
||||||
<i class="fas fa-id-card"></i>
|
<i class="fas fa-id-card"></i>
|
||||||
@@ -1456,6 +1544,20 @@
|
|||||||
const employeeId = document.getElementById("employee_id").value.trim();
|
const employeeId = document.getElementById("employee_id").value.trim();
|
||||||
const submitButton = document.getElementById("submitButton");
|
const submitButton = document.getElementById("submitButton");
|
||||||
|
|
||||||
|
// FIX 2: For dynamic QR codes, block submission if no location was selected
|
||||||
|
var selLocField = document.getElementById("selected_location_name");
|
||||||
|
var locationSelectCard = document.getElementById("locationSelectCard");
|
||||||
|
if (locationSelectCard && selLocField && !selLocField.value.trim()) {
|
||||||
|
// No location selected — send employee back to Step 1
|
||||||
|
document.getElementById("checkinFormCard").style.display = "none";
|
||||||
|
locationSelectCard.style.display = "block";
|
||||||
|
showStatusMessage(
|
||||||
|
"Please select a location first / Por favor seleccione una ubicación primero",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!employeeId) {
|
if (!employeeId) {
|
||||||
showStatusMessage(
|
showStatusMessage(
|
||||||
"Please enter your Employee ID / Por favor ingrese su ID de empleado",
|
"Please enter your Employee ID / Por favor ingrese su ID de empleado",
|
||||||
@@ -1495,6 +1597,16 @@
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("employee_id", employeeId);
|
formData.append("employee_id", employeeId);
|
||||||
|
|
||||||
|
// ADDED: forward employee-selected location for dynamic QR check-in
|
||||||
|
const selLocName = document.getElementById("selected_location_name");
|
||||||
|
const selLocAddr = document.getElementById("selected_location_address");
|
||||||
|
if (selLocName && selLocName.value) {
|
||||||
|
formData.append("selected_location_name", selLocName.value);
|
||||||
|
}
|
||||||
|
if (selLocAddr && selLocAddr.value) {
|
||||||
|
formData.append("selected_location_address", selLocAddr.value);
|
||||||
|
}
|
||||||
|
|
||||||
// Add location data to form submission
|
// Add location data to form submission
|
||||||
if (currentUserLocation.latitude !== null) {
|
if (currentUserLocation.latitude !== null) {
|
||||||
formData.append("latitude", currentUserLocation.latitude.toString());
|
formData.append("latitude", currentUserLocation.latitude.toString());
|
||||||
@@ -1594,44 +1706,38 @@
|
|||||||
|
|
||||||
// Enhanced success display with bilingual details (renamed to avoid conflicts)
|
// Enhanced success display with bilingual details (renamed to avoid conflicts)
|
||||||
function showEnhancedSuccessCard(data) {
|
function showEnhancedSuccessCard(data) {
|
||||||
const successCard = document.getElementById("successCard");
|
const successCard = document.getElementById("successCard");
|
||||||
const checkinCard = document.querySelector(".checkin-card");
|
const checkinCard = document.querySelector(".checkin-card");
|
||||||
const successDetails = document.getElementById("successDetails");
|
const successDetails = document.getElementById("successDetails");
|
||||||
|
|
||||||
// Hide check-in form
|
// Hide the check-in form card
|
||||||
checkinCard.style.display = "none";
|
if (checkinCard) checkinCard.style.display = "none";
|
||||||
|
|
||||||
// Get employee ID from form input (since data.employee_id might be undefined)
|
// Employee ID — prefer server response, fall back to input field
|
||||||
const employeeIdInput = document.getElementById("employee_id");
|
const employeeIdInput = document.getElementById("employee_id");
|
||||||
const employeeId =
|
const employeeId =
|
||||||
data.employee_id ||
|
(data.data && data.data.employee_id) ||
|
||||||
(employeeIdInput ? employeeIdInput.value.trim() : "N/A");
|
(employeeIdInput ? employeeIdInput.value.trim() : "N/A");
|
||||||
|
|
||||||
// Use QR code location instead of GPS address for main location display
|
// Location name — prefer server response (handles dynamic QR correctly),
|
||||||
const qrLocationElement = document.querySelector(".location-info");
|
// fall back to the .location-info element shown on the page header.
|
||||||
let qrLocationName = "Unknown Location";
|
let qrLocationName = (data.data && data.data.location) || null;
|
||||||
if (qrLocationElement) {
|
if (!qrLocationName) {
|
||||||
// Extract text content, removing the icon
|
const el = document.querySelector(".location-info");
|
||||||
const locationText =
|
if (el) qrLocationName = el.textContent.trim();
|
||||||
qrLocationElement.textContent || qrLocationElement.innerText;
|
|
||||||
qrLocationName = locationText.trim();
|
|
||||||
}
|
}
|
||||||
|
if (!qrLocationName) qrLocationName = "Unknown Location";
|
||||||
|
|
||||||
// Get location event (Check In or Check Out)
|
// Location event — prefer server response (handles dynamic QR correctly),
|
||||||
const isCheckOut = document.body.classList.contains("check-out");
|
// fall back to body class set by Jinja for standard QR codes.
|
||||||
const locationEvent = isCheckOut ? "Check Out" : "Check In";
|
const serverEvent = (data.data && data.data.event) || null;
|
||||||
const locationEventSpanish = isCheckOut ? "Salida" : "Entrada";
|
const isCheckOut = document.body.classList.contains("check-out");
|
||||||
|
const locationEvent = serverEvent || (isCheckOut ? "Check Out" : "Check In");
|
||||||
|
const locationEventSpanish = (locationEvent === "Check Out") ? "Salida" : "Entrada";
|
||||||
|
|
||||||
// Alternative: try to get from page title or header
|
// Build success detail rows
|
||||||
if (qrLocationName === "Unknown Location") {
|
const checkInTime = (data.data && data.data.check_in_time) || new Date().toLocaleTimeString();
|
||||||
const headerElement = document.querySelector(
|
const checkInDate = (data.data && data.data.check_in_date) || new Date().toLocaleDateString();
|
||||||
".destination-header h1"
|
|
||||||
);
|
|
||||||
if (headerElement) {
|
|
||||||
// Extract just the location event part (Check In/Check Out)
|
|
||||||
qrLocationName = data.location || "Check-in Location";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
successDetails.innerHTML = `
|
successDetails.innerHTML = `
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
@@ -1654,15 +1760,21 @@
|
|||||||
<span class="spanish-text">${locationEventSpanish}</span>
|
<span class="spanish-text">${locationEventSpanish}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">
|
||||||
|
<span class="english-text">Date</span>
|
||||||
|
<span class="language-separator">/</span>
|
||||||
|
<span class="spanish-text">Fecha</span>
|
||||||
|
</span>
|
||||||
|
<span class="detail-value">${checkInDate}</span>
|
||||||
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span class="detail-label">
|
<span class="detail-label">
|
||||||
<span class="english-text">Time</span>
|
<span class="english-text">Time</span>
|
||||||
<span class="language-separator">/</span>
|
<span class="language-separator">/</span>
|
||||||
<span class="spanish-text">Hora</span>
|
<span class="spanish-text">Hora</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="detail-value">${
|
<span class="detail-value">${checkInTime}</span>
|
||||||
data.timestamp || new Date().toLocaleString()
|
|
||||||
}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<span class="detail-label">
|
<span class="detail-label">
|
||||||
@@ -1677,6 +1789,72 @@
|
|||||||
// Show success card
|
// Show success card
|
||||||
successCard.classList.add("show");
|
successCard.classList.add("show");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Dynamic QR location step handlers =====
|
||||||
|
function confirmLocationSelection() {
|
||||||
|
var dropdown = document.getElementById("locationDropdown");
|
||||||
|
var errorDiv = document.getElementById("locationSelectError");
|
||||||
|
|
||||||
|
// Validate a location has been chosen
|
||||||
|
if (!dropdown || !dropdown.value) {
|
||||||
|
if (errorDiv) errorDiv.style.display = "block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errorDiv) errorDiv.style.display = "none";
|
||||||
|
|
||||||
|
var name = dropdown.value;
|
||||||
|
var selOpt = dropdown.options[dropdown.selectedIndex];
|
||||||
|
var address = selOpt ? (selOpt.getAttribute("data-address") || "") : "";
|
||||||
|
|
||||||
|
// Store selection in hidden fields
|
||||||
|
document.getElementById("selected_location_name").value = name;
|
||||||
|
document.getElementById("selected_location_address").value = address;
|
||||||
|
|
||||||
|
// FIX 1: Update the page header .location-info to show the selected location
|
||||||
|
var locationInfoEl = document.querySelector(".location-info");
|
||||||
|
if (locationInfoEl) {
|
||||||
|
locationInfoEl.innerHTML = "<i class=\"fas fa-map-marker-alt\"></i> " + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the selected location badge on the ID form
|
||||||
|
var display = document.getElementById("selectedLocationDisplay");
|
||||||
|
var badge = document.getElementById("selectedLocationBadge");
|
||||||
|
if (display) display.textContent = name;
|
||||||
|
if (badge) badge.style.display = "block";
|
||||||
|
|
||||||
|
// Transition: hide Step 1, show Step 2
|
||||||
|
document.getElementById("locationSelectCard").style.display = "none";
|
||||||
|
var formCard = document.getElementById("checkinFormCard");
|
||||||
|
formCard.style.display = "block";
|
||||||
|
formCard.classList.add("active");
|
||||||
|
|
||||||
|
// Auto-focus Employee ID input for faster mobile entry
|
||||||
|
var empInput = document.getElementById("employee_id");
|
||||||
|
if (empInput) { setTimeout(function () { empInput.focus(); }, 120); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function backToLocationSelect() {
|
||||||
|
// Clear selection
|
||||||
|
document.getElementById("selected_location_name").value = "";
|
||||||
|
document.getElementById("selected_location_address").value = "";
|
||||||
|
var badge = document.getElementById("selectedLocationBadge");
|
||||||
|
if (badge) badge.style.display = "none";
|
||||||
|
|
||||||
|
// Reset dropdown
|
||||||
|
var dropdown = document.getElementById("locationDropdown");
|
||||||
|
if (dropdown) dropdown.value = "";
|
||||||
|
|
||||||
|
// Reset header back to blank while no location is selected
|
||||||
|
var locationInfoEl = document.querySelector(".location-info");
|
||||||
|
if (locationInfoEl) {
|
||||||
|
locationInfoEl.innerHTML = "<i class=\"fas fa-map-marker-alt\"></i> ";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transition: hide Step 2, show Step 1
|
||||||
|
document.getElementById("checkinFormCard").style.display = "none";
|
||||||
|
document.getElementById("locationSelectCard").style.display = "block";
|
||||||
|
}
|
||||||
|
// ===== END Dynamic QR location step handlers =====
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
Migration: Dynamic QR Code Support
|
||||||
|
====================================
|
||||||
|
Applies the following database changes required for the Dynamic QR feature:
|
||||||
|
|
||||||
|
1. Adds qr_type column to qr_codes (VARCHAR 20, default 'standard')
|
||||||
|
2. Creates qr_code_locations table
|
||||||
|
3. Makes qr_codes.location nullable (was NOT NULL)
|
||||||
|
4. Makes qr_codes.location_address nullable (was NOT NULL)
|
||||||
|
|
||||||
|
Usage (run once from the project root):
|
||||||
|
python tools/migration_dynamic_qr_locations.py
|
||||||
|
|
||||||
|
Fully idempotent — safe to run multiple times without side effects.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from app import app, db
|
||||||
|
|
||||||
|
|
||||||
|
def run_migration():
|
||||||
|
with app.app_context():
|
||||||
|
from sqlalchemy import text, inspect as sa_inspect
|
||||||
|
|
||||||
|
inspector = sa_inspect(db.engine)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Step 1: Add qr_type column to qr_codes if it does not exist yet
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
existing_cols = {c['name'] for c in inspector.get_columns('qr_codes')}
|
||||||
|
|
||||||
|
if 'qr_type' not in existing_cols:
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
conn.execute(text(
|
||||||
|
"ALTER TABLE qr_codes "
|
||||||
|
"ADD COLUMN qr_type VARCHAR(20) NOT NULL DEFAULT 'standard'"
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
print("✅ Added column: qr_codes.qr_type (default = 'standard')")
|
||||||
|
else:
|
||||||
|
print("ℹ️ Column qr_codes.qr_type already exists — skipped.")
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Step 2: Create qr_code_locations table if it does not exist yet
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
existing_tables = set(inspector.get_table_names())
|
||||||
|
|
||||||
|
if 'qr_code_locations' not in existing_tables:
|
||||||
|
from models.qrcode import QRCodeLocation
|
||||||
|
QRCodeLocation.__table__.create(db.engine, checkfirst=True)
|
||||||
|
print("✅ Created table: qr_code_locations")
|
||||||
|
else:
|
||||||
|
print("ℹ️ Table qr_code_locations already exists — skipped.")
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Step 3: Make qr_codes.location and location_address nullable
|
||||||
|
# Dynamic QR codes have no single fixed location/address so these
|
||||||
|
# columns must allow NULL.
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Re-inspect to get current column definitions
|
||||||
|
inspector2 = sa_inspect(db.engine)
|
||||||
|
col_map = {c['name']: c for c in inspector2.get_columns('qr_codes')}
|
||||||
|
|
||||||
|
location_nullable = col_map.get('location', {}).get('nullable', True)
|
||||||
|
loc_addr_nullable = col_map.get('location_address', {}).get('nullable', True)
|
||||||
|
|
||||||
|
if not location_nullable or not loc_addr_nullable:
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
if not location_nullable:
|
||||||
|
conn.execute(text(
|
||||||
|
"ALTER TABLE qr_codes "
|
||||||
|
"MODIFY COLUMN location VARCHAR(100) NULL"
|
||||||
|
))
|
||||||
|
print("✅ Made qr_codes.location nullable")
|
||||||
|
else:
|
||||||
|
print("ℹ️ qr_codes.location already nullable — skipped.")
|
||||||
|
|
||||||
|
if not loc_addr_nullable:
|
||||||
|
conn.execute(text(
|
||||||
|
"ALTER TABLE qr_codes "
|
||||||
|
"MODIFY COLUMN location_address TEXT NULL"
|
||||||
|
))
|
||||||
|
print("✅ Made qr_codes.location_address nullable")
|
||||||
|
else:
|
||||||
|
print("ℹ️ qr_codes.location_address already nullable — skipped.")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
else:
|
||||||
|
print("ℹ️ qr_codes.location and location_address already nullable — skipped.")
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Step 4: Add qr_address column to attendance_data if absent
|
||||||
|
# Stores the selected location's address for dynamic QR check-ins
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
inspector3 = sa_inspect(db.engine)
|
||||||
|
att_cols = {c['name'] for c in inspector3.get_columns('attendance_data')}
|
||||||
|
if 'qr_address' not in att_cols:
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
conn.execute(text(
|
||||||
|
"ALTER TABLE attendance_data ADD COLUMN qr_address TEXT NULL"
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
print("\u2705 Added column: attendance_data.qr_address")
|
||||||
|
else:
|
||||||
|
print("\u2139\ufe0f Column attendance_data.qr_address already exists \u2014 skipped.")
|
||||||
|
|
||||||
|
print("\nMigration complete.")
|
||||||
|
print("All existing QR codes remain fully unaffected (qr_type = 'standard').")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_migration()
|
||||||
Reference in New Issue
Block a user