From f9aa07716b5a1d3c1439093a702ce44a9e605c65 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 6 Apr 2026 09:28:58 -0400 Subject: [PATCH] Apr 06 2026: enhanced - update the dynamic QR code --- app.py | 4 +- models/__init__.py | 4 +- models/attendance.py | 2 + models/qrcode.py | 47 ++++- routes/attendance.py | 4 +- routes/qr_codes.py | 235 +++++++++++++++++++++-- static/js/qr_destination.js | 22 +++ templates/bulk_qr_import.html | 8 +- templates/create_qr_code.html | 54 +++++- templates/edit_qr_code.html | 64 ++++++- templates/qr_destination.html | 242 ++++++++++++++++++++---- tools/migration_dynamic_qr_locations.py | 115 +++++++++++ 12 files changed, 727 insertions(+), 74 deletions(-) create mode 100644 tools/migration_dynamic_qr_locations.py diff --git a/app.py b/app.py index ab25c49..b543b17 100644 --- a/app.py +++ b/app.py @@ -56,9 +56,9 @@ def create_app() -> Flask: with app.app_context(): # Unpack model classes and store on app for shared access - (User, QRCode, QRCodeStyle, Project, AttendanceData, + (User, QRCode, QRCodeStyle, QRCodeLocation, Project, AttendanceData, Employee, TimeAttendance, UserProjectPermission, - UserLocationPermission) = set_db(db) + UserLocationPermission) = set_db(db) # ADDED: QRCodeLocation diff --git a/models/__init__.py b/models/__init__.py index 55a73ca..b4021ac 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -14,11 +14,11 @@ def set_db(database): # Now import all models (they will use base.db) from .user import User - from .qrcode import QRCode, QRCodeStyle + from .qrcode import QRCode, QRCodeStyle, QRCodeLocation # ADDED: QRCodeLocation from .project import Project from .attendance import AttendanceData from .employee import Employee from .time_attendance import TimeAttendance from .permissions import UserProjectPermission, UserLocationPermission - return User, QRCode, QRCodeStyle, Project, AttendanceData, Employee, TimeAttendance, UserProjectPermission, UserLocationPermission \ No newline at end of file + return User, QRCode, QRCodeStyle, QRCodeLocation, Project, AttendanceData, Employee, TimeAttendance, UserProjectPermission, UserLocationPermission diff --git a/models/attendance.py b/models/attendance.py index f50d58e..00fd90d 100644 --- a/models/attendance.py +++ b/models/attendance.py @@ -33,6 +33,8 @@ class AttendanceData(base.db.Model): altitude = base.db.Column(base.db.Float, nullable=True) location_source = base.db.Column(base.db.String(50), default='manual') 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_required = base.db.Column(base.db.Boolean, default=False) verification_status = base.db.Column(base.db.String(20), nullable=True) # 'pending', 'approved', 'rejected' diff --git a/models/qrcode.py b/models/qrcode.py index d794f16..58bd534 100644 --- a/models/qrcode.py +++ b/models/qrcode.py @@ -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. 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) name = base.db.Column(base.db.String(100), nullable=False) - location = base.db.Column(base.db.String(100), nullable=False) - location_address = base.db.Column(base.db.Text, nullable=False) + # nullable=True for dynamic QR codes which have no single fixed location/address + 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) 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) @@ -38,7 +39,11 @@ class QRCode(base.db.Model): border = base.db.Column(base.db.Integer, default=4) 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) - + # --- 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 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')) def __repr__(self): - return f'' \ No newline at end of file + return f'' + + +# --- 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'' diff --git a/routes/attendance.py b/routes/attendance.py index 1531c86..4b7de3f 100644 --- a/routes/attendance.py +++ b/routes/attendance.py @@ -168,7 +168,7 @@ def attendance_report(): ad.check_in_time, ad.location_name, 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.latitude, ad.longitude, @@ -196,7 +196,7 @@ def attendance_report(): ad.check_in_time, ad.location_name, 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.latitude, ad.longitude, diff --git a/routes/qr_codes.py b/routes/qr_codes.py index f1a7876..48119e8 100644 --- a/routes/qr_codes.py +++ b/routes/qr_codes.py @@ -14,7 +14,7 @@ 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.qrcode import QRCode, QRCodeStyle, QRCodeLocation # ADDED: QRCodeLocation for dynamic QR from models.user import User from werkzeug.utils import secure_filename from logger_handler import log_user_activity, log_database_operations @@ -42,6 +42,37 @@ import openpyxl 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') @login_required @@ -52,8 +83,26 @@ def create_qr_code(): try: # Existing form data name = request.form['name'] - location = request.form['location'] - location_address = request.form['location_address'] + qr_type = request.form.get('qr_type', 'standard') # read type first + + # 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', '') project_id = request.form.get('project_id') @@ -124,6 +173,7 @@ def create_qr_code(): address_longitude=address_longitude, coordinate_accuracy=coordinate_accuracy 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) **({ 'fill_color': fill_color, @@ -407,8 +457,19 @@ def edit_qr_code(qr_id): # Update QR code fields new_name = request.form['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', '') # Handle coordinates @@ -502,8 +563,8 @@ def edit_qr_code(qr_id): # GET request - render edit form projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() styles = QRCodeStyle.query.order_by(QRCodeStyle.name.asc()).all() - - return render_template('edit_qr_code.html', qr_code=qr_code, projects=projects, styles=styles) + return render_template('edit_qr_code.html', qr_code=qr_code, + projects=projects, styles=styles) except Exception as e: db.session.rollback() @@ -586,7 +647,25 @@ def qr_destination(qr_url): 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: logger_handler.log_database_error('qr_code_scan', e) @@ -618,6 +697,53 @@ def qr_checkin(qr_url): # Get and validate employee ID 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: return jsonify({ 'success': False, @@ -644,17 +770,22 @@ def qr_checkin(qr_url): # Check if 30 minutes have passed since the last check-in if recent_checkin_datetime > the_last_checkin_time: 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({ '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" - f"Puedes volver a registrarte en {minutes_remaining} minutos. El último registro fue a las {recent_checkin.check_in_time.strftime("%H:%M")}." + 'message': ( + 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 else: logger_handler.logger.debug(f"{time_interval}-minute interval satisfied for employee {employee_id}") 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 location_data = process_location_data_enhanced(request.form) @@ -669,6 +800,14 @@ def qr_checkin(qr_url): # Create 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( qr_code_id=qr_code.id, employee_id=employee_id.upper(), @@ -677,7 +816,8 @@ def qr_checkin(qr_url): device_info=device_info, user_agent=user_agent_string, ip_address=client_ip, - location_name=qr_code.location, + location_name=record_location_name, + qr_address=record_qr_address, latitude=location_data['latitude'], longitude=location_data['longitude'], accuracy=location_data['accuracy'], @@ -702,7 +842,8 @@ def qr_checkin(qr_url): try: # 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}") elif not location_data['address'] and not (location_data['latitude'] and location_data['longitude']): 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") 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_lat=location_data['latitude'], checkin_lng=location_data['longitude'] @@ -808,7 +949,7 @@ def qr_checkin(qr_url): check_in_date=today ).count() - checkin_sequence_text = f"{qr_code.location_event} details" + checkin_sequence_text = f"{effective_location_event} details" except Exception as e: 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.', 'data': { 'employee_id': attendance.employee_id, - 'location': qr_code.location_address, - 'location_event': qr_code.location_event, - 'event': qr_code.location_event, # Add both for compatibility - 'check_in_time': attendance.check_in_time.strftime('%I:%M %p'), # 12-hour format - 'check_in_date': attendance.check_in_date.strftime('%B %d, %Y'), # Full date format + 'location': effective_location_name, # CHANGED: use selected location name + 'location_event': effective_location_event, # CHANGED: use resolved event + 'event': effective_location_event, # CHANGED: use resolved event + 'check_in_time': attendance.check_in_time.strftime('%I:%M %p'), + 'check_in_date': attendance.check_in_date.strftime('%B %d, %Y'), 'device_info': attendance.device_info, 'ip_address': attendance.ip_address, 'location_accuracy': location_accuracy, @@ -865,6 +1006,58 @@ def qr_checkin(qr_url): 'message': 'An unexpected error occurred during check-in.' }), 500 +# --- ADDED: API endpoint returning selectable locations for a dynamic QR code --- +@bp.route('/qr//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//toggle-status', methods=['POST'], endpoint='toggle_qr_status') @login_required def toggle_qr_status(qr_id): diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js index 98ffe81..a110700 100644 --- a/static/js/qr_destination.js +++ b/static/js/qr_destination.js @@ -238,6 +238,18 @@ function proceedWithCheckin() { 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(); if (!employeeId) { @@ -309,6 +321,16 @@ function submitCheckin() { formData.append("location_source", userLocation.source || "manual"); 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 checkinUrl = `${currentUrl}/checkin`; diff --git a/templates/bulk_qr_import.html b/templates/bulk_qr_import.html index eec6440..0e40170 100644 --- a/templates/bulk_qr_import.html +++ b/templates/bulk_qr_import.html @@ -33,7 +33,7 @@
  • QR Code Location - Location identifier
  • Project - Project name (must exist)
  • Location Address - Complete address
  • -
  • Event - Check IN or Check OUT
  • +
  • Event - Check In or Check Out
  • @@ -90,7 +90,7 @@ Main Building Corporate HQ 123 Main St, Springfield, IL 62701 - Check IN + Check In 39.781721 -89.650148 @@ -99,7 +99,7 @@ Main Building Corporate HQ 123 Main St, Springfield, IL 62701 - Check OUT + Check Out 39.781721 -89.650148 @@ -108,7 +108,7 @@ Construction Site A Construction Projects 456 Oak Ave, Chicago, IL 60601 - Check IN + Check In diff --git a/templates/create_qr_code.html b/templates/create_qr_code.html index 6b1b0d6..ac705c1 100644 --- a/templates/create_qr_code.html +++ b/templates/create_qr_code.html @@ -595,6 +595,26 @@ + +
    + + + + Standard: employee checks in at one fixed location.
    + Dynamic: employee picks a location from a list when scanning. +
    +
    + + + +
    +
    - + +

    @@ -721,6 +743,11 @@

    +
    + + + +
    +
    @@ -1404,5 +1432,29 @@ clearBtn.addEventListener('click', clearCoordinates); }); + + + + \ No newline at end of file diff --git a/templates/edit_qr_code.html b/templates/edit_qr_code.html index ffa9153..1545efd 100644 --- a/templates/edit_qr_code.html +++ b/templates/edit_qr_code.html @@ -649,6 +649,26 @@ + +
    + + + + Standard: employee checks in at one fixed location.
    + Dynamic: employee picks a location from a list when scanning. +
    +
    + + + +
    +
    - + +

    @@ -782,6 +804,11 @@

    +
    + + + +
    +
    @@ -1451,4 +1479,32 @@ clearBtn.addEventListener('click', clearCoordinates); }); + + + + {% endblock %} \ No newline at end of file diff --git a/templates/qr_destination.html b/templates/qr_destination.html index 07284b7..c44831e 100644 --- a/templates/qr_destination.html +++ b/templates/qr_destination.html @@ -1068,7 +1068,67 @@ -
    + + + {% if qr_code.qr_type == 'dynamic' %} +
    +
    +
    +

    + + Please select your work location. + + / + Por favor seleccione su ubicación de trabajo. +

    +
    +
    + +
    + +
    + + + + +
    + {% endif %} + + + +

    @@ -1082,6 +1142,34 @@

    + + + + + + {% if qr_code.qr_type == 'dynamic' %} + + {% endif %} +