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():
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
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)
|
||||
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'
|
||||
|
||||
+39
-4
@@ -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,6 +39,10 @@ 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')
|
||||
@@ -79,3 +84,33 @@ class QRCodeStyle(base.db.Model):
|
||||
|
||||
def __repr__(self):
|
||||
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.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,
|
||||
|
||||
+214
-21
@@ -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/<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')
|
||||
@login_required
|
||||
def toggle_qr_status(qr_id):
|
||||
|
||||
@@ -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`;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<li><strong>QR Code Location</strong> - Location identifier</li>
|
||||
<li><strong>Project</strong> - Project name (must exist)</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>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<td>Main Building</td>
|
||||
<td>Corporate HQ</td>
|
||||
<td>123 Main St, Springfield, IL 62701</td>
|
||||
<td>Check IN</td>
|
||||
<td>Check In</td>
|
||||
<td>39.781721</td>
|
||||
<td>-89.650148</td>
|
||||
</tr>
|
||||
@@ -99,7 +99,7 @@
|
||||
<td>Main Building</td>
|
||||
<td>Corporate HQ</td>
|
||||
<td>123 Main St, Springfield, IL 62701</td>
|
||||
<td>Check OUT</td>
|
||||
<td>Check Out</td>
|
||||
<td>39.781721</td>
|
||||
<td>-89.650148</td>
|
||||
</tr>
|
||||
@@ -108,7 +108,7 @@
|
||||
<td>Construction Site A</td>
|
||||
<td>Construction Projects</td>
|
||||
<td>456 Oak Ave, Chicago, IL 60601</td>
|
||||
<td>Check IN</td>
|
||||
<td>Check In</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
||||
@@ -595,6 +595,26 @@
|
||||
</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">
|
||||
<label for="location">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
@@ -616,6 +636,7 @@
|
||||
<span id="locationCounter">0/100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- end standardLocationNameGroup -->
|
||||
|
||||
<div class="form-group">
|
||||
<label for="project_id">
|
||||
@@ -637,7 +658,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Details Section -->
|
||||
<!-- Location Details Section — hidden when Dynamic QR type is selected -->
|
||||
<div id="standardLocationDetailsSection">
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h3>
|
||||
@@ -721,6 +743,11 @@
|
||||
</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">
|
||||
<label for="location_event">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
@@ -741,6 +768,7 @@
|
||||
>Select the event type for this QR code</small
|
||||
>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- QR Code Customization Section -->
|
||||
@@ -1404,5 +1432,29 @@
|
||||
clearBtn.addEventListener('click', clearCoordinates);
|
||||
});
|
||||
</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>
|
||||
</html>
|
||||
@@ -649,6 +649,26 @@
|
||||
</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">
|
||||
<label for="location">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
@@ -658,8 +678,8 @@
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
required
|
||||
value="{{ qr_code.location }}"
|
||||
{% if qr_code.qr_type != 'dynamic' %}required{% endif %}
|
||||
value="{{ qr_code.location if qr_code.qr_type != 'dynamic' else '' }}"
|
||||
data-original="{{ qr_code.location }}"
|
||||
placeholder="e.g., Corporate Headquarters, Branch Office"
|
||||
maxlength="100"
|
||||
@@ -669,9 +689,10 @@
|
||||
used</small
|
||||
>
|
||||
<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><!-- end standardLocationNameGroup -->
|
||||
|
||||
<div class="form-group">
|
||||
<label for="project_id">
|
||||
@@ -693,7 +714,8 @@
|
||||
</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="section-header">
|
||||
<h3>
|
||||
@@ -782,6 +804,11 @@
|
||||
</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">
|
||||
<label for="location_event">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
@@ -803,6 +830,7 @@
|
||||
>Select the event type for this QR code</small
|
||||
>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- QR Code Customization Section -->
|
||||
@@ -1451,4 +1479,32 @@
|
||||
clearBtn.addEventListener('click', clearCoordinates);
|
||||
});
|
||||
</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 %}
|
||||
+207
-29
@@ -1068,7 +1068,67 @@
|
||||
</div>
|
||||
|
||||
<!-- 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="bilingual-container">
|
||||
<p>
|
||||
@@ -1082,6 +1142,34 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<label for="employee_id">
|
||||
<i class="fas fa-id-card"></i>
|
||||
@@ -1456,6 +1544,20 @@
|
||||
const employeeId = document.getElementById("employee_id").value.trim();
|
||||
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) {
|
||||
showStatusMessage(
|
||||
"Please enter your Employee ID / Por favor ingrese su ID de empleado",
|
||||
@@ -1495,6 +1597,16 @@
|
||||
const formData = new FormData();
|
||||
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
|
||||
if (currentUserLocation.latitude !== null) {
|
||||
formData.append("latitude", currentUserLocation.latitude.toString());
|
||||
@@ -1598,40 +1710,34 @@
|
||||
const checkinCard = document.querySelector(".checkin-card");
|
||||
const successDetails = document.getElementById("successDetails");
|
||||
|
||||
// Hide check-in form
|
||||
checkinCard.style.display = "none";
|
||||
// Hide the check-in form card
|
||||
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 employeeId =
|
||||
data.employee_id ||
|
||||
(data.data && data.data.employee_id) ||
|
||||
(employeeIdInput ? employeeIdInput.value.trim() : "N/A");
|
||||
|
||||
// Use QR code location instead of GPS address for main location display
|
||||
const qrLocationElement = document.querySelector(".location-info");
|
||||
let qrLocationName = "Unknown Location";
|
||||
if (qrLocationElement) {
|
||||
// Extract text content, removing the icon
|
||||
const locationText =
|
||||
qrLocationElement.textContent || qrLocationElement.innerText;
|
||||
qrLocationName = locationText.trim();
|
||||
// Location name — prefer server response (handles dynamic QR correctly),
|
||||
// fall back to the .location-info element shown on the page header.
|
||||
let qrLocationName = (data.data && data.data.location) || null;
|
||||
if (!qrLocationName) {
|
||||
const el = document.querySelector(".location-info");
|
||||
if (el) qrLocationName = el.textContent.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 locationEvent = isCheckOut ? "Check Out" : "Check In";
|
||||
const locationEventSpanish = isCheckOut ? "Salida" : "Entrada";
|
||||
const locationEvent = serverEvent || (isCheckOut ? "Check Out" : "Check In");
|
||||
const locationEventSpanish = (locationEvent === "Check Out") ? "Salida" : "Entrada";
|
||||
|
||||
// Alternative: try to get from page title or header
|
||||
if (qrLocationName === "Unknown Location") {
|
||||
const headerElement = document.querySelector(
|
||||
".destination-header h1"
|
||||
);
|
||||
if (headerElement) {
|
||||
// Extract just the location event part (Check In/Check Out)
|
||||
qrLocationName = data.location || "Check-in Location";
|
||||
}
|
||||
}
|
||||
// Build success detail rows
|
||||
const checkInTime = (data.data && data.data.check_in_time) || new Date().toLocaleTimeString();
|
||||
const checkInDate = (data.data && data.data.check_in_date) || new Date().toLocaleDateString();
|
||||
|
||||
successDetails.innerHTML = `
|
||||
<div class="detail-row">
|
||||
@@ -1654,15 +1760,21 @@
|
||||
<span class="spanish-text">${locationEventSpanish}</span>
|
||||
</span>
|
||||
</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">
|
||||
<span class="detail-label">
|
||||
<span class="english-text">Time</span>
|
||||
<span class="language-separator">/</span>
|
||||
<span class="spanish-text">Hora</span>
|
||||
</span>
|
||||
<span class="detail-value">${
|
||||
data.timestamp || new Date().toLocaleString()
|
||||
}</span>
|
||||
<span class="detail-value">${checkInTime}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">
|
||||
@@ -1677,6 +1789,72 @@
|
||||
// Show success card
|
||||
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>
|
||||
</body>
|
||||
</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