Apr 06 2026: enhanced - update the dynamic QR code

This commit is contained in:
2026-04-06 09:28:58 -04:00
parent 28bcd13c7f
commit f9aa07716b
12 changed files with 727 additions and 74 deletions
+2 -2
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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'
+39 -4
View File
@@ -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,6 +39,10 @@ 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')
@@ -79,3 +84,33 @@ class QRCodeStyle(base.db.Model):
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})>'
+2 -2
View File
@@ -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
View File
@@ -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):
+22
View File
@@ -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`;
+4 -4
View File
@@ -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>
+53 -1
View File
@@ -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>
+60 -4
View File
@@ -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 %}
+207 -29
View File
@@ -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());
@@ -1598,40 +1710,34 @@
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),
// fall back to body class set by Jinja for standard QR codes.
const serverEvent = (data.data && data.data.event) || null;
const isCheckOut = document.body.classList.contains("check-out"); const isCheckOut = document.body.classList.contains("check-out");
const locationEvent = isCheckOut ? "Check Out" : "Check In"; const locationEvent = serverEvent || (isCheckOut ? "Check Out" : "Check In");
const locationEventSpanish = isCheckOut ? "Salida" : "Entrada"; 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>
+115
View File
@@ -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()