From 491a6f537c00dfa6779790b632568b000c4dcaca Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 15 Aug 2025 09:56:44 -0400 Subject: [PATCH 01/10] Update tracking page to detect Location Services --- static/js/qr_destination.js | 150 ++++++++++++++++++++++++++++++++++ templates/qr_destination.html | 121 +++++++++++++++++++++++++++ 2 files changed, 271 insertions(+) diff --git a/static/js/qr_destination.js b/static/js/qr_destination.js index 435fc54..358c772 100644 --- a/static/js/qr_destination.js +++ b/static/js/qr_destination.js @@ -59,6 +59,7 @@ document.addEventListener("DOMContentLoaded", function () { // CRITICAL: Initialize systems in correct order initializeLanguage(); + initializeLocationServicesCheck(); initializeStaffIdPersistence(); initializeForm(); initializeLocation(); @@ -733,3 +734,152 @@ function startClock() { updateClock(); setInterval(updateClock, 1000); } + +function checkLocationServicesStatus() { + console.log("📱 Checking location services status..."); + + // Check if geolocation is supported + if (!navigator.geolocation) { + console.log("❌ Geolocation not supported by this browser"); + showLocationServicesWarning("not_supported"); + return; + } + + // Test location access with a quick check + const timeoutId = setTimeout(() => { + console.log("⏰ Location permission check timed out"); + showLocationServicesWarning("timeout"); + }, 3000); // 3 second timeout + + navigator.geolocation.getCurrentPosition( + (position) => { + // Success - location services are working + clearTimeout(timeoutId); + console.log("✅ Location services are available and enabled"); + hideLocationServicesWarning(); + }, + (error) => { + // Error - location services may be disabled + clearTimeout(timeoutId); + console.log("❌ Location services error:", error.message); + + switch (error.code) { + case error.PERMISSION_DENIED: + showLocationServicesWarning("permission_denied"); + break; + case error.POSITION_UNAVAILABLE: + showLocationServicesWarning("position_unavailable"); + break; + case error.TIMEOUT: + showLocationServicesWarning("timeout"); + break; + default: + showLocationServicesWarning("unknown_error"); + break; + } + }, + { + enableHighAccuracy: false, + timeout: 2500, + maximumAge: 60000 + } + ); +} + +/** + * Show location services warning banner + */ +function showLocationServicesWarning(errorType) { + // Remove existing warning if present + hideLocationServicesWarning(); + + const warningMessages = { + en: { + not_supported: "Location services are not supported by your browser.
Los servicios de ubicación no son compatibles con su navegador.", + permission_denied: "Location access has been denied. Please enable location services for accurate check-in.
Se ha denegado el acceso a la ubicación. Habilite los servicios de ubicación para un registro preciso.", + position_unavailable: "Location services appear to be disabled. Please turn on location services for accurate check-in.
Los servicios de ubicación parecen estar deshabilitados. Active los servicios de ubicación para un registro preciso.", + timeout: "Location services may be disabled. Please check your location settings for accurate check-in.
Los servicios de ubicación pueden estar deshabilitados. Verifique su configuración de ubicación para un registro preciso.", + unknown_error: "Unable to access location services. Please check your location settings.
No se puede acceder a los servicios de ubicación. Verifique su configuración de ubicación." + }, + es: { + not_supported: "Los servicios de ubicación no son compatibles con su navegador.", + permission_denied: "Se ha denegado el acceso a la ubicación. Habilite los servicios de ubicación para un registro preciso.", + position_unavailable: "Los servicios de ubicación parecen estar deshabilitados. Active los servicios de ubicación para un registro preciso.", + timeout: "Los servicios de ubicación pueden estar deshabilitados. Verifique su configuración de ubicación para un registro preciso.", + unknown_error: "No se puede acceder a los servicios de ubicación. Verifique su configuración de ubicación." + } + }; + + const currentLang = currentLanguage || 'en'; + const message = warningMessages[currentLang][errorType] || warningMessages['en'][errorType]; + + // Create warning banner + const warningBanner = document.createElement('div'); + warningBanner.id = 'locationServicesWarning'; + warningBanner.className = 'location-warning-banner'; + warningBanner.innerHTML = ` +
+ +
+ ${message} +
+ + +
+
+
+ `; + + // Insert warning at the top of the page + const container = document.querySelector('.destination-container'); + if (container) { + container.insertBefore(warningBanner, container.firstChild); + } + + // Log warning event + console.log(`⚠️ Location services warning displayed: ${errorType}`); +} + +/** + * Hide location services warning banner + */ +function hideLocationServicesWarning() { + const existingWarning = document.getElementById('locationServicesWarning'); + if (existingWarning) { + existingWarning.remove(); + console.log("✅ Location services warning hidden"); + } +} + +/** + * Modified DOMContentLoaded event handler + * Add this to your existing initialization + */ +function initializeLocationServicesCheck() { + // Check location services status when page loads + setTimeout(() => { + checkLocationServicesStatus(); + }, 1000); // Small delay to ensure page is fully loaded + + // Also check before form submission + const originalHandleFormSubmit = handleFormSubmit; + window.handleFormSubmit = function(event) { + // Quick location check before submission + checkLocationServicesStatus(); + + // Continue with original form submission after brief delay + setTimeout(() => { + originalHandleFormSubmit.call(this, event); + }, 500); + }; +} diff --git a/templates/qr_destination.html b/templates/qr_destination.html index 1924017..951bd2b 100644 --- a/templates/qr_destination.html +++ b/templates/qr_destination.html @@ -601,6 +601,127 @@ gap: 0.75rem; } } + .location-warning-banner { + background: linear-gradient(135deg, #fef3c7, #fde68a); + border: 2px solid #f59e0b; + border-radius: var(--border-radius); + margin-bottom: 1rem; + padding: 1rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + animation: slideInWarning 0.5s ease-out; + } + + .warning-content { + display: flex; + align-items: flex-start; + gap: 0.75rem; + } + + .warning-icon { + color: #d97706; + font-size: 1.25rem; + margin-top: 0.125rem; + flex-shrink: 0; + } + + .warning-text { + flex: 1; + } + + .warning-message { + display: block; + color: #92400e; + font-weight: 500; + font-size: 0.95rem; + line-height: 1.5; + margin-bottom: 0.75rem; + } + + .warning-actions { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + } + + .warning-retry-btn, + .warning-dismiss-btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 0.75rem; + border: none; + border-radius: calc(var(--border-radius) * 0.75); + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition); + text-decoration: none; + } + + .warning-retry-btn { + background: #f59e0b; + color: white; + } + + .warning-retry-btn:hover { + background: #d97706; + transform: translateY(-1px); + } + + .warning-dismiss-btn { + background: rgba(156, 163, 175, 0.2); + color: #6b7280; + border: 1px solid rgba(156, 163, 175, 0.3); + } + + .warning-dismiss-btn:hover { + background: rgba(156, 163, 175, 0.3); + color: #4b5563; + } + + /* Warning animation */ + @keyframes slideInWarning { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + /* Language styling for warning buttons */ + .warning-retry-btn .language-separator, + .warning-dismiss-btn .language-separator { + opacity: 0.7; + margin: 0 0.25rem; + } + + .warning-retry-btn .spanish-text, + .warning-dismiss-btn .spanish-text { + font-style: italic; + opacity: 0.9; + } + + /* Responsive adjustments for warning */ + @media (max-width: 640px) { + .warning-content { + flex-direction: column; + gap: 0.5rem; + } + + .warning-actions { + justify-content: center; + } + + .warning-retry-btn, + .warning-dismiss-btn { + flex: 1; + justify-content: center; + min-width: 120px; + } + } From 47760d55c0df71787ebefdf5567e47ba8ff62421 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 15 Aug 2025 12:11:22 -0400 Subject: [PATCH 02/10] Update attendance report display --- app.py | 9 +++++++-- static/js/attendance_report.js | 4 ++-- templates/attendance_report.html | 32 ++++++++++---------------------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/app.py b/app.py index b2436d4..4511d05 100644 --- a/app.py +++ b/app.py @@ -3098,9 +3098,14 @@ def attendance_report(): # Execute query query_result = db.session.execute(text(base_query), params) attendance_records = query_result.fetchall() - print(f"✅ Found {len(attendance_records)} attendance records") - + # Log first 3 records to verify QR address data + for i, record in enumerate(attendance_records[:3]): + print(f"📊 Record {i+1}: Employee={record.employee_id}") + print(f" QR Address: {getattr(record, 'qr_address', 'NOT_FOUND')}") + print(f" Check-in Address: {getattr(record, 'checked_in_address', 'NOT_FOUND')}") + print(f" Location Accuracy: {getattr(record, 'location_accuracy', 'NOT_FOUND')}") + # FIXED: Process records to add calculated fields with proper datetime handling processed_records = [] for record in attendance_records: diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 69a692a..cb7df51 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -812,8 +812,8 @@ function createTableRow(record, displayIndex) {
- - + + ${ record.qr_address.length > 50 ? record.qr_address.substring(0, 50) + "..." diff --git a/templates/attendance_report.html b/templates/attendance_report.html index aa907d7..0a5e668 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -223,28 +223,16 @@ {% endif %}
- -
- - {% if record.address_source == 'qr' %} - - - - {{ record.qr_address[:45] }}{% if record.qr_address|length > 45 %}...{% endif %} - - {% else %} - - - {% if record.location_accuracy and record.location_accuracy > 0.5 %} - - {% endif %} - {{ record.checked_in_address[:45] }}{% if record.checked_in_address|length > 45 %}...{% endif %} - - {% endif %} + +
+ + + {% if record.qr_address %} + {{ record.qr_address[:50] }}{{ '...' if record.qr_address|length > 50 else '' }} + {% else %} + N/A + {% endif %} +
From 944c19b98ad1422285542f0a56a8bc22b2e37460 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 15 Aug 2025 12:30:13 -0400 Subject: [PATCH 03/10] Updated: saving columns order --- templates/export_configuration.html | 64 ----------------------------- 1 file changed, 64 deletions(-) diff --git a/templates/export_configuration.html b/templates/export_configuration.html index 60c5a79..d3acd5b 100644 --- a/templates/export_configuration.html +++ b/templates/export_configuration.html @@ -236,74 +236,10 @@ function goBackToReport() { window.location.href = url; } -function loadSavedPreferences() { - // Load preferences from localStorage or session - try { - const savedPrefs = localStorage.getItem('exportPreferences'); - if (savedPrefs) { - const prefs = JSON.parse(savedPrefs); - - // Apply saved column selections - if (prefs.selected_columns) { - const checkboxes = document.querySelectorAll('input[name="selected_columns"]'); - checkboxes.forEach(cb => { - cb.checked = prefs.selected_columns.includes(cb.value); - toggleColumnName(cb.value); - }); - } - - // Apply saved column names - if (prefs.column_names) { - Object.keys(prefs.column_names).forEach(key => { - const input = document.getElementById('name_' + key); - if (input) { - input.value = prefs.column_names[key]; - } - }); - } - - // Apply saved column order - if (prefs.column_order) { - // Column order will be applied when selected columns are updated - window.savedColumnOrder = prefs.column_order; - } - - updateSelectedColumnsList(); - updatePreview(); - } - } catch (e) { - console.log('No saved preferences found'); - } -} - // Form submission handler to save preferences document.getElementById('exportForm').addEventListener('submit', function() { savePreferences(); updateColumnOrderField(); }); - -function savePreferences() { - const selectedColumns = Array.from(document.querySelectorAll('input[name="selected_columns"]:checked')) - .map(cb => cb.value); - - const columnNames = {}; - selectedColumns.forEach(col => { - const input = document.getElementById('name_' + col); - if (input) { - columnNames[col] = input.value; - } - }); - - // Get current column order from the sortable list - const columnOrder = getCurrentColumnOrder(); - - const prefs = { - selected_columns: selectedColumns, - column_names: columnNames, - column_order: columnOrder - }; - - localStorage.setItem('exportPreferences', JSON.stringify(prefs)); -} {% endblock %} \ No newline at end of file From ebcd8a231867966baffa4e8b0d0e64712b6cde8a Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 15 Aug 2025 12:48:47 -0400 Subject: [PATCH 04/10] Update attendance report display --- static/css/attendance.css | 34 +++++----------------------------- static/js/attendance_report.js | 26 ++++++++++++-------------- 2 files changed, 17 insertions(+), 43 deletions(-) diff --git a/static/css/attendance.css b/static/css/attendance.css index eba85ac..d2f7c55 100644 --- a/static/css/attendance.css +++ b/static/css/attendance.css @@ -993,40 +993,16 @@ opacity: 0.8; } -/* Location accuracy level colors - based on distance */ -.location-accuracy-badge.accuracy-excellent { - background: var(--success-light); - color: var(--success-color); - border: 1px solid rgba(5, 150, 105, 0.3); -} - +/* Location accuracy level colors - 2-level system based on 0.5 mile threshold */ .location-accuracy-badge.accuracy-accurate { - background: var(--success-light); - color: var(--success-color); - border: 1px solid rgba(5, 150, 105, 0.3); -} - -.location-accuracy-badge.accuracy-good { - background: #ecfdf5; + background: #dcfce7; color: #059669; - border: 1px solid rgba(5, 150, 105, 0.2); -} - -.location-accuracy-badge.accuracy-fair { - background: var(--warning-light); - color: var(--warning-color); - border: 1px solid rgba(245, 158, 11, 0.3); -} - -.location-accuracy-badge.accuracy-poor { - background: var(--danger-light); - color: var(--danger-color); - border: 1px solid rgba(220, 38, 38, 0.3); + border: 1px solid rgba(5, 150, 105, 0.3); } .location-accuracy-badge.accuracy-inaccurate { - background: var(--danger-light); - color: var(--danger-color); + background: #fee2e2; + color: #dc2626; border: 1px solid rgba(220, 38, 38, 0.3); } diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index cb7df51..2c93ba7 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -657,16 +657,14 @@ function extractLocationAccuracy(cell) { } function extractLocationAccuracyLevel(cell) { - const text = cell.textContent.toLowerCase(); - if ( - text.includes("high") || - text.includes("excellent") || - text.includes("good") - ) - return "High"; - if (text.includes("medium") || text.includes("fair")) return "Medium"; - if (text.includes("low") || text.includes("poor")) return "Low"; - return "Unknown"; + // Get the numerical accuracy value from the cell + const accuracy = extractLocationAccuracy(cell); + + // Return 2-level accuracy based on 0.5-mile threshold + if (accuracy !== null && accuracy !== undefined) { + return accuracy <= 0.5 ? "accurate" : "inaccurate"; + } + return "unknown"; } function createTableRow(record, displayIndex) { @@ -690,17 +688,17 @@ function createTableRow(record, displayIndex) { }" title="Distance between QR location and check-in location: ${ record.location_accuracy - } miles"> + } miles - ${record.accuracy_level}"> ${record.location_accuracy.toFixed(3)} mi (${record.accuracy_level}) - ` + ` : ` Unknown - `; + `; - // FIXED: Address display logic based on location accuracy + // Address display logic based on location accuracy let addressDisplayHTML = ""; let addressToShow = record.checked_in_address; let addressIcon = "fas fa-location-arrow"; From ed88d5fe3a05f016652dcd336daaccac2900fb86 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 15 Aug 2025 13:49:07 -0400 Subject: [PATCH 05/10] Fixed Create QR Code issue --- app.py | 22 ++++++++++++++++------ static/css/attendance.css | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 4511d05..0c09244 100644 --- a/app.py +++ b/app.py @@ -2399,17 +2399,27 @@ def create_qr_code(): 'longitude': address_longitude, 'coordinate_accuracy': coordinate_accuracy } - logger_handler.logger.info(f"User {session['username']} created QR code: {json.dumps(log_data)}") - logger_handler.log_user_action( - f'QR Code created: {name} with event type: {location_event}', - 'create', - additional_info={'location_event': location_event} + qr_data_for_logging = { + 'location': location, + 'location_address': location_address, + 'location_event': location_event, + 'has_coordinates': has_coordinates, + 'latitude': address_latitude, + 'longitude': address_longitude, + 'coordinate_accuracy': coordinate_accuracy + } + + logger_handler.log_qr_code_created( + qr_code_id=new_qr_code.id, + qr_code_name=name, + created_by_user_id=session['user_id'], + qr_data=qr_data_for_logging ) # Success message with coordinates info project_info = f" in project '{project.name}'" if project else "" coord_info = f" with coordinates ({new_qr_code.coordinates_display})" if has_coordinates else "" - + flash(f'QR Code "{name}" created successfully{project_info}{coord_info}! URL: {qr_url}', 'success') return redirect(url_for('dashboard')) diff --git a/static/css/attendance.css b/static/css/attendance.css index d2f7c55..d9d23de 100644 --- a/static/css/attendance.css +++ b/static/css/attendance.css @@ -5,7 +5,7 @@ /* Page Container */ .attendance-page { - max-width: 1400px; + max-width: 1600px; margin: 0 auto; padding: var(--spacing-6); min-height: 100vh; From ec18c62ed80bd471887404e63a4313d848ae30c8 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 15 Aug 2025 15:48:41 -0400 Subject: [PATCH 06/10] Fix export page role issue --- app.py | 29 ++++++++++++++++++++++++----- static/js/attendance_report.js | 22 +++++++++++++++++++++- templates/attendance_report.html | 5 ++++- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 0c09244..aeaf9fc 100644 --- a/app.py +++ b/app.py @@ -70,6 +70,10 @@ class User(db.Model): def has_staff_permissions(self): """Check if user has staff-level permissions (includes new roles)""" return self.role in STAFF_LEVEL_ROLES + + def has_export_permissions(user_role): + """Check if user role has export permissions""" + return user_role in ['admin', 'payroll'] def get_role_display_name(self): """Get user-friendly role name""" @@ -3001,7 +3005,7 @@ def toggle_qr_status_api(qr_id): return redirect(url_for('dashboard')) @app.route('/attendance') -# @admin_required +@login_required def attendance_report(): """Safe attendance report with backward compatibility for location_accuracy and fixed datetime handling""" try: @@ -3009,6 +3013,7 @@ def attendance_report(): # Log attendance report access try: + user_role = session.get('role', 'unknown') logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed attendance report") except Exception as log_error: print(f"⚠️ Logging error (non-critical): {log_error}") @@ -3090,7 +3095,7 @@ def attendance_report(): conditions.append("ad.employee_id LIKE :employee") params['employee'] = f"%{employee_filter}%" - # FIXED: Apply project filter using SQL approach (not ORM) + # Apply project filter using SQL approach (not ORM) if project_filter: conditions.append("qc.project_id = :project_id") params['project_id'] = int(project_filter) @@ -3279,7 +3284,8 @@ def attendance_report(): project_filter=project_filter, today_date=today_date, current_date_formatted=current_date_formatted, - has_location_accuracy_feature=has_location_accuracy) + has_location_accuracy_feature=has_location_accuracy, + user_role=user_role) except Exception as e: print(f"❌ Error loading attendance report: {e}") @@ -3491,14 +3497,21 @@ def attendance_stats_api(): return jsonify({'error': 'Failed to fetch attendance statistics'}), 500 @app.route('/export-configuration') -@admin_required +@login_required def export_configuration(): """Route to display export configuration page""" try: print("📊 Export configuration route accessed") + # Check if user has export permissions + user_role = session.get('role') + if user_role not in ['admin', 'payroll']: + logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted to access export configuration without permissions") + flash('Access denied. Only administrators and payroll staff can access export configuration.', 'error') + return redirect(url_for('attendance_report')) # Log export configuration access using your existing logger try: + logger_handler.logger.info(f"User {session.get('username', 'unknown')} (role: {user_role}) accessed export configuration") logger_handler.logger.info(f"User {session.get('username', 'unknown')} accessed export configuration page") except Exception as log_error: print(f"⚠️ Logging error (non-critical): {log_error}") @@ -3573,10 +3586,16 @@ def export_configuration(): return redirect(url_for('attendance_report')) @app.route('/generate-excel-export', methods=['POST']) -@admin_required +@login_required def generate_excel_export(): """Generate and download Excel file with selected columns in specified order""" try: + user_role = session.get('role') + if user_role not in ['admin', 'payroll']: + logger_handler.logger.warning(f"User {session.get('username', 'unknown')} (role: {user_role}) attempted unauthorized Excel export") + flash('Access denied. Only administrators and payroll staff can export data.', 'error') + return redirect(url_for('attendance_report')) + print("📊 Excel export generation started") # Log export action using your existing logger diff --git a/static/js/attendance_report.js b/static/js/attendance_report.js index 2c93ba7..98cd4dc 100644 --- a/static/js/attendance_report.js +++ b/static/js/attendance_report.js @@ -1017,8 +1017,19 @@ function exportAttendanceWithAccuracy() { } function exportAttendance() { + // Check user role before proceeding + const userRole = window.userRole; // Read from global variable set in template + console.log("Template - session.role:", '{{ session.role }}'); + console.log("Template - window.userRole set to:", window.userRole); + + if (!['admin', 'payroll'].includes(userRole)) { + console.log("Export access denied - insufficient privileges"); + alert("Access denied. Only administrators and payroll staff can export data."); + return; + } + // Log export action - console.log("Export button clicked - redirecting to configuration page"); + console.log(`Export button clicked by ${userRole} - redirecting to configuration page`); // Get current filters const currentFilters = getCurrentFilters(); @@ -1053,6 +1064,15 @@ function getCurrentFilters() { // Add a quick CSV export function as backup (keep existing functionality) function exportAttendanceCSV() { + // Check user role before proceeding + const userRole = window.userRole; + + if (!['admin', 'payroll'].includes(userRole)) { + console.log("CSV export access denied - insufficient privileges"); + alert("Access denied. Only administrators and payroll staff can export data."); + return; + } + // Build export URL with current filters for CSV const params = new URLSearchParams(); const filters = getCurrentFilters(); diff --git a/templates/attendance_report.html b/templates/attendance_report.html index 0a5e668..53ebb4c 100644 --- a/templates/attendance_report.html +++ b/templates/attendance_report.html @@ -10,7 +10,7 @@ {% endblock %} {% block content %} -
+
@@ -22,10 +22,12 @@
+ {% if session.role in ['admin', 'payroll'] %} + {% endif %} @@ -902,10 +1007,6 @@ - - {% endif %}
@@ -952,41 +1053,58 @@ data-qr-event="{{ qr.location_event }}" data-qr-url="{{ qr.qr_url if qr.qr_url else '' }}" onclick="openQRModalFromData(this)"> -
-
+ +
+
QR Code for {{ qr.name }}
-
-

{{ qr.name }}

-

- - {{ qr.location }} -

+ +
+

{{ qr.name }}

+
+
+ + {{ qr.location }} +
+ + {% if qr.location_address %} +
+ + {{ qr.location_address }} +
+ {% endif %} + + {% if qr.qr_url %} +
+ + {{ qr.qr_url[:50] }}{% if qr.qr_url|length > 50 %}...{% endif %} +
+ {% endif %} +
+
+ +
{{ 'Active' if qr.active_status else 'Inactive' }}
-
- -
- - - - - - {% if session.role == 'admin' %} - - - - {% endif %} +
+ + + + + + + {% if session.role == 'admin' %} + + {% endif %} +
{% endfor %} @@ -1199,50 +1317,10 @@ class ProjectDashboardManager { } } - async deleteQRCode(qrId, qrName) { - const confirmMessage = `Are you sure you want to permanently delete the QR code "${qrName}"?\n\nThis action cannot be undone.`; - - if (!confirm(confirmMessage)) return; - - try { - const response = await fetch(`/qr-codes/${qrId}/delete`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - } - }); - - if (response.ok) { - if (window.showToast) { - window.showToast(`QR code "${qrName}" deleted successfully`, 'success'); - } - - const qrCard = document.querySelector(`[data-qr-id="${qrId}"]`); - if (qrCard) { - qrCard.style.transition = 'all 0.3s ease'; - qrCard.style.opacity = '0'; - qrCard.style.transform = 'translateY(-20px)'; - - setTimeout(() => { - qrCard.remove(); - }, 300); - } - - } else { - throw new Error('Failed to delete QR code'); - } - } catch (error) { - console.error('Delete failed:', error); - if (window.showToast) { - window.showToast('Failed to delete QR code', 'error'); - } - } - } - openQRModalFromData(element) { const qrData = { name: element.dataset.qrName, - image: element.querySelector('.qr-preview img').src, + image: element.querySelector('img').src, location: element.dataset.qrLocation, address: element.dataset.qrAddress, event: element.dataset.qrEvent, @@ -1321,11 +1399,10 @@ class ProjectDashboardManager { downloadQRFromCard(button) { const qrCard = button.closest('.qr-card'); - const img = qrCard.querySelector('.qr-preview img'); + const img = qrCard.querySelector('img'); const qrName = qrCard.dataset.qrName; if (img && img.src) { - // Extract base64 data from the image src const base64Data = img.src.split('base64,')[1]; this.downloadQR(base64Data, qrName); } @@ -1421,7 +1498,6 @@ document.addEventListener('DOMContentLoaded', function() { window.toggleProject = (projectId) => dashboardManager.toggleProject(projectId); window.toggleQRCodeStatus = (qrId) => dashboardManager.toggleQRCodeStatus(qrId); - window.deleteQRCode = (qrId, qrName) => dashboardManager.deleteQRCode(qrId, qrName); window.openQRModalFromData = (element) => dashboardManager.openQRModalFromData(element); window.closeQRModal = () => dashboardManager.closeQRModal(); window.downloadModalQR = () => dashboardManager.downloadModalQR(); From 07249ce84ca2f0a1ecd2c33f74c5a55c644f0778 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 15 Aug 2025 17:29:17 -0400 Subject: [PATCH 09/10] Minor change --- templates/dashboard.html | 195 +++++++++++++++++++++++++++++++-------- 1 file changed, 158 insertions(+), 37 deletions(-) diff --git a/templates/dashboard.html b/templates/dashboard.html index 2376e81..702a4ce 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -173,8 +173,8 @@ .project-qr-grid { display: grid; - grid-template-columns: 1fr; - gap: var(--spacing-3); + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: var(--spacing-4); padding: var(--spacing-6); } @@ -204,14 +204,15 @@ .qr-card-compact { display: flex; align-items: center; - gap: var(--spacing-4); + gap: var(--spacing-3); width: 100%; padding: var(--spacing-3); + min-height: 80px; } .qr-preview-compact { - width: 70px; - height: 70px; + width: 60px; + height: 60px; flex-shrink: 0; border-radius: var(--radius-md); overflow: hidden; @@ -232,24 +233,31 @@ .qr-info-compact { flex: 1; min-width: 0; + display: flex; + flex-direction: column; + gap: var(--spacing-1); } .qr-name-compact { - font-size: var(--font-size-base); + font-size: var(--font-size-sm); font-weight: 600; color: var(--gray-900); - margin: 0 0 var(--spacing-1) 0; - line-height: 1.4; + margin: 0; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .qr-details-row { display: flex; - gap: var(--spacing-4); + gap: var(--spacing-3); flex-wrap: wrap; + align-items: center; } .qr-url-short { - max-width: 200px; + max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -257,7 +265,6 @@ .qr-status-compact { flex-shrink: 0; - margin: 0 var(--spacing-3); } .qr-actions-compact { @@ -269,13 +276,14 @@ /* Standard QR Card Layout for Project Cards */ .qr-card-layout { display: flex; - gap: var(--spacing-4); + gap: var(--spacing-3); margin-bottom: var(--spacing-3); + align-items: flex-start; } .qr-preview-large { - width: 100px; - height: 100px; + width: 80px; + height: 80px; flex-shrink: 0; border-radius: var(--radius-md); overflow: hidden; @@ -297,48 +305,53 @@ flex: 1; display: flex; flex-direction: column; - gap: var(--spacing-2); + gap: var(--spacing-1); + min-width: 0; } .qr-name { - font-size: var(--font-size-base); + font-size: var(--font-size-sm); font-weight: 600; color: var(--gray-900); margin: 0 0 var(--spacing-1) 0; - line-height: 1.4; + line-height: 1.3; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .qr-detail-item { display: flex; align-items: center; - gap: 0.5rem; - font-size: var(--font-size-sm); + gap: 0.4rem; + font-size: var(--font-size-xs); color: var(--gray-600); min-width: 0; } .qr-detail-item i { - font-size: 0.75rem; + font-size: 0.65rem; width: auto; flex-shrink: 0; color: var(--gray-400); } .qr-detail-item span { - word-break: break-word; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + min-width: 0; } .qr-status-badge { display: inline-flex; align-items: center; gap: var(--spacing-1); - padding: 0.25rem 0.5rem; + padding: 0.2rem 0.4rem; border-radius: var(--radius-full); - font-size: var(--font-size-xs); + font-size: 0.65rem; font-weight: 500; + margin-top: var(--spacing-1); } .qr-status-badge.active { @@ -353,15 +366,16 @@ .qr-card-actions { display: flex; - gap: var(--spacing-2); + gap: var(--spacing-1); justify-content: flex-start; - padding-top: var(--spacing-3); + padding-top: var(--spacing-2); border-top: 1px solid var(--gray-200); + margin-top: var(--spacing-2); } .qr-action-btn { - width: 32px; - height: 32px; + width: 28px; + height: 28px; border-radius: var(--radius-md); border: none; display: flex; @@ -369,7 +383,7 @@ justify-content: center; cursor: pointer; transition: var(--transition); - font-size: var(--font-size-sm); + font-size: 0.75rem; text-decoration: none; } @@ -679,10 +693,103 @@ min-width: 140px; } -/* Animations */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } +/* Image Lightbox Styles */ +.image-lightbox { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; + cursor: pointer; +} + +.lightbox-content { + position: relative; + max-width: 90vw; + max-height: 90vh; + text-align: center; +} + +.lightbox-image { + max-width: 100%; + max-height: 80vh; + border-radius: var(--radius-lg); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); + background: var(--white); + padding: var(--spacing-4); +} + +.lightbox-close { + position: absolute; + top: -50px; + right: 0; + background: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + font-size: 1.2rem; + color: var(--gray-700); + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + justify-content: center; +} + +.lightbox-close:hover { + background: var(--white); + color: var(--gray-900); +} + +.lightbox-info { + color: var(--white); + margin-top: var(--spacing-4); + font-size: var(--font-size-lg); + font-weight: 500; +} + +.lightbox-hint { + color: rgba(255, 255, 255, 0.7); + margin-top: var(--spacing-2); + font-size: var(--font-size-sm); +} + +/* Make QR images clickable */ +.qr-preview-large, +.qr-preview-compact { + cursor: zoom-in; + position: relative; +} + +.qr-preview-large::after, +.qr-preview-compact::after { + content: '🔍'; + position: absolute; + top: 2px; + right: 2px; + background: rgba(0, 0, 0, 0.7); + color: white; + border-radius: 50%; + width: 18px; + height: 18px; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: var(--transition); +} + +.qr-card:hover .qr-preview-large::after, +.qr-card:hover .qr-preview-compact::after { + opacity: 1; } @keyframes slideIn { @@ -718,10 +825,17 @@ gap: var(--spacing-2); } + .project-qr-grid { + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--spacing-3); + padding: var(--spacing-4); + } + .qr-card-compact { flex-direction: column; text-align: center; - gap: var(--spacing-3); + gap: var(--spacing-2); + padding: var(--spacing-2); } .qr-info-compact { @@ -730,7 +844,7 @@ .qr-details-row { flex-direction: column; - gap: var(--spacing-2); + gap: var(--spacing-1); align-items: center; } @@ -745,16 +859,17 @@ .qr-card-layout { flex-direction: column; text-align: center; + align-items: center; } .qr-preview-large { - align-self: center; - width: 80px; - height: 80px; + width: 70px; + height: 70px; } .qr-details { align-items: center; + width: 100%; } .qr-detail-item { @@ -807,6 +922,12 @@ overflow: visible; text-overflow: unset; } + + .project-qr-grid { + grid-template-columns: 1fr; + gap: var(--spacing-2); + padding: var(--spacing-3); + } .modal-header { padding: var(--spacing-4); From 18ecfafd80858977c09202987d8a3c08f99d8e36 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Fri, 15 Aug 2025 17:33:12 -0400 Subject: [PATCH 10/10] Minor change --- templates/dashboard.html | 72 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/templates/dashboard.html b/templates/dashboard.html index 702a4ce..3733f67 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -792,6 +792,12 @@ opacity: 1; } +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + @keyframes slideIn { from { opacity: 0; @@ -1083,7 +1089,7 @@ onclick="openQRModalFromData(this)">
-
+
QR Code for {{ qr.name }}
@@ -1176,7 +1182,7 @@ onclick="openQRModalFromData(this)">
-
+
QR Code for {{ qr.name }}
@@ -1257,6 +1263,18 @@ {% endif %}
+ +
+ +
+