Apr 06 2026: enhanced - update the dynamic QR code records UI
This commit is contained in:
@@ -35,6 +35,8 @@ class AttendanceData(base.db.Model):
|
|||||||
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)
|
# 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)
|
qr_address = base.db.Column(base.db.Text, nullable=True)
|
||||||
|
# True when this record was created via a Dynamic QR code scan
|
||||||
|
is_dynamic_qr = base.db.Column(base.db.Boolean, default=False, nullable=False)
|
||||||
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'
|
||||||
|
|||||||
+47
-9
@@ -180,7 +180,8 @@ def attendance_report():
|
|||||||
CONCAT(e.firstName, ' ', e.lastName) as employee_name,
|
CONCAT(e.firstName, ' ', e.lastName) as employee_name,
|
||||||
ad.verification_required,
|
ad.verification_required,
|
||||||
ad.verification_status,
|
ad.verification_status,
|
||||||
ad.verification_photo
|
ad.verification_photo,
|
||||||
|
COALESCE(ad.is_dynamic_qr, 0) as is_dynamic_qr
|
||||||
FROM attendance_data ad
|
FROM attendance_data ad
|
||||||
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
||||||
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
||||||
@@ -208,7 +209,8 @@ def attendance_report():
|
|||||||
CONCAT(e.firstName, ' ', e.lastName) as employee_name,
|
CONCAT(e.firstName, ' ', e.lastName) as employee_name,
|
||||||
ad.verification_required,
|
ad.verification_required,
|
||||||
ad.verification_status,
|
ad.verification_status,
|
||||||
ad.verification_photo
|
ad.verification_photo,
|
||||||
|
COALESCE(ad.is_dynamic_qr, 0) as is_dynamic_qr
|
||||||
FROM attendance_data ad
|
FROM attendance_data ad
|
||||||
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
LEFT JOIN qr_codes qc ON ad.qr_code_id = qc.id
|
||||||
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
LEFT JOIN employee e ON CAST(ad.employee_id AS UNSIGNED) = e.id
|
||||||
@@ -250,6 +252,7 @@ def attendance_report():
|
|||||||
query_params['date_to'] = date_to
|
query_params['date_to'] = date_to
|
||||||
|
|
||||||
if location_filter:
|
if location_filter:
|
||||||
|
# Exact match — dropdown value IS the exact location_name string
|
||||||
filter_conditions.append("ad.location_name = :location")
|
filter_conditions.append("ad.location_name = :location")
|
||||||
query_params['location'] = location_filter
|
query_params['location'] = location_filter
|
||||||
|
|
||||||
@@ -268,7 +271,19 @@ def attendance_report():
|
|||||||
)
|
)
|
||||||
|
|
||||||
if project_filter:
|
if project_filter:
|
||||||
filter_conditions.append("qc.project_id = :project")
|
# For standard QR records: match by the QR code's project_id directly.
|
||||||
|
# For dynamic QR records: the dynamic QR itself may not be in any project,
|
||||||
|
# but the employee-selected location corresponds to a standard QR in that
|
||||||
|
# project. Match those by checking if attendance_data.location_name
|
||||||
|
# appears in the locations of QR codes belonging to the selected project.
|
||||||
|
filter_conditions.append(
|
||||||
|
"(qc.project_id = :project OR "
|
||||||
|
"(ad.is_dynamic_qr = 1 AND ad.location_name IN ("
|
||||||
|
" SELECT DISTINCT qc2.location FROM qr_codes qc2 "
|
||||||
|
" WHERE qc2.project_id = :project AND qc2.qr_type = 'standard' "
|
||||||
|
" AND qc2.location IS NOT NULL AND qc2.location != ''"
|
||||||
|
")))"
|
||||||
|
)
|
||||||
query_params['project'] = project_filter
|
query_params['project'] = project_filter
|
||||||
|
|
||||||
# Combine query with filters
|
# Combine query with filters
|
||||||
@@ -307,7 +322,8 @@ def attendance_report():
|
|||||||
'employee_name': record[15] or 'Unknown Employee',
|
'employee_name': record[15] or 'Unknown Employee',
|
||||||
'verification_required': record[16] if len(record) > 16 else False,
|
'verification_required': record[16] if len(record) > 16 else False,
|
||||||
'verification_status': record[17] if len(record) > 17 else None,
|
'verification_status': record[17] if len(record) > 17 else None,
|
||||||
'verification_photo': record[18] if len(record) > 18 else None
|
'verification_photo': record[18] if len(record) > 18 else None,
|
||||||
|
'is_dynamic_qr': bool(record[19]) if len(record) > 19 else False
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate accuracy_level for template display
|
# Calculate accuracy_level for template display
|
||||||
@@ -339,6 +355,8 @@ def attendance_report():
|
|||||||
SELECT DISTINCT location_name
|
SELECT DISTINCT location_name
|
||||||
FROM attendance_data
|
FROM attendance_data
|
||||||
WHERE location_name IS NOT NULL
|
WHERE location_name IS NOT NULL
|
||||||
|
AND location_name != 'Dynamic'
|
||||||
|
AND location_name != ''
|
||||||
ORDER BY location_name
|
ORDER BY location_name
|
||||||
"""))
|
"""))
|
||||||
locations = [row[0] for row in locations_query.fetchall()]
|
locations = [row[0] for row in locations_query.fetchall()]
|
||||||
@@ -1861,7 +1879,13 @@ def create_excel_export(selected_columns, column_names, filters):
|
|||||||
elif column_key == 'check_in_time':
|
elif column_key == 'check_in_time':
|
||||||
cell.value = attendance_record.check_in_time.strftime('%H:%M:%S') if attendance_record.check_in_time else ''
|
cell.value = attendance_record.check_in_time.strftime('%H:%M:%S') if attendance_record.check_in_time else ''
|
||||||
elif column_key == 'qr_address':
|
elif column_key == 'qr_address':
|
||||||
cell.value = qr_record.location_address if qr_record else ''
|
# Use attendance-level qr_address first (set for dynamic QR check-ins),
|
||||||
|
# fall back to the QR code's location_address for standard QR.
|
||||||
|
cell.value = (
|
||||||
|
getattr(attendance_record, 'qr_address', None)
|
||||||
|
or (qr_record.location_address if qr_record else '')
|
||||||
|
or ''
|
||||||
|
)
|
||||||
elif column_key == 'address':
|
elif column_key == 'address':
|
||||||
# Check-in address logic based on location accuracy WITH HYPERLINKS
|
# Check-in address logic based on location accuracy WITH HYPERLINKS
|
||||||
# If location accuracy < 0.3 miles, use QR address; otherwise use actual check-in address
|
# If location accuracy < 0.3 miles, use QR address; otherwise use actual check-in address
|
||||||
@@ -1870,7 +1894,11 @@ def create_excel_export(selected_columns, column_names, filters):
|
|||||||
accuracy_value = float(attendance_record.location_accuracy)
|
accuracy_value = float(attendance_record.location_accuracy)
|
||||||
if accuracy_value < 0.3:
|
if accuracy_value < 0.3:
|
||||||
# High accuracy - use QR code ADDRESS (not location) with hyperlink
|
# High accuracy - use QR code ADDRESS (not location) with hyperlink
|
||||||
address_text = qr_record.location_address if qr_record and qr_record.location_address else ''
|
address_text = (
|
||||||
|
getattr(attendance_record, 'qr_address', None)
|
||||||
|
or (qr_record.location_address if qr_record and qr_record.location_address else '')
|
||||||
|
or ''
|
||||||
|
)
|
||||||
if address_text and hasattr(qr_record, 'address_latitude') and hasattr(qr_record, 'address_longitude') and qr_record.address_latitude and qr_record.address_longitude:
|
if address_text and hasattr(qr_record, 'address_latitude') and hasattr(qr_record, 'address_longitude') and qr_record.address_latitude and qr_record.address_longitude:
|
||||||
# Format coordinates with 10 decimal places
|
# Format coordinates with 10 decimal places
|
||||||
lat_formatted = f"{float(qr_record.address_latitude):.10f}"
|
lat_formatted = f"{float(qr_record.address_latitude):.10f}"
|
||||||
@@ -2161,7 +2189,13 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
|
|||||||
elif column_key == 'check_in_time':
|
elif column_key == 'check_in_time':
|
||||||
cell.value = attendance_record.check_in_time.strftime('%H:%M:%S') if attendance_record.check_in_time else ''
|
cell.value = attendance_record.check_in_time.strftime('%H:%M:%S') if attendance_record.check_in_time else ''
|
||||||
elif column_key == 'qr_address':
|
elif column_key == 'qr_address':
|
||||||
cell.value = qr_record.location_address if qr_record else ''
|
# Use attendance-level qr_address first (set for dynamic QR check-ins),
|
||||||
|
# fall back to the QR code's location_address for standard QR.
|
||||||
|
cell.value = (
|
||||||
|
getattr(attendance_record, 'qr_address', None)
|
||||||
|
or (qr_record.location_address if qr_record else '')
|
||||||
|
or ''
|
||||||
|
)
|
||||||
elif column_key == 'address':
|
elif column_key == 'address':
|
||||||
# Check-in address logic based on location accuracy WITH HYPERLINKS
|
# Check-in address logic based on location accuracy WITH HYPERLINKS
|
||||||
# If location accuracy < 0.3 miles, use QR address; otherwise use actual check-in address
|
# If location accuracy < 0.3 miles, use QR address; otherwise use actual check-in address
|
||||||
@@ -2170,7 +2204,11 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
|
|||||||
accuracy_value = float(attendance_record.location_accuracy)
|
accuracy_value = float(attendance_record.location_accuracy)
|
||||||
if accuracy_value < 0.3:
|
if accuracy_value < 0.3:
|
||||||
# High accuracy - use QR code ADDRESS (not location) with hyperlink
|
# High accuracy - use QR code ADDRESS (not location) with hyperlink
|
||||||
address_text = qr_record.location_address if qr_record and qr_record.location_address else ''
|
address_text = (
|
||||||
|
getattr(attendance_record, 'qr_address', None)
|
||||||
|
or (qr_record.location_address if qr_record and qr_record.location_address else '')
|
||||||
|
or ''
|
||||||
|
)
|
||||||
if address_text and hasattr(qr_record, 'address_latitude') and hasattr(qr_record, 'address_longitude') and qr_record.address_latitude and qr_record.address_longitude:
|
if address_text and hasattr(qr_record, 'address_latitude') and hasattr(qr_record, 'address_longitude') and qr_record.address_latitude and qr_record.address_longitude:
|
||||||
# Format coordinates with 10 decimal places
|
# Format coordinates with 10 decimal places
|
||||||
lat_formatted = f"{float(qr_record.address_latitude):.10f}"
|
lat_formatted = f"{float(qr_record.address_latitude):.10f}"
|
||||||
@@ -2333,4 +2371,4 @@ def create_excel_export_ordered(selected_columns, column_names, filters):
|
|||||||
except Exception as log_error:
|
except Exception as log_error:
|
||||||
logger_handler.logger.warning(f"Could not log error: {log_error}")
|
logger_handler.logger.warning(f"Could not log error: {log_error}")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
+19
-4
@@ -701,6 +701,21 @@ def qr_checkin(qr_url):
|
|||||||
selected_location_name = request.form.get('selected_location_name', '').strip()
|
selected_location_name = request.form.get('selected_location_name', '').strip()
|
||||||
selected_location_address = request.form.get('selected_location_address', '').strip()
|
selected_location_address = request.form.get('selected_location_address', '').strip()
|
||||||
|
|
||||||
|
# Server-side guard: if this is a dynamic QR and no location was submitted,
|
||||||
|
# reject the check-in so "Dynamic" is never stored as location_name.
|
||||||
|
if getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and not selected_location_name:
|
||||||
|
logger_handler.logger.warning(
|
||||||
|
f"DYNAMIC check-in REJECTED: employee={employee_id}, "
|
||||||
|
f"qr_id={qr_code.id} — no location selected"
|
||||||
|
)
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': (
|
||||||
|
'Please select a location before checking in. / '
|
||||||
|
'Por favor seleccione una ubicación antes de registrarse.'
|
||||||
|
)
|
||||||
|
}), 400
|
||||||
|
|
||||||
if getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and selected_location_name:
|
if getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and selected_location_name:
|
||||||
effective_location_name = selected_location_name
|
effective_location_name = selected_location_name
|
||||||
effective_location_address = selected_location_address or ''
|
effective_location_address = selected_location_address or ''
|
||||||
@@ -800,11 +815,10 @@ 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
|
# Flag whether this check-in came from a dynamic QR scan
|
||||||
is_dynamic = getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and bool(selected_location_name)
|
is_dynamic = getattr(qr_code, 'qr_type', 'standard') == 'dynamic' and bool(selected_location_name)
|
||||||
record_location_name = (
|
# Store clean location_name (no suffix) so filtering/export works normally
|
||||||
f"{effective_location_name} (Dynamic QR)" if is_dynamic else effective_location_name
|
record_location_name = effective_location_name
|
||||||
)
|
|
||||||
# For dynamic QR: store the selected location's address as the QR-side address
|
# 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
|
record_qr_address = effective_location_address if is_dynamic else None
|
||||||
|
|
||||||
@@ -818,6 +832,7 @@ def qr_checkin(qr_url):
|
|||||||
ip_address=client_ip,
|
ip_address=client_ip,
|
||||||
location_name=record_location_name,
|
location_name=record_location_name,
|
||||||
qr_address=record_qr_address,
|
qr_address=record_qr_address,
|
||||||
|
is_dynamic_qr=is_dynamic,
|
||||||
latitude=location_data['latitude'],
|
latitude=location_data['latitude'],
|
||||||
longitude=location_data['longitude'],
|
longitude=location_data['longitude'],
|
||||||
accuracy=location_data['accuracy'],
|
accuracy=location_data['accuracy'],
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ function applyFilters() {
|
|||||||
record.event.toLowerCase().includes(searchTerm);
|
record.event.toLowerCase().includes(searchTerm);
|
||||||
|
|
||||||
const matchesLocation =
|
const matchesLocation =
|
||||||
!locationFilter || record.location === locationFilter;
|
!locationFilter || (record.location && record.location.trim() === locationFilter.trim());
|
||||||
const matchesEmployee =
|
const matchesEmployee =
|
||||||
employeeFilterIds.length === 0 ||
|
employeeFilterIds.length === 0 ||
|
||||||
employeeFilterIds.some(function(id) {
|
employeeFilterIds.some(function(id) {
|
||||||
@@ -514,6 +514,7 @@ function loadTableData() {
|
|||||||
? cells[10].textContent.trim()
|
? cells[10].textContent.trim()
|
||||||
: "",
|
: "",
|
||||||
isModified: row.classList.contains('modified-record'),
|
isModified: row.classList.contains('modified-record'),
|
||||||
|
isDynamic: row.dataset.isDynamic === '1',
|
||||||
verification_required: verificationData.required,
|
verification_required: verificationData.required,
|
||||||
verification_status: verificationData.status
|
verification_status: verificationData.status
|
||||||
};
|
};
|
||||||
@@ -629,6 +630,10 @@ function createTableRow(record, displayIndex) {
|
|||||||
if (record.isModified) {
|
if (record.isModified) {
|
||||||
row.classList.add('modified-record');
|
row.classList.add('modified-record');
|
||||||
}
|
}
|
||||||
|
// Apply blue-border highlight for Dynamic QR records
|
||||||
|
if (record.isDynamic) {
|
||||||
|
row.classList.add('dynamic-qr-record');
|
||||||
|
}
|
||||||
|
|
||||||
// Debug logging for first few records
|
// Debug logging for first few records
|
||||||
if (displayIndex <= 3) {
|
if (displayIndex <= 3) {
|
||||||
|
|||||||
+1444
-1411
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,16 @@
|
|||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<!-- Attendance-specific CSS -->
|
<!-- Attendance-specific CSS -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/attendance.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/attendance.css') }}">
|
||||||
|
<style>
|
||||||
|
/* Dynamic QR record row — subtle left accent to distinguish from standard records */
|
||||||
|
tr.dynamic-qr-record {
|
||||||
|
border-left: 3px solid #3b5bdb;
|
||||||
|
background-color: rgba(59, 91, 219, 0.03);
|
||||||
|
}
|
||||||
|
tr.dynamic-qr-record:hover {
|
||||||
|
background-color: rgba(59, 91, 219, 0.07) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<!-- Fullscreen CSS -->
|
<!-- Fullscreen CSS -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/attendance_fullscreen.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/attendance_fullscreen.css') }}">
|
||||||
<!-- Chart.js for analytics -->
|
<!-- Chart.js for analytics -->
|
||||||
@@ -221,7 +231,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for record in attendance_records %}
|
{% for record in attendance_records %}
|
||||||
<tr data-record-id="{{ record.id }}" class="{% if record.updated_timestamp > record.created_timestamp %}modified-record{% endif %}">
|
<tr data-record-id="{{ record.id }}"
|
||||||
|
data-is-dynamic="{{ '1' if (record.get('is_dynamic_qr') or record.location_name == 'Dynamic') else '0' }}"
|
||||||
|
class="{% if record.updated_timestamp > record.created_timestamp %}modified-record{% endif %}{% if record.get('is_dynamic_qr') or record.location_name == 'Dynamic' %} dynamic-qr-record{% endif %}">
|
||||||
<td>{{ loop.index }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="employee-info">
|
<div class="employee-info">
|
||||||
@@ -237,7 +249,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="location-info">
|
<div class="location-info">
|
||||||
<i class="fas fa-map-marker-alt"></i>
|
<i class="fas fa-map-marker-alt"></i>
|
||||||
{{ record.location_name }}
|
{{ record.location_name if record.location_name != 'Dynamic' else '(No location recorded)' }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -1082,4 +1094,5 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}());
|
}());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1457,4 +1457,4 @@
|
|||||||
</script>
|
</script>
|
||||||
<!-- ===== END Dynamic QR Type Toggle ===== -->
|
<!-- ===== END Dynamic QR Type Toggle ===== -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1333,6 +1333,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Enhanced JavaScript with preserved functionality -->
|
<!-- Enhanced JavaScript with preserved functionality -->
|
||||||
|
<!-- Dynamic QR: expose selected location to qr_destination.js via window globals -->
|
||||||
|
<script>
|
||||||
|
window._dynamicQRSelectedName = "";
|
||||||
|
window._dynamicQRSelectedAddress = "";
|
||||||
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/qr_destination.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/qr_destination.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/android_location_handler.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/android_location_handler.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -1547,7 +1552,11 @@
|
|||||||
// FIX 2: For dynamic QR codes, block submission if no location was selected
|
// FIX 2: For dynamic QR codes, block submission if no location was selected
|
||||||
var selLocField = document.getElementById("selected_location_name");
|
var selLocField = document.getElementById("selected_location_name");
|
||||||
var locationSelectCard = document.getElementById("locationSelectCard");
|
var locationSelectCard = document.getElementById("locationSelectCard");
|
||||||
if (locationSelectCard && selLocField && !selLocField.value.trim()) {
|
var _guardDropdown = document.getElementById("locationDropdown");
|
||||||
|
var _hasLocation = (selLocField && selLocField.value.trim()) ||
|
||||||
|
(_guardDropdown && _guardDropdown.value.trim());
|
||||||
|
|
||||||
|
if (locationSelectCard && !_hasLocation) {
|
||||||
// No location selected — send employee back to Step 1
|
// No location selected — send employee back to Step 1
|
||||||
document.getElementById("checkinFormCard").style.display = "none";
|
document.getElementById("checkinFormCard").style.display = "none";
|
||||||
locationSelectCard.style.display = "block";
|
locationSelectCard.style.display = "block";
|
||||||
@@ -1597,14 +1606,30 @@
|
|||||||
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
|
// Read selected location — window globals (most reliable), then hidden field, then dropdown
|
||||||
const selLocName = document.getElementById("selected_location_name");
|
var _selLocName = window._dynamicQRSelectedName || "";
|
||||||
const selLocAddr = document.getElementById("selected_location_address");
|
var _selLocAddr = window._dynamicQRSelectedAddress || "";
|
||||||
if (selLocName && selLocName.value) {
|
var _hiddenName = document.getElementById("selected_location_name");
|
||||||
formData.append("selected_location_name", selLocName.value);
|
var _hiddenAddr = document.getElementById("selected_location_address");
|
||||||
|
var _dropdown = document.getElementById("locationDropdown");
|
||||||
|
|
||||||
|
if (!_selLocName && _hiddenName && _hiddenName.value.trim()) {
|
||||||
|
_selLocName = _hiddenName.value.trim();
|
||||||
|
_selLocAddr = (_hiddenAddr && _hiddenAddr.value.trim()) ? _hiddenAddr.value.trim() : "";
|
||||||
}
|
}
|
||||||
if (selLocAddr && selLocAddr.value) {
|
if (!_selLocName && _dropdown && _dropdown.value.trim()) {
|
||||||
formData.append("selected_location_address", selLocAddr.value);
|
_selLocName = _dropdown.value.trim();
|
||||||
|
var _opt = _dropdown.options[_dropdown.selectedIndex];
|
||||||
|
_selLocAddr = _opt ? (_opt.getAttribute("data-address") || "") : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📍 [inline] Location — name:", _selLocName, "| addr:", _selLocAddr);
|
||||||
|
|
||||||
|
if (_selLocName) {
|
||||||
|
formData.append("selected_location_name", _selLocName);
|
||||||
|
}
|
||||||
|
if (_selLocAddr) {
|
||||||
|
formData.append("selected_location_address", _selLocAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add location data to form submission
|
// Add location data to form submission
|
||||||
@@ -1806,9 +1831,13 @@
|
|||||||
var selOpt = dropdown.options[dropdown.selectedIndex];
|
var selOpt = dropdown.options[dropdown.selectedIndex];
|
||||||
var address = selOpt ? (selOpt.getAttribute("data-address") || "") : "";
|
var address = selOpt ? (selOpt.getAttribute("data-address") || "") : "";
|
||||||
|
|
||||||
// Store selection in hidden fields
|
// Store selection in hidden fields AND window globals
|
||||||
|
// (window globals are read by qr_destination.js's submitCheckin)
|
||||||
document.getElementById("selected_location_name").value = name;
|
document.getElementById("selected_location_name").value = name;
|
||||||
document.getElementById("selected_location_address").value = address;
|
document.getElementById("selected_location_address").value = address;
|
||||||
|
window._dynamicQRSelectedName = name;
|
||||||
|
window._dynamicQRSelectedAddress = address;
|
||||||
|
console.log("✅ confirmLocationSelection: stored location =", name);
|
||||||
|
|
||||||
// FIX 1: Update the page header .location-info to show the selected location
|
// FIX 1: Update the page header .location-info to show the selected location
|
||||||
var locationInfoEl = document.querySelector(".location-info");
|
var locationInfoEl = document.querySelector(".location-info");
|
||||||
@@ -1834,9 +1863,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function backToLocationSelect() {
|
function backToLocationSelect() {
|
||||||
// Clear selection
|
// Clear selection from hidden fields and window globals
|
||||||
document.getElementById("selected_location_name").value = "";
|
document.getElementById("selected_location_name").value = "";
|
||||||
document.getElementById("selected_location_address").value = "";
|
document.getElementById("selected_location_address").value = "";
|
||||||
|
window._dynamicQRSelectedName = "";
|
||||||
|
window._dynamicQRSelectedAddress = "";
|
||||||
var badge = document.getElementById("selectedLocationBadge");
|
var badge = document.getElementById("selectedLocationBadge");
|
||||||
if (badge) badge.style.display = "none";
|
if (badge) badge.style.display = "none";
|
||||||
|
|
||||||
|
|||||||
@@ -107,8 +107,43 @@ def run_migration():
|
|||||||
else:
|
else:
|
||||||
print("\u2139\ufe0f Column attendance_data.qr_address already exists \u2014 skipped.")
|
print("\u2139\ufe0f Column attendance_data.qr_address already exists \u2014 skipped.")
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Step 5: Add is_dynamic_qr column to attendance_data if absent
|
||||||
|
# Flags records that came from a Dynamic QR code scan
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
if 'is_dynamic_qr' not in att_cols:
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
conn.execute(text(
|
||||||
|
"ALTER TABLE attendance_data "
|
||||||
|
"ADD COLUMN is_dynamic_qr TINYINT(1) NOT NULL DEFAULT 0"
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
print("\u2705 Added column: attendance_data.is_dynamic_qr")
|
||||||
|
else:
|
||||||
|
print("\u2139\ufe0f Column attendance_data.is_dynamic_qr already exists \u2014 skipped.")
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
# Step 6: Backfill is_dynamic_qr=1 on old broken records
|
||||||
|
# Records created before this migration where location_name='Dynamic'
|
||||||
|
# are legacy dynamic QR check-ins that were saved with the wrong name.
|
||||||
|
# Mark them so the badge shows correctly in the attendance report.
|
||||||
|
# ----------------------------------------------------------------
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
result = conn.execute(text(
|
||||||
|
"UPDATE attendance_data "
|
||||||
|
"SET is_dynamic_qr = 1 "
|
||||||
|
"WHERE location_name = 'Dynamic' AND is_dynamic_qr = 0"
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
updated = result.rowcount
|
||||||
|
if updated > 0:
|
||||||
|
print(f"\u2705 Backfilled is_dynamic_qr=1 on {updated} legacy 'Dynamic' record(s)")
|
||||||
|
else:
|
||||||
|
print("\u2139\ufe0f No legacy 'Dynamic' records to backfill.")
|
||||||
|
|
||||||
print("\nMigration complete.")
|
print("\nMigration complete.")
|
||||||
print("All existing QR codes remain fully unaffected (qr_type = 'standard').")
|
print("All existing QR codes remain fully unaffected (qr_type = 'standard').")
|
||||||
|
print("All existing attendance records default to is_dynamic_qr = 0 (False).")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Reference in New Issue
Block a user